Skip to content

Commit 576e202

Browse files
refactor: update images manager
1 parent 5ffd3aa commit 576e202

File tree

7 files changed

+187
-81
lines changed

7 files changed

+187
-81
lines changed

apps/studio/electron/main/assets/images.ts

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,87 @@ import { DefaultSettings } from '@onlook/models/constants';
33
import { promises as fs, readFileSync } from 'fs';
44
import mime from 'mime-lite';
55
import path from 'path';
6+
import { detectRouterType } from '../pages';
7+
8+
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico'];
9+
const MAX_FILENAME_LENGTH = 255;
10+
const VALID_FILENAME_REGEX = /^[a-zA-Z0-9-_. ]+$/;
11+
12+
async function getImageFolderPath(projectRoot: string, folder?: string): Promise<string> {
13+
if (folder) {
14+
return path.join(projectRoot, folder);
15+
}
16+
17+
const routerType = await detectRouterType(projectRoot);
18+
return routerType?.basePath
19+
? routerType.basePath
20+
: path.join(projectRoot, DefaultSettings.IMAGE_FOLDER);
21+
}
22+
23+
// Helper function to validate and process image file
24+
function processImageFile(filePath: string, folder: string): ImageContentData {
25+
const image = readFileSync(filePath, { encoding: 'base64' });
26+
const mimeType = mime.getType(filePath) || 'application/octet-stream';
27+
28+
return {
29+
fileName: path.basename(filePath),
30+
content: `data:${mimeType};base64,${image}`,
31+
mimeType,
32+
folder,
33+
};
34+
}
635

