From 3c7a060655dec45ab37abf3fcd0379421be15e59 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 11 Sep 2025 23:08:59 +0530 Subject: [PATCH 01/41] feat: show image ribbon gallery when an image element is called with random images --- .../BrowserScripts/RemoteFunctions.js | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 14cc4c584f..4425553805 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1911,6 +1911,155 @@ function RemoteFunctions(config = {}) { } }; + /** + * when user clicks on an image we call this, + * this creates a image ribbon gallery at the bottom of the live preview + */ + function ImageRibbonGallery(element) { + this.element = element; + this.remove = this.remove.bind(this); + this.create(); + } + + ImageRibbonGallery.prototype = { + _style: function () { + this.body = window.document.createElement("div"); + this._shadow = this.body.attachShadow({ mode: "closed" }); + + this._shadow.innerHTML = ` + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `; + }, + + create: function () { + this.remove(); // remove existing ribbon if already present + + this._style(); // style the ribbon + window.document.body.appendChild(this.body); + }, + + remove: function () { + if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { + window.document.body.removeChild(this.body); + this.body = null; + } + } + }; + function Highlight(color, trigger) { this.color = color; this.trigger = !!trigger; @@ -2219,6 +2368,7 @@ function RemoteFunctions(config = {}) { var _nodeInfoBox; var _nodeMoreOptionsBox; var _aiPromptBox; + var _imageRibbonGallery; var _setup = false; function onMouseOver(event) { @@ -2371,6 +2521,13 @@ function RemoteFunctions(config = {}) { _selectElement(element); } + + // if the image is an element we show the image ribbon gallery + if(element && element.tagName.toLowerCase() === 'img') { + _imageRibbonGallery = new ImageRibbonGallery(element); + } else { // when any other element is clicked we close the ribbon + dismissImageRibbonGallery(); + } } /** @@ -2934,6 +3091,18 @@ function RemoteFunctions(config = {}) { return false; } + /** + * to dismiss the image ribbon gallery if its available + */ + function dismissImageRibbonGallery() { + if (_imageRibbonGallery) { + _imageRibbonGallery.remove(); + _imageRibbonGallery = null; + return true; + } + return false; + } + /** * Helper function to dismiss all UI boxes at once * @return {boolean} true if any boxes were dismissed, false otherwise From 5cd9a2065f1c639dd5f518afc30d8551a2fbb61c Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 12 Sep 2025 00:32:52 +0530 Subject: [PATCH 02/41] feat: make unsplash calls for images --- .../BrowserScripts/RemoteFunctions.js | 279 +++++++++++------- 1 file changed, 172 insertions(+), 107 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 4425553805..20ddd590dd 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1924,132 +1924,197 @@ function RemoteFunctions(config = {}) { ImageRibbonGallery.prototype = { _style: function () { this.body = window.document.createElement("div"); - this._shadow = this.body.attachShadow({ mode: "closed" }); + this._shadow = this.body.attachShadow({mode: 'closed'}); this._shadow.innerHTML = ` - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ .phoenix-ribbon-nav.right { + right: 18px !important; + } + + .phoenix-ribbon-loading { + display: flex !important; + align-items: center !important; + justify-content: center !important; + height: 100% !important; + color: #eaeaf0 !important; + font-size: 14px !important; + } + + .phoenix-ribbon-error { + display: flex !important; + align-items: center !important; + justify-content: center !important; + height: 100% !important; + color: #ff6b6b !important; + font-size: 14px !important; + } + +
+
+
+
+
+ Loading images...
-
- `; +
+
+ `; + }, + + _fetchImages: function() { + const apiUrl = 'https://images.phcode.dev/api/images/search?q=sunshine&per_page=10'; + + fetch(apiUrl) + .then(response => { + if (!response.ok) { + throw new Error(`API request failed: ${response.status}`); + } + return response.json(); + }) + .then(data => { + if (data.results && data.results.length > 0) { + this._renderImages(data.results); + } else { + this._showError('No images found'); + } + }) + .catch(error => { + console.error('Failed to fetch images:', error); + this._showError('Failed to load images'); + }); }, - create: function () { + _renderImages: function(images) { + const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); + if (!rowElement) { return; } + + // remove the loading state + rowElement.innerHTML = ''; + rowElement.className = 'phoenix-ribbon-row'; + + // Create thumbnails from API data + images.forEach(image => { + const thumbDiv = window.document.createElement('div'); + thumbDiv.className = 'phoenix-ribbon-thumb'; + + const img = window.document.createElement('img'); + img.src = image.thumb_url || image.url; + img.alt = image.alt_text || 'Unsplash image'; + img.loading = 'lazy'; + + thumbDiv.appendChild(img); + rowElement.appendChild(thumbDiv); + }); + }, + + _showError: function(message) { + const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); + if (!rowElement) { return; } + + rowElement.innerHTML = message; + rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-error'; + }, + + create: function() { this.remove(); // remove existing ribbon if already present - this._style(); // style the ribbon + this._style(); window.document.body.appendChild(this.body); + + this._fetchImages(); }, remove: function () { From fd7cd34c76f1e05e8138b45fca98ec42f5a43fe7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 12 Sep 2025 01:19:24 +0530 Subject: [PATCH 03/41] feat: add search and close panel functionality --- .../BrowserScripts/RemoteFunctions.js | 115 +++++++++++++++++- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 20ddd590dd..2b4cb378e8 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2039,8 +2039,59 @@ function RemoteFunctions(config = {}) { color: #ff6b6b !important; font-size: 14px !important; } + + .phoenix-ribbon-header { + position: absolute !important; + top: -20px !important; + left: 0 !important; + right: 0 !important; + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 0 20px !important; + } + + .phoenix-ribbon-search { + display: flex !important; + align-items: center !important; + gap: 8px !important; + background: rgba(0,0,0,0.5) !important; + padding: 5px 10px !important; + border-radius: 5px !important; + } + + .phoenix-ribbon-search input { + background: transparent !important; + border: none !important; + outline: none !important; + color: white !important; + width: 200px !important; + } + + .phoenix-ribbon-search-btn { + background: none !important; + border: none !important; + color: #6aa9ff !important; + cursor: pointer !important; + } + + .phoenix-ribbon-close { + background: rgba(0,0,0,0.5) !important; + border: none !important; + color: white !important; + cursor: pointer !important; + padding: 5px 8px !important; + border-radius: 3px !important; + }
+
+ + +
@@ -2054,8 +2105,9 @@ function RemoteFunctions(config = {}) { `; }, - _fetchImages: function() { - const apiUrl = 'https://images.phcode.dev/api/images/search?q=sunshine&per_page=10'; + _fetchImages: function(searchQuery = 'sunshine') { + const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10`; + this._showLoading(); fetch(apiUrl) .then(response => { @@ -2077,6 +2129,56 @@ function RemoteFunctions(config = {}) { }); }, + _showLoading: function() { + const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); + if (!rowElement) { return; } + + rowElement.innerHTML = 'Loading images...'; + rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-loading'; + }, + + _attachEventHandlers: function() { + const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input'); + const searchButton = this._shadow.querySelector('.phoenix-ribbon-search-btn'); + const closeButton = this._shadow.querySelector('.phoenix-ribbon-close'); + + if (searchInput && searchButton) { + const performSearch = (e) => { + e.stopPropagation(); + const query = searchInput.value.trim(); + if (query) { + this._fetchImages(query); + } + }; + + searchButton.addEventListener('click', performSearch); + searchInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + performSearch(e); + } + }); + + searchInput.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + + if (closeButton) { + closeButton.addEventListener('click', (e) => { + e.stopPropagation(); + this.remove(); + }); + } + + // Prevent clicks anywhere inside the ribbon from bubbling up + const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon'); + if (ribbonContainer) { + ribbonContainer.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + }, + _renderImages: function(images) { const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); if (!rowElement) { return; } @@ -2113,7 +2215,7 @@ function RemoteFunctions(config = {}) { this._style(); window.document.body.appendChild(this.body); - + this._attachEventHandlers(); this._fetchImages(); }, @@ -2589,9 +2691,12 @@ function RemoteFunctions(config = {}) { // if the image is an element we show the image ribbon gallery if(element && element.tagName.toLowerCase() === 'img') { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + _imageRibbonGallery = new ImageRibbonGallery(element); - } else { // when any other element is clicked we close the ribbon - dismissImageRibbonGallery(); + return; } } From 473bf2d1ce9832ad99bdc6d765281d834bd2efc5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 13 Sep 2025 18:17:03 +0530 Subject: [PATCH 04/41] feat: show new images on hover from image ribbon --- .../BrowserScripts/RemoteFunctions.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 2b4cb378e8..820079c703 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1965,7 +1965,7 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-row { display: flex !important; - gap: 12px !important; + gap: 2px !important; align-items: center !important; height: 100% !important; } @@ -2197,6 +2197,27 @@ function RemoteFunctions(config = {}) { img.alt = image.alt_text || 'Unsplash image'; img.loading = 'lazy'; + // this is the original image, we store it so that we can show new images on hover + const originalImageSrc = this.element.src; + // we also store its dimensions to show the new image with the same dimension + const computedStyle = window.getComputedStyle(this.element); + const originalWidth = computedStyle.width; + const originalHeight = computedStyle.height; + const originalObjectFit = computedStyle.objectFit; + + // show hovered image along with dimensions + thumbDiv.addEventListener('mouseenter', () => { + this.element.style.width = originalWidth; + this.element.style.height = originalHeight; + this.element.style.objectFit = originalObjectFit || 'cover'; + this.element.src = image.url || image.thumb_url; + }); + + // show original image when hover ends + thumbDiv.addEventListener('mouseleave', () => { + this.element.src = originalImageSrc; + }); + thumbDiv.appendChild(img); rowElement.appendChild(thumbDiv); }); From 3cd19d507bcfe942b8ea3a0a470b2e5c7bfd8bc4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 13 Sep 2025 18:46:39 +0530 Subject: [PATCH 05/41] feat: add attribution in images in image ribbon gallery --- .../BrowserScripts/RemoteFunctions.js | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 820079c703..468a01f4d6 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2083,6 +2083,36 @@ function RemoteFunctions(config = {}) { padding: 5px 8px !important; border-radius: 3px !important; } + + .phoenix-ribbon-attribution { + position: absolute !important; + bottom: 6px !important; + left: 6px !important; + background: rgba(0,0,0,0.8) !important; + color: white !important; + padding: 4px 6px !important; + border-radius: 5px !important; + font-size: 10px !important; + line-height: 1.2 !important; + max-width: calc(100% - 12px) !important; + text-shadow: 0 1px 2px rgba(0,0,0,0.9) !important; + pointer-events: none !important; + opacity: 0.95 !important; + } + + .phoenix-ribbon-attribution .photographer { + display: block !important; + font-weight: 500 !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } + + .phoenix-ribbon-attribution .source { + display: block !important; + font-size: 9px !important; + opacity: 0.85 !important; + }
@@ -2218,7 +2248,31 @@ function RemoteFunctions(config = {}) { this.element.src = originalImageSrc; }); + // attribution overlay, we show this only in the image ribbon gallery + const attribution = window.document.createElement('div'); + attribution.className = 'phoenix-ribbon-attribution'; + + const photographer = window.document.createElement('span'); + photographer.className = 'photographer'; + + // unsplash attribution is in the format 'Photo by on Unsplash' + // we extract the name from there + let photographerName = 'Anonymous'; // if not present, show anonymous + if (image.attribution) { + const match = image.attribution.match(/Photo by (.+) on Unsplash/); + if (match) { photographerName = match[1]; } + } + photographer.textContent = photographerName; + + const source = window.document.createElement('span'); + source.className = 'source'; + source.textContent = 'on Unsplash'; + + attribution.appendChild(photographer); + attribution.appendChild(source); + thumbDiv.appendChild(img); + thumbDiv.appendChild(attribution); rowElement.appendChild(thumbDiv); }); }, From 20119b7ada3c9092378d0c9e9229c0ed61f4f7a9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 16 Sep 2025 12:21:32 +0530 Subject: [PATCH 06/41] feat: add use this image option in image ribbon gallery --- .../BrowserScripts/RemoteFunctions.js | 111 ++++++++++++++++-- src/LiveDevelopment/LivePreviewEdit.js | 82 +++++++++++++ 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 468a01f4d6..c58d097b09 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2113,6 +2113,55 @@ function RemoteFunctions(config = {}) { font-size: 9px !important; opacity: 0.85 !important; } + + .phoenix-use-image-btn { + position: absolute !important; + top: 6px !important; + right: 6px !important; + background: rgba(0,0,0,0.55) !important; + border: none !important; + color: white !important; + border-radius: 20px !important; + height: 26px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + cursor: pointer !important; + font-size: 12px !important; + z-index: 2 !important; + padding: 0 8px !important; + white-space: nowrap !important; + opacity: 0 !important; + transition: all 0.2s ease !important; + } + + .phoenix-use-image-btn i { + margin-right: 0 !important; + transition: margin 0.2s !important; + } + + .phoenix-use-image-btn span { + display: none !important; + font-size: 11px !important; + font-weight: 500 !important; + } + + .phoenix-ribbon-thumb:hover .phoenix-use-image-btn { + opacity: 1 !important; + } + + .phoenix-use-image-btn:hover { + background: rgba(0,0,0,0.8) !important; + padding: 0 10px !important; + } + + .phoenix-use-image-btn:hover i { + margin-right: 4px !important; + } + + .phoenix-use-image-btn:hover span { + display: inline !important; + }
@@ -2136,6 +2185,7 @@ function RemoteFunctions(config = {}) { }, _fetchImages: function(searchQuery = 'sunshine') { + this._currentSearchQuery = searchQuery; const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10`; this._showLoading(); @@ -2254,14 +2304,7 @@ function RemoteFunctions(config = {}) { const photographer = window.document.createElement('span'); photographer.className = 'photographer'; - - // unsplash attribution is in the format 'Photo by on Unsplash' - // we extract the name from there - let photographerName = 'Anonymous'; // if not present, show anonymous - if (image.attribution) { - const match = image.attribution.match(/Photo by (.+) on Unsplash/); - if (match) { photographerName = match[1]; } - } + const photographerName = this._getPhotographerName(image); photographer.textContent = photographerName; const source = window.document.createElement('span'); @@ -2271,8 +2314,24 @@ function RemoteFunctions(config = {}) { attribution.appendChild(photographer); attribution.appendChild(source); + // use image button + const useImageBtn = window.document.createElement('button'); + useImageBtn.className = 'phoenix-use-image-btn'; + useImageBtn.innerHTML = '⬇Use this image'; + + // when use image button is clicked, we first generate the file name by which we need to save the image + // and then we add the image to project + useImageBtn.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + const filename = this._generateFilename(image); + const extnName = ".jpg"; + this._useImage(image.url, filename, extnName); + }); + thumbDiv.appendChild(img); thumbDiv.appendChild(attribution); + thumbDiv.appendChild(useImageBtn); rowElement.appendChild(thumbDiv); }); }, @@ -2285,6 +2344,42 @@ function RemoteFunctions(config = {}) { rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-error'; }, + _getPhotographerName: function(image) { + // unsplash API returns attribution in format 'Photo by on Unsplash' + // this function is responsible to get the name + if (image.attribution) { + const match = image.attribution.match(/Photo by (.+) on Unsplash/); + if (match) { + return match[1]; + } + } + return 'Anonymous'; + }, + + // file name with which we need to save the image + _generateFilename: function(image) { + const photographerName = this._getPhotographerName(image); + const searchTerm = this._currentSearchQuery || 'image'; + + // clean the search term and the photograper name to write in file name + const cleanSearchTerm = searchTerm.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + const cleanPhotographerName = photographerName.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + + return `${cleanSearchTerm}-by-${cleanPhotographerName}`; + }, + + _useImage: function(imageUrl, filename, extnName) { + // to use the image we send the message to the editor instance + // this is handled inside liveDevProtocol.js file + window._Brackets_MessageBroker.send({ + livePreviewEditEnabled: true, + useImage: true, + imageUrl: imageUrl, + filename: filename, + extnName: extnName + }); + }, + create: function() { this.remove(); // remove existing ribbon if already present diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 1b40ba6727..d502c181b1 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -28,6 +28,8 @@ define(function (require, exports, module) { const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"); const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser"); const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"); + const ProjectManager = require("project/ProjectManager"); + const FileSystem = require("filesystem/FileSystem"); /** * This function syncs text content changes between the original source code @@ -593,6 +595,80 @@ define(function (require, exports, module) { // write the AI implementation here...@abose } + /** + * this is a helper function to make sure that when saving a new image, there's no existing file with the same name + * @param {String} basePath - this is the base path where the image will be saved + * @param {String} filename - the name of the image file + * @param {String} extnName - the name of the image extension. (defaults to "jpg") + * @returns {String} - the new file name + */ + function getUniqueFilename(basePath, filename, extnName) { + let counter = 0; + let uniqueFilename = filename + extnName; + + function checkAndIncrement() { + const filePath = basePath + uniqueFilename; + const file = FileSystem.getFileForPath(filePath); + + return new Promise((resolve) => { + file.exists((err, exists) => { + if (exists) { + counter++; + uniqueFilename = `${filename}-${counter}${extnName}`; + checkAndIncrement().then(resolve); + } else { + resolve(uniqueFilename); + } + }); + }); + } + + return checkAndIncrement(); + } + + /** + * This function is called when 'use this image' button is clicked in the image ribbon gallery + * this is responsible to download the image in the appropriate place + * and also change the src attribute of the element + * @param {Object} message - the message object which stores all the required data for this operation + */ + function _handleUseThisImage(message) { + const { imageUrl, filename } = message; + const extnName = message.extnName || "jpg"; + + const projectRoot = ProjectManager.getProjectRoot(); + if (!projectRoot) { + console.error('No project root found'); + return; + } + + getUniqueFilename(projectRoot.fullPath, filename, extnName).then((uniqueFilename) => { + fetch(imageUrl) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.arrayBuffer(); + }) + .then(arrayBuffer => { + const uint8Array = new Uint8Array(arrayBuffer); + + const targetPath = projectRoot.fullPath + uniqueFilename; + window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array), + { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => { + if (err) { + console.error('Failed to save image:', err); + } + }); + }) + .catch(error => { + console.error('Failed to fetch image:', error); + }); + }).catch(error => { + console.error('Something went wrong when trying to use this image', error); + }); + } + /** * This is the main function that is exported. * it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js @@ -623,6 +699,12 @@ define(function (require, exports, module) { return; } + // use this image + if (message.useImage && message.imageUrl && message.filename) { + _handleUseThisImage(message); + return; + } + if (!message.element || !message.tagId) { // check for undo if (message.undoLivePreviewOperation || message.redoLivePreviewOperation) { From 5a11e4cfb177a808788a7b669bb09447f65c7dee Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 16 Sep 2025 16:07:55 +0530 Subject: [PATCH 07/41] feat: change the img src attribute when user clicks on use this image button --- .../BrowserScripts/RemoteFunctions.js | 9 +++-- src/LiveDevelopment/LivePreviewEdit.js | 38 ++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index c58d097b09..acce27b8fc 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2369,14 +2369,17 @@ function RemoteFunctions(config = {}) { }, _useImage: function(imageUrl, filename, extnName) { - // to use the image we send the message to the editor instance - // this is handled inside liveDevProtocol.js file + // send the message to the editor instance to save the image and update the source code + const tagId = this.element.getAttribute("data-brackets-id"); + window._Brackets_MessageBroker.send({ livePreviewEditEnabled: true, useImage: true, imageUrl: imageUrl, filename: filename, - extnName: extnName + extnName: extnName, + element: this.element, + tagId: Number(tagId) }); }, diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index d502c181b1..b1677c56ad 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -626,6 +626,40 @@ define(function (require, exports, module) { return checkAndIncrement(); } + /** + * This function updates the src attribute of an image element in the source code + * @param {Number} tagId - the data-brackets-id of the image element + * @param {String} newSrcValue - the new src value to set + */ + function _updateImageSrcAttribute(tagId, newSrcValue) { + const editor = _getEditorAndValidate(tagId); + if (!editor) { + return; + } + + const range = _getElementRange(editor, tagId); + if (!range) { + return; + } + + const { startPos, endPos } = range; + const elementText = editor.getTextBetween(startPos, endPos); + + // parse it using DOM parser so that we can update the src attribute + const parser = new DOMParser(); + const doc = parser.parseFromString(elementText, "text/html"); + const imgElement = doc.querySelector('img'); + + if (imgElement) { + imgElement.setAttribute('src', newSrcValue); + const updatedElementText = imgElement.outerHTML; + + editor.document.batchOperation(function () { + editor.replaceRange(updatedElementText, startPos, endPos); + }); + } + } + /** * This function is called when 'use this image' button is clicked in the image ribbon gallery * this is responsible to download the image in the appropriate place @@ -633,7 +667,7 @@ define(function (require, exports, module) { * @param {Object} message - the message object which stores all the required data for this operation */ function _handleUseThisImage(message) { - const { imageUrl, filename } = message; + const { imageUrl, filename, tagId } = message; const extnName = message.extnName || "jpg"; const projectRoot = ProjectManager.getProjectRoot(); @@ -658,6 +692,8 @@ define(function (require, exports, module) { { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => { if (err) { console.error('Failed to save image:', err); + } else { + _updateImageSrcAttribute(tagId, uniqueFilename); } }); }) From 1496025726bcb38901c30cbd6f7d638f348af3f6 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 16 Sep 2025 16:44:45 +0530 Subject: [PATCH 08/41] feat: use relative path instead of the image file name directly --- src/LiveDevelopment/LivePreviewEdit.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index b1677c56ad..b67524cbe5 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -30,6 +30,7 @@ define(function (require, exports, module) { const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"); const ProjectManager = require("project/ProjectManager"); const FileSystem = require("filesystem/FileSystem"); + const PathUtils = require("thirdparty/path-utils/path-utils"); /** * This function syncs text content changes between the original source code @@ -693,7 +694,18 @@ define(function (require, exports, module) { if (err) { console.error('Failed to save image:', err); } else { - _updateImageSrcAttribute(tagId, uniqueFilename); + // once the image is saved, we need to update the source code + // so we get the relative path between the current file and the image file + // and that relative path is written as the src value + const editor = _getEditorAndValidate(tagId); + if (editor) { + const htmlFilePath = editor.document.file.fullPath; + const relativePath = PathUtils.makePathRelative(targetPath, htmlFilePath); + _updateImageSrcAttribute(tagId, relativePath); + } else { + // if editor is not available we directly write the image file name as the src value + _updateImageSrcAttribute(tagId, uniqueFilename); + } } }); }) From c7a2d31cb14ead8568804ad5338479bfcbe60580 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 16 Sep 2025 17:24:20 +0530 Subject: [PATCH 09/41] fix: image ribbon gallery gets unresponsive after successful completion as tag markers are updated --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 1 + src/LiveDevelopment/LivePreviewEdit.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index acce27b8fc..e4221b151c 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3668,6 +3668,7 @@ function RemoteFunctions(config = {}) { "finishEditing" : finishEditing, "hasVisibleLivePreviewBoxes" : hasVisibleLivePreviewBoxes, "dismissUIAndCleanupState" : dismissUIAndCleanupState, + "dismissImageRibbonGallery" : dismissImageRibbonGallery, "registerHandlers" : registerHandlers }; } diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index b67524cbe5..7ee7a72514 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -706,6 +706,13 @@ define(function (require, exports, module) { // if editor is not available we directly write the image file name as the src value _updateImageSrcAttribute(tagId, uniqueFilename); } + + // after successful update we dismiss the image ribbon gallery + // to ensure that the user doesn't work with image ribbon gallery on a stale DOM + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) { + currLiveDoc.protocol.evaluate("_LD.dismissImageRibbonGallery()"); + } } }); }) From e652af513e44d57a669e5632a0fafe2394209e66 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 17 Sep 2025 14:14:52 +0530 Subject: [PATCH 10/41] feat: implement nav-right nav-left buttons to scroll the image ribbon gallery --- .../BrowserScripts/RemoteFunctions.js | 204 +++++++++++++++++- 1 file changed, 194 insertions(+), 10 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index e4221b151c..51f2a9b9d0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1918,6 +1918,11 @@ function RemoteFunctions(config = {}) { function ImageRibbonGallery(element) { this.element = element; this.remove = this.remove.bind(this); + this.currentPage = 1; + this.totalPages = 1; + this.allImages = []; + this.imagesPerPage = 10; + this.scrollPosition = 0; this.create(); } @@ -2014,6 +2019,25 @@ function RemoteFunctions(config = {}) { font-size: 14px !important; } + .phoenix-ribbon-nav { + font-size: 18px !important; + font-weight: 600 !important; + user-select: none !important; + transition: all 0.2s ease !important; + z-index: 10 !important; + } + + .phoenix-ribbon-nav:hover { + background: rgba(21,25,36,0.85) !important; + border-color: rgba(255,255,255,0.25) !important; + transform: translateY(-50%) scale(1.05) !important; + box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important; + } + + .phoenix-ribbon-nav:active { + transform: translateY(-50%) scale(0.95) !important; + } + .phoenix-ribbon-nav.left { left: 18px !important; } @@ -2184,10 +2208,13 @@ function RemoteFunctions(config = {}) { `; }, - _fetchImages: function(searchQuery = 'sunshine') { + _fetchImages: function(searchQuery = 'sunshine', page = 1, append = false) { this._currentSearchQuery = searchQuery; - const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10`; - this._showLoading(); + const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10&page=${page}`; + + if (!append) { + this._showLoading(); + } fetch(apiUrl) .then(response => { @@ -2198,17 +2225,103 @@ function RemoteFunctions(config = {}) { }) .then(data => { if (data.results && data.results.length > 0) { - this._renderImages(data.results); - } else { + if (append) { + this.allImages = this.allImages.concat(data.results); + this._renderImages(data.results, true); // true means need to append new images at the end + } else { + this.allImages = data.results; + this._renderImages(this.allImages, false); // false means its a new search + } + this.totalPages = data.total_pages || 1; + this.currentPage = page; + this._updateNavButtons(); + } else if (!append) { this._showError('No images found'); } + + if (append) { + this._isLoadingMore = false; + this._hideLoadingMore(); + } }) .catch(error => { console.error('Failed to fetch images:', error); - this._showError('Failed to load images'); + if (!append) { + this._showError('Failed to load images'); + } else { + this._isLoadingMore = false; + this._hideLoadingMore(); + } }); }, + _handleNavLeft: function() { + const container = this._shadow.querySelector('.phoenix-ribbon-strip'); + if (!container) { return; } + + const containerWidth = container.clientWidth; + const scrollAmount = containerWidth; + + this.scrollPosition = Math.max(0, this.scrollPosition - scrollAmount); + container.scrollTo({ left: this.scrollPosition, behavior: 'smooth' }); + this._updateNavButtons(); + }, + + _handleNavRight: function() { + const container = this._shadow.querySelector('.phoenix-ribbon-strip'); + if (!container) { return; } + + const containerWidth = container.clientWidth; + const totalWidth = container.scrollWidth; + const scrollAmount = containerWidth; + + // if we're near the end, we need to load more images + const nearEnd = (this.scrollPosition + containerWidth + scrollAmount) >= totalWidth - 100; + if (nearEnd && this.currentPage < this.totalPages && !this._isLoadingMore) { + this._isLoadingMore = true; + this._showLoadingMore(); + this._fetchImages(this._currentSearchQuery, this.currentPage + 1, true); + } + + this.scrollPosition = Math.min(totalWidth - containerWidth, this.scrollPosition + scrollAmount); + container.scrollTo({ left: this.scrollPosition, behavior: 'smooth' }); + this._updateNavButtons(); + }, + + _updateNavButtons: function() { + // this function is responsible to update the nav buttons + // because when we're at the very left, then we style the nav-left button differently (reduce opacity) + // and when we're at the very right and no more pages available, we reduce opacity for nav-right + const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left'); + const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right'); + const container = this._shadow.querySelector('.phoenix-ribbon-strip'); + + if (!navLeft || !navRight || !container) { return; } + + // show/hide left button + if (this.scrollPosition <= 0) { + navLeft.style.opacity = '0.3'; + navLeft.style.pointerEvents = 'none'; + } else { + navLeft.style.opacity = '1'; + navLeft.style.pointerEvents = 'auto'; + } + + // show/hide right button + const containerWidth = container.clientWidth; + const totalWidth = container.scrollWidth; + const atEnd = (this.scrollPosition + containerWidth) >= totalWidth - 10; + const hasMorePages = this.currentPage < this.totalPages; + + if (atEnd && !hasMorePages) { + navRight.style.opacity = '0.3'; + navRight.style.pointerEvents = 'none'; + } else { + navRight.style.opacity = '1'; + navRight.style.pointerEvents = 'auto'; + } + }, + _showLoading: function() { const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); if (!rowElement) { return; } @@ -2217,16 +2330,53 @@ function RemoteFunctions(config = {}) { rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-loading'; }, + _showLoadingMore: function() { + const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); + if (!rowElement) { return; } + + // when loading more images we need to show the message at the end of the image ribbon + const loadingIndicator = window.document.createElement('div'); + loadingIndicator.className = 'phoenix-loading-more'; + loadingIndicator.style.cssText = ` + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-width: 120px !important; + height: 116px !important; + margin-left: 2px !important; + background: rgba(255,255,255,0.03) !important; + border-radius: 8px !important; + color: #e8eaf0 !important; + font-size: 12px !important; + border: 1px dashed rgba(255,255,255,0.1) !important; + `; + loadingIndicator.textContent = 'Loading...'; + rowElement.appendChild(loadingIndicator); + }, + + _hideLoadingMore: function() { + const loadingIndicator = this._shadow.querySelector('.phoenix-loading-more'); + if (loadingIndicator) { + loadingIndicator.remove(); + } + }, + _attachEventHandlers: function() { const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input'); const searchButton = this._shadow.querySelector('.phoenix-ribbon-search-btn'); const closeButton = this._shadow.querySelector('.phoenix-ribbon-close'); + const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left'); + const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right'); if (searchInput && searchButton) { const performSearch = (e) => { e.stopPropagation(); const query = searchInput.value.trim(); if (query) { + // reset pagination when searching + this.currentPage = 1; + this.allImages = []; + this.scrollPosition = 0; this._fetchImages(query); } }; @@ -2250,6 +2400,20 @@ function RemoteFunctions(config = {}) { }); } + if (navLeft) { + navLeft.addEventListener('click', (e) => { + e.stopPropagation(); + this._handleNavLeft(); + }); + } + + if (navRight) { + navRight.addEventListener('click', (e) => { + e.stopPropagation(); + this._handleNavRight(); + }); + } + // Prevent clicks anywhere inside the ribbon from bubbling up const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon'); if (ribbonContainer) { @@ -2259,13 +2423,26 @@ function RemoteFunctions(config = {}) { } }, - _renderImages: function(images) { + // append true means load more images (user clicked on nav-right) + // append false means its a new query + _renderImages: function(images, append = false) { const rowElement = this._shadow.querySelector('.phoenix-ribbon-row'); if (!rowElement) { return; } - // remove the loading state - rowElement.innerHTML = ''; - rowElement.className = 'phoenix-ribbon-row'; + const container = this._shadow.querySelector('.phoenix-ribbon-strip'); + const savedScrollPosition = container ? container.scrollLeft : 0; + + // if not appending we clear the phoenix ribbon + if (!append) { + rowElement.innerHTML = ''; + rowElement.className = 'phoenix-ribbon-row'; + } else { + // when appending we add the new images at the end + const loadingIndicator = this._shadow.querySelector('.phoenix-loading-more'); + if (loadingIndicator) { + loadingIndicator.remove(); + } + } // Create thumbnails from API data images.forEach(image => { @@ -2334,6 +2511,12 @@ function RemoteFunctions(config = {}) { thumbDiv.appendChild(useImageBtn); rowElement.appendChild(thumbDiv); }); + + if (append && container && savedScrollPosition > 0) { + setTimeout(() => { + container.scrollLeft = savedScrollPosition; + }, 0); + } }, _showError: function(message) { @@ -2390,6 +2573,7 @@ function RemoteFunctions(config = {}) { window.document.body.appendChild(this.body); this._attachEventHandlers(); this._fetchImages(); + setTimeout(() => this._updateNavButtons(), 0); }, remove: function () { From 7ecab385bfaf3e5f502056ec69170b6bf834a1d4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 17 Sep 2025 14:20:20 +0530 Subject: [PATCH 11/41] fix: remove scrollbar from image ribbon gallery --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 51f2a9b9d0..39ec5910bf 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1963,7 +1963,7 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-strip { position: absolute !important; inset: 0 !important; - overflow: auto hidden !important; + overflow: hidden !important; scroll-behavior: smooth !important; padding: 8px !important; } From cb2f4ddf2d21a4a8632d51ca22f4f497190ef536 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 17 Sep 2025 14:40:18 +0530 Subject: [PATCH 12/41] feat: show results of random queries instead of a specific one --- .../BrowserScripts/RemoteFunctions.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 39ec5910bf..7c96ccb02b 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2208,7 +2208,25 @@ function RemoteFunctions(config = {}) { `; }, - _fetchImages: function(searchQuery = 'sunshine', page = 1, append = false) { + _getDefaultQuery: function() { + // this are the default queries, so when image ribbon gallery is shown, we select a random query and show it + const qualityQueries = [ + 'nature', 'minimal', 'workspace', 'abstract', 'coffee', + 'mountains', 'city', 'flowers', 'ocean', 'sunset', + 'architecture', 'forest', 'travel', 'technology', 'sky', + 'landscape', 'creative', 'design', 'art', 'modern', + 'food', 'patterns', 'colors', 'photography', 'studio', + 'light', 'winter', 'summer', 'vintage', 'geometric', + 'water', 'beach', 'space', 'garden', 'textures', + 'urban', 'portrait', 'music', 'books', 'home', + 'cozy', 'aesthetic', 'autumn', 'spring', 'clouds' + ]; + + const randIndex = Math.floor(Math.random() * qualityQueries.length); + return qualityQueries[randIndex]; + }, + + _fetchImages: function(searchQuery, page = 1, append = false) { this._currentSearchQuery = searchQuery; const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10&page=${page}`; @@ -2572,7 +2590,7 @@ function RemoteFunctions(config = {}) { this._style(); window.document.body.appendChild(this.body); this._attachEventHandlers(); - this._fetchImages(); + this._fetchImages(this._getDefaultQuery()); setTimeout(() => this._updateNavButtons(), 0); }, From d0902555546cb66fa63ce86757daa1f18b2a8f5e Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 18 Sep 2025 13:50:22 +0530 Subject: [PATCH 13/41] feat: add upload from computer button in image ribbon gallery --- .../BrowserScripts/RemoteFunctions.js | 94 +++++++++++- src/LiveDevelopment/LivePreviewEdit.js | 137 ++++++++++++------ 2 files changed, 179 insertions(+), 52 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 7c96ccb02b..d765d899e1 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2099,6 +2099,24 @@ function RemoteFunctions(config = {}) { cursor: pointer !important; } + .phoenix-select-image-btn { + background: rgba(255,255,255,0.1) !important; + border: 1px solid rgba(255,255,255,0.2) !important; + color: #e8eaf0 !important; + padding: 6px 12px !important; + border-radius: 6px !important; + font-size: 12px !important; + cursor: pointer !important; + margin-left: 8px !important; + white-space: nowrap !important; + transition: all 0.2s ease !important; + } + + .phoenix-select-image-btn:hover { + background: rgba(255,255,255,0.2) !important; + border-color: rgba(255,255,255,0.3) !important; + } + .phoenix-ribbon-close { background: rgba(0,0,0,0.5) !important; border: none !important; @@ -2192,6 +2210,8 @@ function RemoteFunctions(config = {}) {
@@ -2380,11 +2400,14 @@ function RemoteFunctions(config = {}) { }, _attachEventHandlers: function() { + const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon'); const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input'); const searchButton = this._shadow.querySelector('.phoenix-ribbon-search-btn'); const closeButton = this._shadow.querySelector('.phoenix-ribbon-close'); const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left'); const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right'); + const selectImageBtn = this._shadow.querySelector('.phoenix-select-image-btn'); + const fileInput = this._shadow.querySelector('.phoenix-file-input'); if (searchInput && searchButton) { const performSearch = (e) => { @@ -2411,6 +2434,22 @@ function RemoteFunctions(config = {}) { }); } + if (selectImageBtn && fileInput) { + selectImageBtn.addEventListener('click', (e) => { + e.stopPropagation(); + fileInput.click(); + }); + + fileInput.addEventListener('change', (e) => { + e.stopPropagation(); + const file = e.target.files[0]; + if (file) { + this._handleLocalImageSelection(file); + fileInput.value = ''; + } + }); + } + if (closeButton) { closeButton.addEventListener('click', (e) => { e.stopPropagation(); @@ -2433,7 +2472,6 @@ function RemoteFunctions(config = {}) { } // Prevent clicks anywhere inside the ribbon from bubbling up - const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon'); if (ribbonContainer) { ribbonContainer.addEventListener('click', (e) => { e.stopPropagation(); @@ -2521,7 +2559,7 @@ function RemoteFunctions(config = {}) { e.preventDefault(); const filename = this._generateFilename(image); const extnName = ".jpg"; - this._useImage(image.url, filename, extnName); + this._useImage(image.url, filename, extnName, false); }); thumbDiv.appendChild(img); @@ -2569,11 +2607,11 @@ function RemoteFunctions(config = {}) { return `${cleanSearchTerm}-by-${cleanPhotographerName}`; }, - _useImage: function(imageUrl, filename, extnName) { + _useImage: function(imageUrl, filename, extnName, isLocalFile) { // send the message to the editor instance to save the image and update the source code const tagId = this.element.getAttribute("data-brackets-id"); - window._Brackets_MessageBroker.send({ + const messageData = { livePreviewEditEnabled: true, useImage: true, imageUrl: imageUrl, @@ -2581,9 +2619,55 @@ function RemoteFunctions(config = {}) { extnName: extnName, element: this.element, tagId: Number(tagId) - }); + }; + + // if this is a local file we need some more data before sending it to the editor + if (isLocalFile) { + messageData.isLocalFile = true; + // Convert data URL to binary data array for local files + const byteCharacters = atob(imageUrl.split(',')[1]); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + messageData.imageData = byteNumbers; + } + + window._Brackets_MessageBroker.send(messageData); }, + _handleLocalImageSelection: function(file) { + if (!file || !file.type.startsWith('image/')) { + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const imageDataUrl = e.target.result; + + const originalName = file.name; + const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.')) || originalName; + const extension = originalName.substring(originalName.lastIndexOf('.')) || '.jpg'; + + // we clean the file name because the file might have some chars which might not be compatible + const cleanName = nameWithoutExt.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); + const filename = cleanName || 'selected-image'; + + // Use the unified _useImage method with isLocalFile flag + this._useImage(imageDataUrl, filename, extension, true); + + // Close the ribbon after successful selection + this.remove(); + }; + + reader.onerror = (error) => { + console.error('Something went wrong when reading the image:', error); + }; + + reader.readAsDataURL(file); + }, + + create: function() { this.remove(); // remove existing ribbon if already present diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index 7ee7a72514..f7c9439b00 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -661,64 +661,107 @@ define(function (require, exports, module) { } } + /** + * Helper function to update image src attribute and dismiss ribbon gallery + * + * @param {Number} tagId - the data-brackets-id of the image element + * @param {String} targetPath - the full path where the image was saved + * @param {String} filename - the filename of the saved image + */ + function _updateImageAndDismissRibbon(tagId, targetPath, filename) { + const editor = _getEditorAndValidate(tagId); + if (editor) { + const htmlFilePath = editor.document.file.fullPath; + const relativePath = PathUtils.makePathRelative(targetPath, htmlFilePath); + _updateImageSrcAttribute(tagId, relativePath); + } else { + _updateImageSrcAttribute(tagId, filename); + } + + // dismiss the image ribbon gallery + const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); + if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) { + currLiveDoc.protocol.evaluate("_LD.dismissImageRibbonGallery()"); + } + } + + /** + * helper function to handle 'upload from computer' + * @param {Object} message - the message object + * @param {String} filename - the file name with which we need to save the image + * @param {Directory} projectRoot - the project root in which the image is to be saved + */ + function _handleUseThisImageLocalFiles(message, filename, projectRoot) { + const { tagId, imageData } = message; + + const uint8Array = new Uint8Array(imageData); + const targetPath = projectRoot.fullPath + filename; + + window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array), + { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => { + if (err) { + console.error('Failed to save image:', err); + } else { + _updateImageAndDismissRibbon(tagId, targetPath, filename); + } + }); + } + + /** + * helper function to handle 'use this image' button click on remote images + * @param {Object} message - the message object + * @param {String} filename - the file name with which we need to save the image + * @param {Directory} projectRoot - the project root in which the image is to be saved + */ + function _handleUseThisImageRemote(message, filename, projectRoot) { + const { imageUrl, tagId } = message; + + fetch(imageUrl) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.arrayBuffer(); + }) + .then(arrayBuffer => { + const uint8Array = new Uint8Array(arrayBuffer); + const targetPath = projectRoot.fullPath + filename; + + window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array), + { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => { + if (err) { + console.error('Failed to save image:', err); + } else { + _updateImageAndDismissRibbon(tagId, targetPath, filename); + } + }); + }) + .catch(error => { + console.error('Failed to fetch image:', error); + }); + } + /** * This function is called when 'use this image' button is clicked in the image ribbon gallery + * or user loads an image file from the computer * this is responsible to download the image in the appropriate place - * and also change the src attribute of the element + * and also change the src attribute of the element (by calling appropriate helper functions) * @param {Object} message - the message object which stores all the required data for this operation */ function _handleUseThisImage(message) { - const { imageUrl, filename, tagId } = message; + const filename = message.filename; const extnName = message.extnName || "jpg"; const projectRoot = ProjectManager.getProjectRoot(); - if (!projectRoot) { - console.error('No project root found'); - return; - } + if (!projectRoot) { return; } getUniqueFilename(projectRoot.fullPath, filename, extnName).then((uniqueFilename) => { - fetch(imageUrl) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response.arrayBuffer(); - }) - .then(arrayBuffer => { - const uint8Array = new Uint8Array(arrayBuffer); - - const targetPath = projectRoot.fullPath + uniqueFilename; - window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array), - { encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => { - if (err) { - console.error('Failed to save image:', err); - } else { - // once the image is saved, we need to update the source code - // so we get the relative path between the current file and the image file - // and that relative path is written as the src value - const editor = _getEditorAndValidate(tagId); - if (editor) { - const htmlFilePath = editor.document.file.fullPath; - const relativePath = PathUtils.makePathRelative(targetPath, htmlFilePath); - _updateImageSrcAttribute(tagId, relativePath); - } else { - // if editor is not available we directly write the image file name as the src value - _updateImageSrcAttribute(tagId, uniqueFilename); - } - - // after successful update we dismiss the image ribbon gallery - // to ensure that the user doesn't work with image ribbon gallery on a stale DOM - const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc(); - if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) { - currLiveDoc.protocol.evaluate("_LD.dismissImageRibbonGallery()"); - } - } - }); - }) - .catch(error => { - console.error('Failed to fetch image:', error); - }); + // check if the image is loaded from computer or from remote + if (message.isLocalFile && message.imageData) { + _handleUseThisImageLocalFiles(message, uniqueFilename, projectRoot); + } else { + _handleUseThisImageRemote(message, uniqueFilename, projectRoot); + } }).catch(error => { console.error('Something went wrong when trying to use this image', error); }); From 032c4ce881b960d93332fc3d4dfb3d7c876c2b7a Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 18 Sep 2025 14:11:24 +0530 Subject: [PATCH 14/41] fix: sometimes the original image doesn't get restored after hover out --- .../BrowserScripts/RemoteFunctions.js | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index d765d899e1..904e95dbbd 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2510,25 +2510,17 @@ function RemoteFunctions(config = {}) { img.alt = image.alt_text || 'Unsplash image'; img.loading = 'lazy'; - // this is the original image, we store it so that we can show new images on hover - const originalImageSrc = this.element.src; - // we also store its dimensions to show the new image with the same dimension - const computedStyle = window.getComputedStyle(this.element); - const originalWidth = computedStyle.width; - const originalHeight = computedStyle.height; - const originalObjectFit = computedStyle.objectFit; - // show hovered image along with dimensions thumbDiv.addEventListener('mouseenter', () => { - this.element.style.width = originalWidth; - this.element.style.height = originalHeight; - this.element.style.objectFit = originalObjectFit || 'cover'; + this.element.style.width = this._originalImageStyle.width; + this.element.style.height = this._originalImageStyle.height; + this.element.style.objectFit = this._originalImageStyle.objectFit || 'cover'; this.element.src = image.url || image.thumb_url; }); // show original image when hover ends thumbDiv.addEventListener('mouseleave', () => { - this.element.src = originalImageSrc; + this.element.src = this._originalImageSrc; }); // attribution overlay, we show this only in the image ribbon gallery @@ -2671,6 +2663,15 @@ function RemoteFunctions(config = {}) { create: function() { this.remove(); // remove existing ribbon if already present + // when image ribbon gallery is created we get the original image along with its dimensions + // so that on hover in we can show the hovered image and on hover out we can restore the original image + this._originalImageSrc = this.element.src; + this._originalImageStyle = { + width: window.getComputedStyle(this.element).width, + height: window.getComputedStyle(this.element).height, + objectFit: window.getComputedStyle(this.element).objectFit + }; + this._style(); window.document.body.appendChild(this.body); this._attachEventHandlers(); From 1eade6eba58aba6a7ce95907883cc467c5fcc93b Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 18 Sep 2025 17:31:22 +0530 Subject: [PATCH 15/41] refactor: wrap image ribbon inside image ribbon container --- .../BrowserScripts/RemoteFunctions.js | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 904e95dbbd..2057c8ce23 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1939,24 +1939,17 @@ function RemoteFunctions(config = {}) { left: 0 !important; right: 0 !important; width: 100vw !important; - height: 150px !important; background: linear-gradient(180deg, rgba(12,14,20,0.0), rgba(12,14,20,0.7)) !important; - z-index: 999999 !important; + z-index: 2147483647 !important; display: flex !important; - align-items: center !important; - justify-content: center !important; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial !important; pointer-events: auto !important; } .phoenix-ribbon-container { - width: min(1200px, 96vw) !important; - height: 132px !important; - border-radius: 16px !important; + width: 100% !important; + height: 160px !important; background: rgba(21,25,36,0.55) !important; - backdrop-filter: blur(8px) !important; - border: 1px solid rgba(255,255,255,0.08) !important; - overflow: hidden !important; position: relative !important; } @@ -1965,14 +1958,13 @@ function RemoteFunctions(config = {}) { inset: 0 !important; overflow: hidden !important; scroll-behavior: smooth !important; - padding: 8px !important; + padding: 6px !important; + top: 34px !important; } .phoenix-ribbon-row { display: flex !important; gap: 2px !important; - align-items: center !important; - height: 100% !important; } .phoenix-ribbon-thumb { @@ -2065,14 +2057,8 @@ function RemoteFunctions(config = {}) { } .phoenix-ribbon-header { - position: absolute !important; - top: -20px !important; - left: 0 !important; - right: 0 !important; display: flex !important; justify-content: space-between !important; - align-items: center !important; - padding: 0 20px !important; } .phoenix-ribbon-search { @@ -2206,24 +2192,24 @@ function RemoteFunctions(config = {}) { }
-
- - -
-
+
+ + +
+
Loading images...
+
-
`; }, From 33ee72dbf9e3e6bc35ea498426a11aff8765a891 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 18 Sep 2025 22:36:17 +0530 Subject: [PATCH 16/41] refactor: move select button out of search button div --- .../BrowserScripts/RemoteFunctions.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 2057c8ce23..c0c979cee0 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2058,15 +2058,17 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-header { display: flex !important; - justify-content: space-between !important; + width: 100% !important; } .phoenix-ribbon-search { + position: absolute !important; + top: 8px !important; + left: 6px !important; display: flex !important; align-items: center !important; - gap: 8px !important; background: rgba(0,0,0,0.5) !important; - padding: 5px 10px !important; + padding: 5px !important; border-radius: 5px !important; } @@ -2085,11 +2087,17 @@ function RemoteFunctions(config = {}) { cursor: pointer !important; } + .phoenix-ribbon-select { + position: absolute !important; + top: 8px !important; + left: 50% !important; + } + .phoenix-select-image-btn { background: rgba(255,255,255,0.1) !important; border: 1px solid rgba(255,255,255,0.2) !important; color: #e8eaf0 !important; - padding: 6px 12px !important; + padding: 4px 5px !important; border-radius: 6px !important; font-size: 12px !important; cursor: pointer !important; @@ -2110,6 +2118,9 @@ function RemoteFunctions(config = {}) { cursor: pointer !important; padding: 5px 8px !important; border-radius: 3px !important; + position: absolute !important; + right: 4px !important; + top: 10px !important; } .phoenix-ribbon-attribution { @@ -2197,10 +2208,12 @@ function RemoteFunctions(config = {}) { +
- +
From 99120e0e2851c7f292e50adb7cedd27c6a2f7fe7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 18 Sep 2025 23:30:49 +0530 Subject: [PATCH 17/41] refactor: image ribbon nav buttons positioning --- .../BrowserScripts/RemoteFunctions.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index c0c979cee0..ed3a52ea3a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1948,7 +1948,7 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-container { width: 100% !important; - height: 160px !important; + height: 156px !important; background: rgba(21,25,36,0.55) !important; position: relative !important; } @@ -1995,28 +1995,20 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-nav { position: absolute !important; - top: 50% !important; + top: 58% !important; transform: translateY(-50%) !important; - width: 40px !important; - height: 40px !important; border-radius: 12px !important; border: 1px solid rgba(255,255,255,0.14) !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; color: #eaeaf0 !important; background: rgba(21,25,36,0.65) !important; cursor: pointer !important; backdrop-filter: blur(8px) !important; - font-size: 14px !important; - } - - .phoenix-ribbon-nav { - font-size: 18px !important; + font-size: 20px !important; font-weight: 600 !important; user-select: none !important; transition: all 0.2s ease !important; z-index: 10 !important; + padding: 2px 12px 6px 12px !important; } .phoenix-ribbon-nav:hover { @@ -2116,7 +2108,7 @@ function RemoteFunctions(config = {}) { border: none !important; color: white !important; cursor: pointer !important; - padding: 5px 8px !important; + padding: 4px 8px !important; border-radius: 3px !important; position: absolute !important; right: 4px !important; From 11431c33f64056adb53c84eaebbcbcd1da355798 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 00:45:38 +0530 Subject: [PATCH 18/41] feat: add option in edit mode dropdown and preferences to toggle image gallery --- .../BrowserScripts/RemoteFunctions.js | 9 ++++-- src/LiveDevelopment/main.js | 13 +++++++++ .../Phoenix-live-preview/main.js | 29 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index ed3a52ea3a..72c3296545 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3020,6 +3020,11 @@ function RemoteFunctions(config = {}) { return getHighlightMode() !== "click"; } + // helper function to check if image ribbon gallery should be shown + function shouldShowImageRibbon() { + return config.imageRibbon !== false; + } + // helper function to clear element background highlighting function clearElementBackground(element) { if (element._originalBackgroundColor !== undefined) { @@ -3140,8 +3145,8 @@ function RemoteFunctions(config = {}) { _selectElement(element); } - // if the image is an element we show the image ribbon gallery - if(element && element.tagName.toLowerCase() === 'img') { + // if the image is an element we show the image ribbon gallery (if enabled in preferences) + if(element && element.tagName.toLowerCase() === 'img' && shouldShowImageRibbon()) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 451427b2da..97490af88e 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -70,6 +70,7 @@ define(function main(require, exports, module) { }, isProUser: isProUser, elemHighlights: "hover", // default value, this will get updated when the extension loads + imageRibbon: true, // default value, this will get updated when the extension loads // this strings are used in RemoteFunctions.js // we need to pass this through config as remoteFunctions runs in browser context and cannot // directly reference Strings file @@ -365,6 +366,17 @@ define(function main(require, exports, module) { } } + // this function is responsible to update image ribbon config + // called from live preview extension when preference changes + function updateImageRibbonConfig() { + const prefValue = PreferencesManager.get("livePreviewImageRibbon"); + config.imageRibbon = prefValue !== false; // default to true if undefined + if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { + MultiBrowserLiveDev.updateConfig(JSON.stringify(config)); + MultiBrowserLiveDev.registerHandlers(); + } + } + // init commands CommandManager.register(Strings.CMD_LIVE_HIGHLIGHT, Commands.FILE_LIVE_HIGHLIGHT, togglePreviewHighlight); CommandManager.register(Strings.CMD_RELOAD_LIVE_PREVIEW, Commands.CMD_RELOAD_LIVE_PREVIEW, _handleReloadLivePreviewCommand); @@ -393,6 +405,7 @@ define(function main(require, exports, module) { exports.togglePreviewHighlight = togglePreviewHighlight; exports.setLivePreviewEditFeaturesActive = setLivePreviewEditFeaturesActive; exports.updateElementHighlightConfig = updateElementHighlightConfig; + exports.updateImageRibbonConfig = updateImageRibbonConfig; exports.getConnectionIds = MultiBrowserLiveDev.getConnectionIds; exports.getLivePreviewDetails = MultiBrowserLiveDev.getLivePreviewDetails; exports.hideHighlight = MultiBrowserLiveDev.hideHighlight; diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 4942e15225..2e2fc56c65 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -113,6 +113,12 @@ define(function (require, exports, module) { description: Strings.LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE }); + // live preview image ribbon gallery preference (whether to show image gallery when clicking images) + const PREFERENCE_PROJECT_IMAGE_RIBBON = "livePreviewImageRibbon"; + PreferencesManager.definePreference(PREFERENCE_PROJECT_IMAGE_RIBBON, "boolean", true, { + description: "Show image gallery when clicked" + }); + const LIVE_PREVIEW_PANEL_ID = "live-preview-panel"; const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame"; const LIVE_PREVIEW_IFRAME_HTML = ` @@ -419,6 +425,7 @@ define(function (require, exports, module) { if (isEditFeaturesActive) { items.push("---"); items.push(Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON); + items.push("Show image gallery when clicked"); } const rawMode = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_MODE) || _getDefaultMode(); @@ -444,6 +451,12 @@ define(function (require, exports, module) { return `✓ ${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`; } return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`; + } else if (item === "Show image gallery when clicked") { + const isImageRibbonEnabled = PreferencesManager.get(PREFERENCE_PROJECT_IMAGE_RIBBON) !== false; + if(isImageRibbonEnabled) { + return `✓ ${item}`; + } + return `${'\u00A0'.repeat(4)}${item}`; } return item; }); @@ -492,6 +505,15 @@ define(function (require, exports, module) { const newMode = currentMode !== "click" ? "click" : "hover"; PreferencesManager.set(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, newMode); return; // Don't dismiss highlights for this option + } else if (item === "Show image gallery when clicked") { + // Don't allow image ribbon toggle if edit features are not active + if (!isEditFeaturesActive) { + return; + } + // Toggle image ribbon preference + const currentEnabled = PreferencesManager.get(PREFERENCE_PROJECT_IMAGE_RIBBON); + PreferencesManager.set(PREFERENCE_PROJECT_IMAGE_RIBBON, !currentEnabled); + return; // Don't dismiss highlights for this option } // need to dismiss the previous highlighting and stuff @@ -1296,8 +1318,15 @@ define(function (require, exports, module) { LiveDevelopment.updateElementHighlightConfig(); }); + // Handle image ribbon preference changes from this extension + PreferencesManager.on("change", PREFERENCE_PROJECT_IMAGE_RIBBON, function() { + LiveDevelopment.updateImageRibbonConfig(); + }); + // Initialize element highlight config on startup LiveDevelopment.updateElementHighlightConfig(); + // Initialize image ribbon config on startup + LiveDevelopment.updateImageRibbonConfig(); LiveDevelopment.openLivePreview(); LiveDevelopment.on(LiveDevelopment.EVENT_OPEN_PREVIEW_URL, _openLivePreviewURL); From d2b5ed3fb4e35f232b685dd38a90f7cda2520d84 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 00:55:43 +0530 Subject: [PATCH 19/41] chore: move image gallery toggle option string to strings file --- src/extensionsIntegrated/Phoenix-live-preview/main.js | 8 ++++---- src/nls/root/strings.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 2e2fc56c65..5d0653821a 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -116,7 +116,7 @@ define(function (require, exports, module) { // live preview image ribbon gallery preference (whether to show image gallery when clicking images) const PREFERENCE_PROJECT_IMAGE_RIBBON = "livePreviewImageRibbon"; PreferencesManager.definePreference(PREFERENCE_PROJECT_IMAGE_RIBBON, "boolean", true, { - description: "Show image gallery when clicked" + description: Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON }); const LIVE_PREVIEW_PANEL_ID = "live-preview-panel"; @@ -425,7 +425,7 @@ define(function (require, exports, module) { if (isEditFeaturesActive) { items.push("---"); items.push(Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON); - items.push("Show image gallery when clicked"); + items.push(Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON); } const rawMode = PreferencesManager.get(PREFERENCE_LIVE_PREVIEW_MODE) || _getDefaultMode(); @@ -451,7 +451,7 @@ define(function (require, exports, module) { return `✓ ${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`; } return `${'\u00A0'.repeat(4)}${Strings.LIVE_PREVIEW_EDIT_HIGHLIGHT_ON}`; - } else if (item === "Show image gallery when clicked") { + } else if (item === Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON) { const isImageRibbonEnabled = PreferencesManager.get(PREFERENCE_PROJECT_IMAGE_RIBBON) !== false; if(isImageRibbonEnabled) { return `✓ ${item}`; @@ -505,7 +505,7 @@ define(function (require, exports, module) { const newMode = currentMode !== "click" ? "click" : "hover"; PreferencesManager.set(PREFERENCE_PROJECT_ELEMENT_HIGHLIGHT, newMode); return; // Don't dismiss highlights for this option - } else if (item === "Show image gallery when clicked") { + } else if (item === Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON) { // Don't allow image ribbon toggle if edit features are not active if (!isEditFeaturesActive) { return; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 7b6f551094..8feef40fbc 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -195,6 +195,7 @@ define({ "LIVE_PREVIEW_MODE_HIGHLIGHT": "Highlight Mode", "LIVE_PREVIEW_MODE_EDIT": "Edit Mode", "LIVE_PREVIEW_EDIT_HIGHLIGHT_ON": "Edit Highlights on Hover", + "LIVE_PREVIEW_EDIT_IMAGE_RIBBON": "Show image gallery when clicked", "LIVE_PREVIEW_MODE_PREFERENCE": "{0} shows only the webpage, {1} connects the webpage to your code - click on elements to jump to their code and vice versa, {2} provides highlighting along with advanced element manipulation", "LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes", "LIVE_PREVIEW_PRO_FEATURE_TITLE": "Pro Feature", From 2087acfe779a87feffb30c97412c69d3897b48ee Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 01:24:50 +0530 Subject: [PATCH 20/41] feat: hide/show image ribbon as soon as the setting is updated --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 8 ++++++++ src/LiveDevelopment/LiveDevMultiBrowser.js | 10 ++++++++++ src/LiveDevelopment/main.js | 3 +++ 3 files changed, 21 insertions(+) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 72c3296545..422f0b72dc 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3628,11 +3628,19 @@ function RemoteFunctions(config = {}) { const highlightModeChanged = oldHighlightMode !== newHighlightMode; const isProStatusChanged = oldConfig.isProUser !== config.isProUser; const highlightSettingChanged = oldConfig.highlight !== config.highlight; + const imageRibbonJustEnabled = !oldConfig.imageRibbon && config.imageRibbon; // Handle significant configuration changes if (highlightModeChanged || isProStatusChanged || highlightSettingChanged) { _handleConfigurationChange(); } + + // if user enabled the image ribbon setting and an image is selected, then we show the image ribbon + if (imageRibbonJustEnabled && previouslyClickedElement && + previouslyClickedElement.tagName.toLowerCase() === 'img') { + _imageRibbonGallery = new ImageRibbonGallery(previouslyClickedElement); + } + _updateEventListeners(); return JSON.stringify(config); diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index 3b2381dc09..ed619ca880 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -718,6 +718,15 @@ define(function (require, exports, module) { } } + /** + * Dismiss image ribbon gallery if it's open + */ + function dismissImageRibbonGallery() { + if (_protocol) { + _protocol.evaluate("_LD.dismissImageRibbonGallery()"); + } + } + /** * Register event handlers in the remote browser for live preview functionality */ @@ -804,6 +813,7 @@ define(function (require, exports, module) { exports.redrawHighlight = redrawHighlight; exports.hasVisibleLivePreviewBoxes = hasVisibleLivePreviewBoxes; exports.dismissLivePreviewBoxes = dismissLivePreviewBoxes; + exports.dismissImageRibbonGallery = dismissImageRibbonGallery; exports.registerHandlers = registerHandlers; exports.updateConfig = updateConfig; exports.init = init; diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 97490af88e..dfe1d8333c 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -371,7 +371,10 @@ define(function main(require, exports, module) { function updateImageRibbonConfig() { const prefValue = PreferencesManager.get("livePreviewImageRibbon"); config.imageRibbon = prefValue !== false; // default to true if undefined + if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { + if (!prefValue) { MultiBrowserLiveDev.dismissImageRibbonGallery(); } // to remove any existing image ribbons + MultiBrowserLiveDev.updateConfig(JSON.stringify(config)); MultiBrowserLiveDev.registerHandlers(); } From c785a980e7947a4d9c58d94bb17490dcee60982c Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 15:19:49 +0530 Subject: [PATCH 21/41] refactor: update strings in dropdown as well as prefs --- src/LiveDevelopment/main.js | 4 ++-- src/extensionsIntegrated/Phoenix-live-preview/main.js | 4 ++-- src/nls/root/strings.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index dfe1d8333c..72d23b9a6e 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -366,10 +366,10 @@ define(function main(require, exports, module) { } } - // this function is responsible to update image ribbon config + // this function is responsible to update image picker config // called from live preview extension when preference changes function updateImageRibbonConfig() { - const prefValue = PreferencesManager.get("livePreviewImageRibbon"); + const prefValue = PreferencesManager.get("livePreviewImagePicker"); config.imageRibbon = prefValue !== false; // default to true if undefined if (MultiBrowserLiveDev && MultiBrowserLiveDev.status >= MultiBrowserLiveDev.STATUS_ACTIVE) { diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 5d0653821a..cb2fd9350a 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -113,8 +113,8 @@ define(function (require, exports, module) { description: Strings.LIVE_DEV_SETTINGS_ELEMENT_HIGHLIGHT_PREFERENCE }); - // live preview image ribbon gallery preference (whether to show image gallery when clicking images) - const PREFERENCE_PROJECT_IMAGE_RIBBON = "livePreviewImageRibbon"; + // live preview image picker preference (whether to show image gallery when clicking images) + const PREFERENCE_PROJECT_IMAGE_RIBBON = "livePreviewImagePicker"; PreferencesManager.definePreference(PREFERENCE_PROJECT_IMAGE_RIBBON, "boolean", true, { description: Strings.LIVE_PREVIEW_EDIT_IMAGE_RIBBON }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 8feef40fbc..08ce8d5ec3 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -195,7 +195,7 @@ define({ "LIVE_PREVIEW_MODE_HIGHLIGHT": "Highlight Mode", "LIVE_PREVIEW_MODE_EDIT": "Edit Mode", "LIVE_PREVIEW_EDIT_HIGHLIGHT_ON": "Edit Highlights on Hover", - "LIVE_PREVIEW_EDIT_IMAGE_RIBBON": "Show image gallery when clicked", + "LIVE_PREVIEW_EDIT_IMAGE_RIBBON": "Show Image Picker on Image click", "LIVE_PREVIEW_MODE_PREFERENCE": "{0} shows only the webpage, {1} connects the webpage to your code - click on elements to jump to their code and vice versa, {2} provides highlighting along with advanced element manipulation", "LIVE_PREVIEW_CONFIGURE_MODES": "Configure Live Preview Modes", "LIVE_PREVIEW_PRO_FEATURE_TITLE": "Pro Feature", From f26f706f35f3837a6c8201c3a56f71b93d75f353 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 17:51:29 +0530 Subject: [PATCH 22/41] feat: show image gallery when image tag in the source code is clicked --- .../BrowserScripts/RemoteFunctions.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 422f0b72dc..838e0dde6a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2671,6 +2671,7 @@ function RemoteFunctions(config = {}) { }, remove: function () { + _imageRibbonGallery = null; if (this.body && this.body.parentNode && this.body.parentNode === window.document.body) { window.document.body.removeChild(this.body); this.body = null; @@ -3022,6 +3023,7 @@ function RemoteFunctions(config = {}) { // helper function to check if image ribbon gallery should be shown function shouldShowImageRibbon() { + if (_imageRibbonGallery) { return false; } return config.imageRibbon !== false; } @@ -3096,6 +3098,7 @@ function RemoteFunctions(config = {}) { function _selectElement(element) { // dismiss all UI boxes and cleanup previous element state when selecting a different element dismissUIAndCleanupState(); + dismissImageRibbonGallery(); if(!isElementEditable(element)) { return false; } @@ -3109,6 +3112,11 @@ function RemoteFunctions(config = {}) { _nodeMoreOptionsBox = null; } + // if the selected element is an image, show the image ribbon gallery (make sure its enabled in preferences) + if(element && element.tagName.toLowerCase() === 'img' && shouldShowImageRibbon()) { + _imageRibbonGallery = new ImageRibbonGallery(element); + } + element._originalOutline = element.style.outline; element.style.outline = "1px solid #4285F4"; @@ -3638,7 +3646,9 @@ function RemoteFunctions(config = {}) { // if user enabled the image ribbon setting and an image is selected, then we show the image ribbon if (imageRibbonJustEnabled && previouslyClickedElement && previouslyClickedElement.tagName.toLowerCase() === 'img') { - _imageRibbonGallery = new ImageRibbonGallery(previouslyClickedElement); + if (!_imageRibbonGallery) { + _imageRibbonGallery = new ImageRibbonGallery(previouslyClickedElement); + } } _updateEventListeners(); From 6fb78b46cb1324f72a94abbf2ed18adf213eadcf Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 18:18:56 +0530 Subject: [PATCH 23/41] refactor: add important to all the boxes styling to prevent user styles from overriding it --- .../BrowserScripts/RemoteFunctions.js | 200 +++++++++--------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 838e0dde6a..3c28e926ce 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -770,7 +770,7 @@ function RemoteFunctions(config = {}) { let rect = element.getBoundingClientRect(); marker.style.position = "fixed"; - marker.style.zIndex = "2147483646"; + marker.style.zIndex = "2147483647"; marker.style.borderRadius = "2px"; marker.style.pointerEvents = "none"; @@ -1273,52 +1273,52 @@ function RemoteFunctions(config = {}) { const styles = ` :host { - all: initial; + all: initial !important; } .phoenix-more-options-box { - background-color: #4285F4; - color: white; - border-radius: 3px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - font-size: 12px; - font-family: Arial, sans-serif; - z-index: 2147483647; - position: absolute; + background-color: #4285F4 !important; + color: white !important; + border-radius: 3px !important; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important; + font-size: 12px !important; + font-family: Arial, sans-serif !important; + z-index: 2147483647 !important; + position: absolute !important; left: -1000px; top: -1000px; - box-sizing: border-box; + box-sizing: border-box !important; } .node-options { - display: flex; - align-items: center; + display: flex !important; + align-items: center !important; } .node-options span { - padding: 4px 3.9px; - cursor: pointer; - display: flex; - align-items: center; - border-radius: 0; + padding: 4px 3.9px !important; + cursor: pointer !important; + display: flex !important; + align-items: center !important; + border-radius: 0 !important; } .node-options span:first-child { - border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px !important; } .node-options span:last-child { - border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0 !important; } .node-options span:hover { - background-color: rgba(255, 255, 255, 0.15); + background-color: rgba(255, 255, 255, 0.15) !important; } .node-options span > svg { - width: 16px; - height: 16px; - display: block; + width: 16px !important; + height: 16px !important; + display: block !important; } `; @@ -1535,37 +1535,37 @@ function RemoteFunctions(config = {}) { const styles = ` :host { - all: initial; + all: initial !important; } .phoenix-node-info-box { - background-color: #4285F4; - color: white; - border-radius: 3px; - padding: 5px 8px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - font-size: 12px; - font-family: Arial, sans-serif; - z-index: 2147483647; - position: absolute; + background-color: #4285F4 !important; + color: white !important; + border-radius: 3px !important; + padding: 5px 8px !important; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important; + font-size: 12px !important; + font-family: Arial, sans-serif !important; + z-index: 2147483647 !important; + position: absolute !important; left: ${leftPos}px; top: -1000px; - max-width: 300px; - box-sizing: border-box; - pointer-events: none; + max-width: 300px !important; + box-sizing: border-box !important; + pointer-events: none !important; } .tag-name { - font-weight: bold; + font-weight: bold !important; } .id-name, .class-name { - margin-top: 3px; + margin-top: 3px !important; } .exceeded-classes { - opacity: 0.8; + opacity: 0.8 !important; } `; @@ -1682,96 +1682,96 @@ function RemoteFunctions(config = {}) { const styles = ` :host { - all: initial; + all: initial !important; } .phoenix-ai-prompt-box { - position: absolute; - background: white; - border: 1px solid #4285F4; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - font-family: Arial, sans-serif; - z-index: 2147483647; - width: ${boxWidth}px; - padding: 0; - box-sizing: border-box; + position: absolute !important; + background: white !important; + border: 1px solid #4285F4 !important; + border-radius: 8px !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important; + font-family: Arial, sans-serif !important; + z-index: 2147483647 !important; + width: ${boxWidth}px !important; + padding: 0 !important; + box-sizing: border-box !important; } .phoenix-ai-prompt-input-container { - position: relative; + position: relative !important; } .phoenix-ai-prompt-textarea { - width: 100%; - height: ${boxHeight}px; - border: none; - border-radius: 8px; - padding: 12px 40px 12px 16px; - font-size: 14px; - font-family: Arial, sans-serif; - resize: none; - outline: none; - box-sizing: border-box; - background: #f9f9f9; + width: 100% !important; + height: ${boxHeight}px !important; + border: none !important; + border-radius: 8px !important; + padding: 12px 40px 12px 16px !important; + font-size: 14px !important; + font-family: Arial, sans-serif !important; + resize: none !important; + outline: none !important; + box-sizing: border-box !important; + background: #f9f9f9 !important; } .phoenix-ai-prompt-textarea:focus { - background: white; + background: white !important; } .phoenix-ai-prompt-textarea::placeholder { - color: #999; + color: #999 !important; } .phoenix-ai-prompt-send-button { - width: 28px; - height: 28px; - border: none; - border-radius: 50%; - background: #4285F4; - color: white; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - transition: background-color 0.2s; - line-height: 0.5; + width: 28px !important; + height: 28px !important; + border: none !important; + border-radius: 50% !important; + background: #4285F4 !important; + color: white !important; + cursor: pointer !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + font-size: 14px !important; + transition: background-color 0.2s !important; + line-height: 0.5 !important; } .phoenix-ai-prompt-send-button:hover:not(:disabled) { - background: #4285F4; + background: #4285F4 !important; } .phoenix-ai-prompt-send-button:disabled { - background: #dadce0; - color: #9aa0a6; - cursor: not-allowed; + background: #dadce0 !important; + color: #9aa0a6 !important; + cursor: not-allowed !important; } .phoenix-ai-bottom-controls { - border-top: 1px solid #e0e0e0; - padding: 8px 16px; - background: #f9f9f9; - border-radius: 0 0 8px 8px; - display: flex; - align-items: center; - justify-content: space-between; + border-top: 1px solid #e0e0e0 !important; + padding: 8px 16px !important; + background: #f9f9f9 !important; + border-radius: 0 0 8px 8px !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; } .phoenix-ai-model-select { - padding: 4px 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 12px; - background: white; - outline: none; - cursor: pointer; + padding: 4px 8px !important; + border: 1px solid #ddd !important; + border-radius: 4px !important; + font-size: 12px !important; + background: white !important; + outline: none !important; + cursor: pointer !important; } .phoenix-ai-model-select:focus { - border-color: #4285F4; + border-color: #4285F4 !important; } `; @@ -2007,7 +2007,7 @@ function RemoteFunctions(config = {}) { font-weight: 600 !important; user-select: none !important; transition: all 0.2s ease !important; - z-index: 10 !important; + z-index: 2147483647 !important; padding: 2px 12px 6px 12px !important; } @@ -2159,7 +2159,7 @@ function RemoteFunctions(config = {}) { justify-content: center !important; cursor: pointer !important; font-size: 12px !important; - z-index: 2 !important; + z-index: 2147483647 !important; padding: 0 8px !important; white-space: nowrap !important; opacity: 0 !important; @@ -2873,7 +2873,7 @@ function RemoteFunctions(config = {}) { "top": offset.top + "px", "width": elementBounds.width + "px", "height": elementBounds.height + "px", - "z-index": 2000000, + "z-index": 2147483647, "margin": 0, "padding": 0, "position": "absolute", From edf4ca9dc83bf87b523b31f47b227ac62e26e229 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 20:02:23 +0530 Subject: [PATCH 24/41] feat: replace use this image button with a download button right in image --- .../BrowserScripts/RemoteFunctions.js | 69 ++++++++----------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3c28e926ce..33a5c89d18 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2145,53 +2145,41 @@ function RemoteFunctions(config = {}) { opacity: 0.85 !important; } - .phoenix-use-image-btn { + .phoenix-download-icon { position: absolute !important; - top: 6px !important; - right: 6px !important; - background: rgba(0,0,0,0.55) !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + background: rgba(0,0,0,0.7) !important; border: none !important; - color: white !important; - border-radius: 20px !important; - height: 26px !important; + color: #eee !important; + border-radius: 50% !important; + width: 21px !important; + height: 21px !important; + padding: 4px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; - font-size: 12px !important; + font-size: 16px !important; z-index: 2147483647 !important; - padding: 0 8px !important; - white-space: nowrap !important; - opacity: 0 !important; transition: all 0.2s ease !important; + pointer-events: none !important; + opacity: 0 !important; } - .phoenix-use-image-btn i { - margin-right: 0 !important; - transition: margin 0.2s !important; - } - - .phoenix-use-image-btn span { - display: none !important; - font-size: 11px !important; - font-weight: 500 !important; - } - - .phoenix-ribbon-thumb:hover .phoenix-use-image-btn { + .phoenix-ribbon-thumb:hover .phoenix-download-icon { opacity: 1 !important; + pointer-events: auto !important; } - .phoenix-use-image-btn:hover { - background: rgba(0,0,0,0.8) !important; - padding: 0 10px !important; - } - - .phoenix-use-image-btn:hover i { - margin-right: 4px !important; + .phoenix-download-icon:hover { + background: rgba(0,0,0,0.9) !important; + transform: translate(-50%, -50%) scale(1.1) !important; } - .phoenix-use-image-btn:hover span { - display: inline !important; + .phoenix-ribbon-thumb { + cursor: pointer !important; }
@@ -2530,14 +2518,15 @@ function RemoteFunctions(config = {}) { attribution.appendChild(photographer); attribution.appendChild(source); - // use image button - const useImageBtn = window.document.createElement('button'); - useImageBtn.className = 'phoenix-use-image-btn'; - useImageBtn.innerHTML = '⬇Use this image'; + // download icon + const downloadIcon = window.document.createElement('div'); + downloadIcon.className = 'phoenix-download-icon'; + downloadIcon.innerHTML = ` + + `; - // when use image button is clicked, we first generate the file name by which we need to save the image - // and then we add the image to project - useImageBtn.addEventListener('click', (e) => { + // when the image is clicked we download the image + thumbDiv.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const filename = this._generateFilename(image); @@ -2547,7 +2536,7 @@ function RemoteFunctions(config = {}) { thumbDiv.appendChild(img); thumbDiv.appendChild(attribution); - thumbDiv.appendChild(useImageBtn); + thumbDiv.appendChild(downloadIcon); rowElement.appendChild(thumbDiv); }); From b49ec402a11cc1a2ac811ba24ddc4ca04046b3eb Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 19 Sep 2025 20:56:45 +0530 Subject: [PATCH 25/41] refactor: move download icon from centre to top-right --- .../BrowserScripts/RemoteFunctions.js | 12 ++++++------ src/LiveDevelopment/main.js | 3 ++- src/nls/root/strings.js | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 33a5c89d18..821a31611a 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2147,15 +2147,14 @@ function RemoteFunctions(config = {}) { .phoenix-download-icon { position: absolute !important; - top: 50% !important; - left: 50% !important; - transform: translate(-50%, -50%) !important; + top: 8px !important; + right: 8px !important; background: rgba(0,0,0,0.7) !important; border: none !important; color: #eee !important; border-radius: 50% !important; - width: 21px !important; - height: 21px !important; + width: 18px !important; + height: 18px !important; padding: 4px !important; display: flex !important; align-items: center !important; @@ -2175,7 +2174,7 @@ function RemoteFunctions(config = {}) { .phoenix-download-icon:hover { background: rgba(0,0,0,0.9) !important; - transform: translate(-50%, -50%) scale(1.1) !important; + transform: scale(1.1) !important; } .phoenix-ribbon-thumb { @@ -2521,6 +2520,7 @@ function RemoteFunctions(config = {}) { // download icon const downloadIcon = window.document.createElement('div'); downloadIcon.className = 'phoenix-download-icon'; + downloadIcon.title = config.strings.imageGalleryUseImage; downloadIcon.innerHTML = ` `; diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 72d23b9a6e..9756345869 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -80,7 +80,8 @@ define(function main(require, exports, module) { duplicate: Strings.LIVE_DEV_MORE_OPTIONS_DUPLICATE, delete: Strings.LIVE_DEV_MORE_OPTIONS_DELETE, ai: Strings.LIVE_DEV_MORE_OPTIONS_AI, - aiPromptPlaceholder: Strings.LIVE_DEV_AI_PROMPT_PLACEHOLDER + aiPromptPlaceholder: Strings.LIVE_DEV_AI_PROMPT_PLACEHOLDER, + imageGalleryUseImage: Strings.LIVE_DEV_IMAGE_GALLERY_USE_IMAGE } }; // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 08ce8d5ec3..cf9eef9f26 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -189,6 +189,7 @@ define({ "LIVE_DEV_MORE_OPTIONS_DUPLICATE": "Duplicate", "LIVE_DEV_MORE_OPTIONS_DELETE": "Delete", "LIVE_DEV_MORE_OPTIONS_AI": "Edit with AI", + "LIVE_DEV_IMAGE_GALLERY_USE_IMAGE": "Use this image", "LIVE_DEV_AI_PROMPT_PLACEHOLDER": "Ask Phoenix AI to modify this element...", "LIVE_PREVIEW_CUSTOM_SERVER_BANNER": "Getting preview from your custom server {0}", "LIVE_PREVIEW_MODE_PREVIEW": "Preview Mode", From b18189800dd8b5af099cf3d2ec04b2be3a033d61 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 20 Sep 2025 12:45:40 +0530 Subject: [PATCH 26/41] refactor: show attribution only on hover --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 821a31611a..18093ed9ca 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2128,7 +2128,8 @@ function RemoteFunctions(config = {}) { max-width: calc(100% - 12px) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9) !important; pointer-events: none !important; - opacity: 0.95 !important; + opacity: 0 !important; + transition: all 0.2s ease !important; } .phoenix-ribbon-attribution .photographer { @@ -2172,6 +2173,10 @@ function RemoteFunctions(config = {}) { pointer-events: auto !important; } + .phoenix-ribbon-thumb:hover .phoenix-ribbon-attribution { + opacity: 1 !important; + } + .phoenix-download-icon:hover { background: rgba(0,0,0,0.9) !important; transform: scale(1.1) !important; From 70695e43a9814006aadf06f16a0bd020df260baa Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 20 Sep 2025 13:33:01 +0530 Subject: [PATCH 27/41] fix: image gallery not getting dismissed when non editable elements are clicked --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 18093ed9ca..3740ced9a6 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3154,7 +3154,8 @@ function RemoteFunctions(config = {}) { event.stopImmediatePropagation(); _imageRibbonGallery = new ImageRibbonGallery(element); - return; + } else { + dismissImageRibbonGallery(); } } @@ -3250,6 +3251,7 @@ function RemoteFunctions(config = {}) { // if no valid element present we dismiss the boxes if (!foundValidElement) { dismissUIAndCleanupState(); + dismissImageRibbonGallery(); } } From f741fd4e837f2fa9386dec65e3401c94a7a3ac95 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 20 Sep 2025 13:48:00 +0530 Subject: [PATCH 28/41] fix: when nav buttons are disabled, clicking on it triggers the image download --- .../BrowserScripts/RemoteFunctions.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 3740ced9a6..035768270d 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2311,8 +2311,8 @@ function RemoteFunctions(config = {}) { _updateNavButtons: function() { // this function is responsible to update the nav buttons - // because when we're at the very left, then we style the nav-left button differently (reduce opacity) - // and when we're at the very right and no more pages available, we reduce opacity for nav-right + // when we're at the very left, we hide the nav-left button completely + // when we're at the very right and no more pages available, we hide the nav-right button const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left'); const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right'); const container = this._shadow.querySelector('.phoenix-ribbon-strip'); @@ -2321,11 +2321,9 @@ function RemoteFunctions(config = {}) { // show/hide left button if (this.scrollPosition <= 0) { - navLeft.style.opacity = '0.3'; - navLeft.style.pointerEvents = 'none'; + navLeft.style.display = 'none'; } else { - navLeft.style.opacity = '1'; - navLeft.style.pointerEvents = 'auto'; + navLeft.style.display = 'block'; } // show/hide right button @@ -2335,11 +2333,9 @@ function RemoteFunctions(config = {}) { const hasMorePages = this.currentPage < this.totalPages; if (atEnd && !hasMorePages) { - navRight.style.opacity = '0.3'; - navRight.style.pointerEvents = 'none'; + navRight.style.display = 'none'; } else { - navRight.style.opacity = '1'; - navRight.style.pointerEvents = 'auto'; + navRight.style.display = 'block'; } }, From 7f092c3fc22f1011b678eb0f86f2f08a9f87a0b2 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 20 Sep 2025 14:17:25 +0530 Subject: [PATCH 29/41] fix: dismiss image gallery when escape key is pressed --- src/LiveDevelopment/BrowserScripts/RemoteFunctions.js | 3 --- src/LiveDevelopment/LiveDevMultiBrowser.js | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 035768270d..9008e8697f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -3184,9 +3184,6 @@ function RemoteFunctions(config = {}) { } function onKeyDown(event) { - if ((event.key === "Escape" || event.key === "Esc")) { - dismissUIAndCleanupState(); - } if (!_setup && _validEvent(event)) { window.document.addEventListener("keyup", onKeyUp); window.document.addEventListener("mouseover", onMouseOver); diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index ed619ca880..f1b3de2435 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -715,6 +715,7 @@ define(function (require, exports, module) { function dismissLivePreviewBoxes() { if (_protocol) { _protocol.evaluate("_LD.dismissUIAndCleanupState()"); + _protocol.evaluate("_LD.dismissImageRibbonGallery()"); } } From c82371818a69aa4031c6f07f668466c2bc1fd742 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 20 Sep 2025 16:35:33 +0530 Subject: [PATCH 30/41] refactor: remove absolute positioning from elements --- .../BrowserScripts/RemoteFunctions.js | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 9008e8697f..1860d94e5f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1949,7 +1949,11 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-container { width: 100% !important; height: 156px !important; - background: rgba(21,25,36,0.55) !important; + background: rgba(255, 255, 255, 0.3) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: 12px !important; position: relative !important; } @@ -2051,17 +2055,28 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-header { display: flex !important; width: 100% !important; + position: absolute !important; + top: 5px !important; + } + + .phoenix-ribbon-header-left { + width: 60% !important; + display: flex !important; + } + + .phoenix-ribbon-header-right { + width: 40% !important; + display: flex !important; + justify-content: flex-end !important; } .phoenix-ribbon-search { - position: absolute !important; - top: 8px !important; - left: 6px !important; display: flex !important; align-items: center !important; background: rgba(0,0,0,0.5) !important; padding: 5px !important; border-radius: 5px !important; + margin-left: 8px !important; } .phoenix-ribbon-search input { @@ -2080,29 +2095,20 @@ function RemoteFunctions(config = {}) { } .phoenix-ribbon-select { - position: absolute !important; - top: 8px !important; - left: 50% !important; + margin-left: 10px !important; } .phoenix-select-image-btn { - background: rgba(255,255,255,0.1) !important; - border: 1px solid rgba(255,255,255,0.2) !important; - color: #e8eaf0 !important; - padding: 4px 5px !important; + background: gray !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + color: #fff !important; + padding: 2px 4px !important; border-radius: 6px !important; font-size: 12px !important; cursor: pointer !important; - margin-left: 8px !important; - white-space: nowrap !important; transition: all 0.2s ease !important; } - .phoenix-select-image-btn:hover { - background: rgba(255,255,255,0.2) !important; - border-color: rgba(255,255,255,0.3) !important; - } - .phoenix-ribbon-close { background: rgba(0,0,0,0.5) !important; border: none !important; @@ -2110,9 +2116,7 @@ function RemoteFunctions(config = {}) { cursor: pointer !important; padding: 4px 8px !important; border-radius: 3px !important; - position: absolute !important; - right: 4px !important; - top: 10px !important; + margin-right: 16px !important; } .phoenix-ribbon-attribution { @@ -2189,15 +2193,24 @@ function RemoteFunctions(config = {}) {
-
-
@@ -2551,7 +2529,6 @@ function RemoteFunctions(config = {}) { const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input'); const searchButton = this._shadow.querySelector('.phoenix-ribbon-search-btn'); const closeButton = this._shadow.querySelector('.phoenix-ribbon-close'); - const folderSelectBtn = this._shadow.querySelector('.phoenix-ribbon-folder-select'); const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left'); const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right'); const selectImageBtn = this._shadow.querySelector('.phoenix-select-image-btn'); @@ -2605,13 +2582,6 @@ function RemoteFunctions(config = {}) { }); } - if (folderSelectBtn) { - folderSelectBtn.addEventListener('click', (e) => { - e.stopPropagation(); - console.log('i got clicked'); - }); - } - if (navLeft) { navLeft.addEventListener('click', (e) => { e.stopPropagation(); diff --git a/src/LiveDevelopment/LivePreviewEdit.js b/src/LiveDevelopment/LivePreviewEdit.js index f7c9439b00..ddddb8c465 100644 --- a/src/LiveDevelopment/LivePreviewEdit.js +++ b/src/LiveDevelopment/LivePreviewEdit.js @@ -755,12 +755,39 @@ define(function (require, exports, module) { const projectRoot = ProjectManager.getProjectRoot(); if (!projectRoot) { return; } - getUniqueFilename(projectRoot.fullPath, filename, extnName).then((uniqueFilename) => { + // phoenix-assets folder, all the images will be stored inside this + const phoenixAssetsPath = projectRoot.fullPath + "phoenix-code-assets/"; + const phoenixAssetsDir = FileSystem.getDirectoryForPath(phoenixAssetsPath); + + // check if the phoenix-assets dir exists + // if present, download the image inside it, if not create the dir and then download the image inside it + phoenixAssetsDir.exists((err, exists) => { + if (err) { return; } + + if (!exists) { + phoenixAssetsDir.create((err) => { + if (err) { + console.error('Error creating phoenix-code-assets directory:', err); + return; + } + _downloadImageToPhoenixAssets(message, filename, extnName, phoenixAssetsDir); + }); + } else { + _downloadImageToPhoenixAssets(message, filename, extnName, phoenixAssetsDir); + } + }); + } + + /** + * Helper function to download image to phoenix-assets folder + */ + function _downloadImageToPhoenixAssets(message, filename, extnName, phoenixAssetsDir) { + getUniqueFilename(phoenixAssetsDir.fullPath, filename, extnName).then((uniqueFilename) => { // check if the image is loaded from computer or from remote if (message.isLocalFile && message.imageData) { - _handleUseThisImageLocalFiles(message, uniqueFilename, projectRoot); + _handleUseThisImageLocalFiles(message, uniqueFilename, phoenixAssetsDir); } else { - _handleUseThisImageRemote(message, uniqueFilename, projectRoot); + _handleUseThisImageRemote(message, uniqueFilename, phoenixAssetsDir); } }).catch(error => { console.error('Something went wrong when trying to use this image', error); From c327ae7cbaf8d377ba5e3f1d2812eb7723f3e639 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 24 Sep 2025 14:32:43 +0530 Subject: [PATCH 40/41] feat: set max dimension such that when its not defined we can use it --- .../BrowserScripts/RemoteFunctions.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index d9dfed47be..012c6bea78 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1934,6 +1934,9 @@ function RemoteFunctions(config = {}) { this.allImages = []; this.imagesPerPage = 10; this.scrollPosition = 0; + this.maxWidth = '800px'; // when current image dimension is not defined we use this as unsplash images are very large + this.maxHeight = '600px'; + this.create(); } @@ -2637,8 +2640,9 @@ function RemoteFunctions(config = {}) { // show hovered image along with dimensions thumbDiv.addEventListener('mouseenter', () => { - this.element.style.width = this._originalImageStyle.width; - this.element.style.height = this._originalImageStyle.height; + this.element.style.width = this._originalImageStyle.width || this.maxWidth; + this.element.style.height = this._originalImageStyle.height || this.maxHeight; + this.element.style.objectFit = this._originalImageStyle.objectFit || 'cover'; this.element.src = image.url || image.thumb_url; }); @@ -2689,7 +2693,14 @@ function RemoteFunctions(config = {}) { e.preventDefault(); const filename = this._generateFilename(image); const extnName = ".jpg"; - this._useImage(image.url, filename, extnName, false); + + const targetWidth = this._originalImageStyle.width || this.maxWidth; + const targetHeight = this._originalImageStyle.height || this.maxHeight; + const widthNum = parseInt(targetWidth); + const heightNum = parseInt(targetHeight); + + const downloadUrl = image.url ? `${image.url}?w=${widthNum}&h=${heightNum}&fit=crop` : image.thumb_url; + this._useImage(downloadUrl, filename, extnName, false); }); thumbDiv.appendChild(img); From 029ab8b152ca12d008f4b5f853de0761cf70b53b Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 24 Sep 2025 19:51:56 +0530 Subject: [PATCH 41/41] feat: show downloading indicator when image is being downloaded --- .../BrowserScripts/RemoteFunctions.js | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 012c6bea78..26a43dbefc 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -2233,6 +2233,40 @@ function RemoteFunctions(config = {}) { .phoenix-ribbon-thumb { cursor: pointer !important; } + + .phoenix-ribbon-thumb.downloading { + opacity: 0.6 !important; + pointer-events: none !important; + } + + .phoenix-download-indicator { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + background: rgba(0, 0, 0, 0.8) !important; + border-radius: 50% !important; + width: 40px !important; + height: 40px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + z-index: 10 !important; + } + + .phoenix-download-spinner { + width: 20px !important; + height: 20px !important; + border: 2px solid rgba(255, 255, 255, 0.3) !important; + border-top: 2px solid #fff !important; + border-radius: 50% !important; + animation: phoenix-spin 1s linear infinite !important; + } + + @keyframes phoenix-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + }
@@ -2691,6 +2725,13 @@ function RemoteFunctions(config = {}) { thumbDiv.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); + + // prevent multiple downloads of the same image + if (thumbDiv.classList.contains('downloading')) { return; } + + // show download indicator + this._showDownloadIndicator(thumbDiv); + const filename = this._generateFilename(image); const extnName = ".jpg"; @@ -2700,7 +2741,7 @@ function RemoteFunctions(config = {}) { const heightNum = parseInt(targetHeight); const downloadUrl = image.url ? `${image.url}?w=${widthNum}&h=${heightNum}&fit=crop` : image.thumb_url; - this._useImage(downloadUrl, filename, extnName, false); + this._useImage(downloadUrl, filename, extnName, false, thumbDiv); }); thumbDiv.appendChild(img); @@ -2736,7 +2777,7 @@ function RemoteFunctions(config = {}) { return `${cleanSearchTerm}-by-${cleanPhotographerName}`; }, - _useImage: function(imageUrl, filename, extnName, isLocalFile) { + _useImage: function(imageUrl, filename, extnName, isLocalFile, thumbDiv) { // send the message to the editor instance to save the image and update the source code const tagId = this.element.getAttribute("data-brackets-id"); @@ -2763,6 +2804,14 @@ function RemoteFunctions(config = {}) { } window._Brackets_MessageBroker.send(messageData); + + // if thumbDiv is provided, hide the download indicator after a reasonable timeout + // this is to make sure that the indicator is always removed even if there's no explicit success callback + if (thumbDiv) { + setTimeout(() => { + this._hideDownloadIndicator(thumbDiv); + }, 3000); + } }, _handleLocalImageSelection: function(file) { @@ -2783,7 +2832,7 @@ function RemoteFunctions(config = {}) { const filename = cleanName || 'selected-image'; // Use the unified _useImage method with isLocalFile flag - this._useImage(imageDataUrl, filename, extension, true); + this._useImage(imageDataUrl, filename, extension, true, null); // Close the ribbon after successful selection this.remove(); @@ -2824,6 +2873,32 @@ function RemoteFunctions(config = {}) { window.document.body.removeChild(this.body); this.body = null; } + }, + + _showDownloadIndicator: function(thumbDiv) { + // add downloading class + thumbDiv.classList.add('downloading'); + + // create download indicator + const indicator = window.document.createElement('div'); + indicator.className = 'phoenix-download-indicator'; + + const spinner = window.document.createElement('div'); + spinner.className = 'phoenix-download-spinner'; + + indicator.appendChild(spinner); + thumbDiv.appendChild(indicator); + }, + + _hideDownloadIndicator: function(thumbDiv) { + // remove downloading class + thumbDiv.classList.remove('downloading'); + + // remove download indicator + const indicator = thumbDiv.querySelector('.phoenix-download-indicator'); + if (indicator) { + indicator.remove(); + } } };