Skip to content

Commit 43e041e

Browse files
committed
feat: add use this image option in image ribbon gallery
1 parent 6307a2d commit 43e041e

File tree

2 files changed

+185
-8
lines changed

2 files changed

+185
-8
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,55 @@ function RemoteFunctions(config = {}) {
21132113
font-size: 9px !important;
21142114
opacity: 0.85 !important;
21152115
}
2116+
2117+
.phoenix-use-image-btn {
2118+
position: absolute !important;
2119+
top: 6px !important;
2120+
right: 6px !important;
2121+
background: rgba(0,0,0,0.55) !important;
2122+
border: none !important;
2123+
color: white !important;
2124+
border-radius: 20px !important;
2125+
height: 26px !important;
2126+
display: flex !important;
2127+
align-items: center !important;
2128+
justify-content: center !important;
2129+
cursor: pointer !important;
2130+
font-size: 12px !important;
2131+
z-index: 2 !important;
2132+
padding: 0 8px !important;
2133+
white-space: nowrap !important;
2134+
opacity: 0 !important;
2135+
transition: all 0.2s ease !important;
2136+
}
2137+
2138+
.phoenix-use-image-btn i {
2139+
margin-right: 0 !important;
2140+
transition: margin 0.2s !important;
2141+
}
2142+
2143+
.phoenix-use-image-btn span {
2144+
display: none !important;
2145+
font-size: 11px !important;
2146+
font-weight: 500 !important;
2147+
}
2148+
2149+
.phoenix-ribbon-thumb:hover .phoenix-use-image-btn {
2150+
opacity: 1 !important;
2151+
}
2152+
2153+
.phoenix-use-image-btn:hover {
2154+
background: rgba(0,0,0,0.8) !important;
2155+
padding: 0 10px !important;
2156+
}
2157+
2158+
.phoenix-use-image-btn:hover i {
2159+
margin-right: 4px !important;
2160+
}
2161+
2162+
.phoenix-use-image-btn:hover span {
2163+
display: inline !important;
2164+
}
21162165
</style>
21172166
<div class="phoenix-image-ribbon">
21182167
<div class="phoenix-ribbon-header">
@@ -2136,6 +2185,7 @@ function RemoteFunctions(config = {}) {
21362185
},
21372186

21382187
_fetchImages: function(searchQuery = 'sunshine') {
2188+
this._currentSearchQuery = searchQuery;
21392189
const apiUrl = `https://images.phcode.dev/api/images/search?q=${encodeURIComponent(searchQuery)}&per_page=10`;
21402190
this._showLoading();
21412191

@@ -2254,14 +2304,7 @@ function RemoteFunctions(config = {}) {
22542304

22552305
const photographer = window.document.createElement('span');
22562306
photographer.className = 'photographer';
2257-
2258-
// unsplash attribution is in the format 'Photo by <name> on Unsplash'
2259-
// we extract the name from there
2260-
let photographerName = 'Anonymous'; // if not present, show anonymous
2261-
if (image.attribution) {
2262-
const match = image.attribution.match(/Photo by (.+) on Unsplash/);
2263-
if (match) { photographerName = match[1]; }
2264-
}
2307+
const photographerName = this._getPhotographerName(image);
22652308
photographer.textContent = photographerName;
22662309

22672310
const source = window.document.createElement('span');
@@ -2271,8 +2314,24 @@ function RemoteFunctions(config = {}) {
22712314
attribution.appendChild(photographer);
22722315
attribution.appendChild(source);
22732316

2317+
// use image button
2318+
const useImageBtn = window.document.createElement('button');
2319+
useImageBtn.className = 'phoenix-use-image-btn';
2320+
useImageBtn.innerHTML = '⬇<span>Use this image</span>';
2321+
2322+
// when use image button is clicked, we first generate the file name by which we need to save the image
2323+
// and then we add the image to project
2324+
useImageBtn.addEventListener('click', (e) => {
2325+
e.stopPropagation();
2326+
e.preventDefault();
2327+
const filename = this._generateFilename(image);
2328+
const extnName = ".jpg";
2329+
this._useImage(image.url, filename, extnName);
2330+
});
2331+
22742332
thumbDiv.appendChild(img);
22752333
thumbDiv.appendChild(attribution);
2334+
thumbDiv.appendChild(useImageBtn);
22762335
rowElement.appendChild(thumbDiv);
22772336
});
22782337
},
@@ -2285,6 +2344,42 @@ function RemoteFunctions(config = {}) {
22852344
rowElement.className = 'phoenix-ribbon-row phoenix-ribbon-error';
22862345
},
22872346