736
async function scanImagesDirectory(projectRoot: string): Promise<ImageContentData[]> {
8-
const imagesPath = path.join(projectRoot, DefaultSettings.IMAGE_FOLDER);
937
const images: ImageContentData[] = [];
1038

39+
const publicImagesPath = path.join(projectRoot, DefaultSettings.IMAGE_FOLDER);
1140
try {
12-
const entries = await fs.readdir(imagesPath, { withFileTypes: true });
13-
14-
for (const entry of entries) {
15-
if (entry.isFile()) {
16-
const extension = path.extname(entry.name).toLowerCase();
17-
// Common image extensions
18-
if (
19-
['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico'].includes(extension)
20-
) {
21-
const imagePath = path.join(imagesPath, entry.name);
22-
const image = readFileSync(imagePath, { encoding: 'base64' });
23-
const mimeType = mime.getType(imagePath) || 'application/octet-stream';
24-
images.push({
25-
fileName: entry.name,
26-
content: `data:${mimeType};base64,${image}`,
27-
mimeType,
28-
});
29-
}
41+
const publicEntries = await fs.readdir(publicImagesPath, { withFileTypes: true });
42+
for (const entry of publicEntries) {
43+
if (
44+
entry.isFile() &&
45+
SUPPORTED_IMAGE_EXTENSIONS.includes(path.extname(entry.name).toLowerCase())
46+
) {
47+
const imagePath = path.join(publicImagesPath, entry.name);
48+
images.push(processImageFile(imagePath, DefaultSettings.IMAGE_FOLDER));
3049
}
3150
}
51+
} catch (error) {
52+
console.error('Error scanning public images directory:', error);
53+
}
3254

33-
return images;
55+
// Scan app directory images
56+
const appDir = path.join(projectRoot, 'app');
57+
try {
58+
const appImages = await findImagesInDirectory(appDir);
59+
for (const imagePath of appImages) {
60+
images.push(processImageFile(imagePath, 'app'));
61+
}
3462
} catch (error) {
35-
console.error('Error scanning images directory:', error);
36-
return [];
63+
console.error('Error scanning app directory images:', error);
3764
}
65+
66+
return images;
67+
}
68+
69+
async function findImagesInDirectory(dirPath: string): Promise<string[]> {
70+
const imageFiles: string[] = [];
71+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
72+
73+
for (const entry of entries) {
74+
const fullPath = path.join(dirPath, entry.name);
75+
76+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
77+
imageFiles.push(...(await findImagesInDirectory(fullPath)));
78+
} else if (
79+
entry.isFile() &&
80+
SUPPORTED_IMAGE_EXTENSIONS.includes(path.extname(entry.name).toLowerCase())
81+
) {
82+
imageFiles.push(fullPath);
83+
}
84+
}
85+
86+
return imageFiles;
3887
}
3988

4089
export async function scanNextJsImages(projectRoot: string): Promise<ImageContentData[]> {
@@ -72,16 +121,15 @@ async function getUniqueFileName(imageFolder: string, fileName: string): Promise
72121
}
73122

74123
export async function saveImageToProject(
75-
projectFolder: string,
76-
content: string,
77-
fileName: string,
124+
projectRoot: string,
125+
image: ImageContentData,
78126
): Promise<string> {
79127
try {
80-
const imageFolder = path.join(projectFolder, DefaultSettings.IMAGE_FOLDER);
81-
const uniqueFileName = await getUniqueFileName(imageFolder, fileName);
128+
const imageFolder = await getImageFolderPath(projectRoot, image.folder);
129+
const uniqueFileName = await getUniqueFileName(imageFolder, image.fileName);
82130
const imagePath = path.join(imageFolder, uniqueFileName);
83131

84-
const buffer = Buffer.from(content, 'base64');
132+
const buffer = Buffer.from(image.content, 'base64');
85133
await fs.writeFile(imagePath, buffer);
86134
return imagePath;
87135
} catch (error) {
@@ -92,11 +140,11 @@ export async function saveImageToProject(
92140

93141
export async function deleteImageFromProject(
94142
projectRoot: string,
95-
imageName: string,
143+
image: ImageContentData,
96144
): Promise<string> {
97145
try {
98-
const imageFolder = path.join(projectRoot, DefaultSettings.IMAGE_FOLDER);
99-
const imagePath = path.join(imageFolder, imageName);
146+
const imageFolder = await getImageFolderPath(projectRoot, image.folder);
147+
const imagePath = path.join(imageFolder, image.fileName);
100148
await fs.unlink(imagePath);
101149
return imagePath;
102150
} catch (error) {
@@ -107,32 +155,28 @@ export async function deleteImageFromProject(
107155

108156
export async function renameImageInProject(
109157
projectRoot: string,
110-
imageName: string,
158+
image: ImageContentData,
111159
newName: string,
112160
): Promise<string> {
113-
if (!imageName || !newName) {
161+
if (!image.fileName || !newName) {
114162
throw new Error('Image name and new name are required');
115163
}
116164

117-
const imageFolder = path.join(projectRoot, DefaultSettings.IMAGE_FOLDER);
118-
const oldImagePath = path.join(imageFolder, imageName);
165+
const imageFolder = await getImageFolderPath(projectRoot, image.folder);
166+
const oldImagePath = path.join(imageFolder, image.fileName);
119167
const newImagePath = path.join(imageFolder, newName);
120168

121169
try {
122170
await validateRename(oldImagePath, newImagePath);
123171
await fs.rename(oldImagePath, newImagePath);
124-
125-
await updateImageReferences(projectRoot, imageName, newName);
172+
await updateImageReferences(projectRoot, image.fileName, newName);
126173
return newImagePath;
127174
} catch (error) {
128175
console.error('Error renaming image:', error);
129176
throw error;
130177
}
131178
}
132179

133-
const MAX_FILENAME_LENGTH = 255;
134-
const VALID_FILENAME_REGEX = /^[a-zA-Z0-9-_. ]+$/;
135-
136180
async function validateRename(oldImagePath: string, newImagePath: string): Promise<void> {
137181
try {
138182
await fs.access(oldImagePath);

apps/studio/electron/main/events/asset.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
deleteImageFromProject,
77
renameImageInProject,
88
} from '../assets/images';
9+
import type { ImageContentData } from '@onlook/models/actions';
910

1011
export function listenForAssetMessages() {
1112
ipcMain.handle(MainChannels.SCAN_IMAGES_IN_PROJECT, async (_event, projectRoot: string) => {
@@ -19,31 +20,49 @@ export function listenForAssetMessages() {
1920
_event,
2021
{
2122
projectFolder,
22-
content,
23-
fileName,
23+
image,
2424
}: {
2525
projectFolder: string;
26-
content: string;
27-
fileName: string;
26+
image: ImageContentData;
2827
},
2928
) => {
30-
const imagePath = await saveImageToProject(projectFolder, content, fileName);
29+
const imagePath = await saveImageToProject(projectFolder, image);
3130
return imagePath;
3231
},
3332
);
3433

3534
ipcMain.handle(
3635
MainChannels.DELETE_IMAGE_FROM_PROJECT,
37-
async (_event, projectRoot: string, imageName: string) => {
38-
const imagePath = await deleteImageFromProject(projectRoot, imageName);
36+
async (
37+
_event,
38+
{
39+
projectFolder,
40+
image,
41+
}: {
42+
projectFolder: string;
43+
image: ImageContentData;
44+
},
45+
) => {
46+
const imagePath = await deleteImageFromProject(projectFolder, image);
3947
return imagePath;
4048
},
4149
);
4250

4351
ipcMain.handle(
4452
MainChannels.RENAME_IMAGE_IN_PROJECT,
45-
async (_event, projectRoot: string, imageName: string, newName: string) => {
46-
const imagePath = await renameImageInProject(projectRoot, imageName, newName);
53+
async (
54+
_event,
55+
{
56+
projectFolder,
57+
image,
58+
newName,
59+
}: {
60+
projectFolder: string;
61+
image: ImageContentData;
62+
newName: string;
63+
},
64+
) => {
65+
const imagePath = await renameImageInProject(projectFolder, image, newName);
4766
return imagePath;
4867
},
4968
);

apps/studio/src/components/Modals/Settings/Site/Favicon.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,11 @@ export const Favicon = forwardRef<
108108
onDragOver={handleDragOver}
109109
onDragLeave={handleDragLeave}
110110
onDrop={handleDrop}
111-
style={{
112-
backgroundImage: selectedImage ? `url(${selectedImage})` : 'none',
113-
}}
114111
>
115112
<input
116113
ref={fileInputRef}
117114
type="file"
118-
accept="image/*"
115+
accept=".ico"
119116
className="hidden"
120117
id="favicon-upload"
121118
onChange={handleFileSelect}

apps/studio/src/components/Modals/Settings/Site/index.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useEditorEngine, useProjectsManager } from '@/components/Context';
22
import { useMetadataForm } from '@/hooks/useMetadataForm';
3-
import { DefaultSettings, type PageMetadata } from '@onlook/models';
3+
import { DefaultSettings, type ImageContentData, type PageMetadata } from '@onlook/models';
4+
import { MainChannels } from '@onlook/models/constants';
5+
import { invokeMainChannel } from '@/lib/utils';
46
import { toast } from '@onlook/ui/use-toast';
57
import { observer } from 'mobx-react-lite';
68
import { useState } from 'react';
@@ -53,10 +55,44 @@ export const SiteTab = observer(() => {
5355
}
5456

5557
if (uploadedFavicon) {
56-
await editorEngine.image.upload(uploadedFavicon);
57-
const faviconPath = `/${DefaultSettings.IMAGE_FOLDER.replace(/^public\//, '')}/${uploadedFavicon.name}`;
58+
// Delete old favicon if it exists
59+
if (siteSetting?.icons?.icon) {
60+
const oldFavicon = editorEngine.image.assets.find(
61+
(image) => image.fileName === 'favicon.ico',
62+
);
63+
if (oldFavicon) {
64+
try {
65+
await invokeMainChannel(MainChannels.DELETE_IMAGE_FROM_PROJECT, {
66+
projectFolder: project.folderPath,
67+
image: oldFavicon,
68+
});
69+
} catch (error) {
70+
console.warn('Failed to delete old favicon:', error);
71+
}
72+
}
73+
}
74+
75+
const buffer = await uploadedFavicon.arrayBuffer();
76+
const base64String = btoa(
77+
new Uint8Array(buffer).reduce(
78+
(data, byte) => data + String.fromCharCode(byte),
79+
'',
80+
),
81+
);
82+
83+
const image: ImageContentData = {
84+
content: base64String,
85+
fileName: 'favicon.ico',
86+
mimeType: 'image/x-icon',
87+
};
88+
89+
await invokeMainChannel(MainChannels.SAVE_IMAGE_TO_PROJECT, {
90+
projectFolder: project.folderPath,
91+
image,
92+
});
93+
5894
updatedMetadata.icons = {
59-
icon: faviconPath,
95+
icon: '/favicon.ico',
6096
};
6197
}
6298
if (uploadedImage) {
@@ -86,6 +122,8 @@ export const SiteTab = observer(() => {
86122
});
87123

88124
await editorEngine.pages.updateMetadataPage('/', updatedMetadata);
125+
await editorEngine.image.scanImages();
126+
89127
setUploadedFavicon(null);
90128
setIsDirty(false);
91129
toast({

0 commit comments

Comments
 (0)