2347+
_getPhotographerName: function(image) {
2348+
// unsplash API returns attribution in format 'Photo by <name> on Unsplash'
2349+
// this function is responsible to get the name
2350+
if (image.attribution) {
2351+
const match = image.attribution.match(/Photo by (.+) on Unsplash/);
2352+
if (match) {
2353+
return match[1];
2354+
}
2355+
}
2356+
return 'Anonymous';
2357+
},
2358+
2359+
// file name with which we need to save the image
2360+
_generateFilename: function(image) {
2361+
const photographerName = this._getPhotographerName(image);
2362+
const searchTerm = this._currentSearchQuery || 'image';
2363+
2364+
// clean the search term and the photograper name to write in file name
2365+
const cleanSearchTerm = searchTerm.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
2366+
const cleanPhotographerName = photographerName.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
2367+
2368+
return `${cleanSearchTerm}-by-${cleanPhotographerName}`;
2369+
},
2370+
2371+
_useImage: function(imageUrl, filename, extnName) {
2372+
// to use the image we send the message to the editor instance
2373+
// this is handled inside liveDevProtocol.js file
2374+
window._Brackets_MessageBroker.send({
2375+
livePreviewEditEnabled: true,
2376+
useImage: true,
2377+
imageUrl: imageUrl,
2378+
filename: filename,
2379+
extnName: extnName
2380+
});
2381+
},
2382+
22882383
create: function() {
22892384
this.remove(); // remove existing ribbon if already present
22902385

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ define(function (require, exports, module) {
2828
const HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation");
2929
const LiveDevMultiBrowser = require("LiveDevelopment/LiveDevMultiBrowser");
3030
const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror");
31+
const ProjectManager = require("project/ProjectManager");
32+
const FileSystem = require("filesystem/FileSystem");
3133

3234
/**
3335
* This function syncs text content changes between the original source code
@@ -593,6 +595,80 @@ define(function (require, exports, module) {
593595
// write the AI implementation here...@abose
594596
}
595597

598+
/**
599+
* this is a helper function to make sure that when saving a new image, there's no existing file with the same name
600+
* @param {String} basePath - this is the base path where the image will be saved
601+
* @param {String} filename - the name of the image file
602+
* @param {String} extnName - the name of the image extension. (defaults to "jpg")
603+
* @returns {String} - the new file name
604+
*/
605+
function getUniqueFilename(basePath, filename, extnName) {
606+
let counter = 0;
607+
let uniqueFilename = filename + extnName;
608+
609+
function checkAndIncrement() {
610+
const filePath = basePath + uniqueFilename;
611+
const file = FileSystem.getFileForPath(filePath);
612+
613+
return new Promise((resolve) => {
614+
file.exists((err, exists) => {
615+
if (exists) {
616+
counter++;
617+
uniqueFilename = `${filename}-${counter}${extnName}`;
618+
checkAndIncrement().then(resolve);
619+
} else {
620+
resolve(uniqueFilename);
621+
}
622+
});
623+
});
624+
}
625+
626+
return checkAndIncrement();
627+
}
628+
629+
/**
630+
* This function is called when 'use this image' button is clicked in the image ribbon gallery
631+
* this is responsible to download the image in the appropriate place
632+
* and also change the src attribute of the element
633+
* @param {Object} message - the message object which stores all the required data for this operation
634+
*/
635+
function _handleUseThisImage(message) {
636+
const { imageUrl, filename } = message;
637+
const extnName = message.extnName || "jpg";
638+
639+
const projectRoot = ProjectManager.getProjectRoot();
640+
if (!projectRoot) {
641+
console.error('No project root found');
642+
return;
643+
}
644+
645+
getUniqueFilename(projectRoot.fullPath, filename, extnName).then((uniqueFilename) => {
646+
fetch(imageUrl)
647+
.then(response => {
648+
if (!response.ok) {
649+
throw new Error(`HTTP error! status: ${response.status}`);
650+
}
651+
return response.arrayBuffer();
652+
})
653+
.then(arrayBuffer => {
654+
const uint8Array = new Uint8Array(arrayBuffer);
655+
656+
const targetPath = projectRoot.fullPath + uniqueFilename;
657+
window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array),
658+
{ encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => {
659+
if (err) {
660+
console.error('Failed to save image:', err);
661+
}
662+
});
663+
})
664+
.catch(error => {
665+
console.error('Failed to fetch image:', error);
666+
});
667+
}).catch(error => {
668+
console.error('Something went wrong when trying to use this image', error);
669+
});
670+
}
671+
596672
/**
597673
* This is the main function that is exported.
598674
* it will be called by LiveDevProtocol when it receives a message from RemoteFunctions.js
@@ -623,6 +699,12 @@ define(function (require, exports, module) {
623699
return;
624700
}
625701

702+
// use this image
703+
if (message.useImage && message.imageUrl && message.filename) {
704+
_handleUseThisImage(message);
705+
return;
706+
}
707+
626708
if (!message.element || !message.tagId) {
627709
// check for undo
628710
if (message.undoLivePreviewOperation || message.redoLivePreviewOperation) {

0 commit comments

Comments
 (0)