Feature: Implemented thumbnail.png saving and load. Projects on homepage will have images (#5133)

* feature: implemented saving thumbnail.png to have project thumbnails in the home page

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* Fix the failing test by increasing window height (related to toast covering now-larger project tiles)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
This commit is contained in:
Kevin Nadro
2025-02-01 06:40:02 -05:00
committed by GitHub
parent b962b5fcb3
commit 229433126d
13 changed files with 70 additions and 20 deletions

View File

@ -572,7 +572,7 @@ test(
fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await page.setBodyDimensions({ width: 1200, height: 600 })
page.on('console', console.log)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useHotKeyListener } from './hooks/useHotKeyListener'
import { Stream } from './components/Stream'
import { AppHeader } from './components/AppHeader'
@ -24,6 +24,10 @@ import { UnitsMenu } from 'components/UnitsMenu'
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry'
import { takeScreenshotOfVideoStreamCanvas } from 'lib/screenshot'
import { writeProjectThumbnailFile } from 'lib/desktop'
import { useRouteLoaderData } from 'react-router-dom'
import { useEngineCommands } from 'components/EngineCommands'
import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk()
@ -55,6 +59,12 @@ export function App() {
const projectName = project?.name || null
const projectPath = project?.path || null
const [commands] = useEngineCommands()
const [capturedCanvas, setCapturedCanvas] = useState(false)
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const lastCommandType = commands[commands.length - 1]?.type
useEffect(() => {
onProjectOpen({ name: projectName, path: projectPath }, file || null)
}, [projectName, projectPath])
@ -92,6 +102,28 @@ export function App() {
useEngineConnectionSubscriptions()
// Generate thumbnail.png when loading the app
useEffect(() => {
if (!capturedCanvas && lastCommandType === 'execution-done') {
setTimeout(() => {
const projectDirectoryWithoutEndingSlash = loaderData?.project?.path
if (!projectDirectoryWithoutEndingSlash) {
return
}
const dataUrl: string = takeScreenshotOfVideoStreamCanvas()
// zoom to fit command does not wait, wait 500ms to see if zoom to fit finishes
writeProjectThumbnailFile(dataUrl, projectDirectoryWithoutEndingSlash)
.then(() => {})
.catch((e) => {
console.error(
`Failed to generate thumbnail for ${projectDirectoryWithoutEndingSlash}`
)
console.error(e)
})
}, 500)
}
}, [lastCommandType])
return (
<div className="relative h-full flex flex-col" ref={ref}>
<AppHeader

View File

@ -1,10 +1,8 @@
import { useEngineCommands } from './EngineCommands'
import { Spinner } from './Spinner'
import { CustomIcon } from './CustomIcon'
export const ModelStateIndicator = () => {
const [commands] = useEngineCommands()
const lastCommandType = commands[commands.length - 1]?.type
let className = 'w-6 h-6 '

View File

@ -2,7 +2,7 @@ import { FormEvent, useEffect, useRef, useState } from 'react'
import { PATHS } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from '../ActionButton'
import { FILE_EXT } from 'lib/constants'
import { FILE_EXT, PROJECT_IMAGE_NAME } from 'lib/constants'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from '../Tooltip'
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
@ -29,7 +29,7 @@ function ProjectCard({
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
// const [imageUrl, setImageUrl] = useState('')
const [imageUrl, setImageUrl] = useState('')
let inputRef = useRef<HTMLInputElement>(null)
@ -53,18 +53,21 @@ function ProjectCard({
setNumberOfFolders(project.directory_count)
}
// async function setupImageUrl() {
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
// if (await exists(projectImagePath)) {
// const imageData = await readFile(projectImagePath)
// const blob = new Blob([imageData], { type: 'image/jpg' })
// const imageUrl = URL.createObjectURL(blob)
// setImageUrl(imageUrl)
// }
// }
async function setupImageUrl() {
const projectImagePath = window.electron.path.join(
project.path,
PROJECT_IMAGE_NAME
)
if (await window.electron.exists(projectImagePath)) {
const imageData = await window.electron.readFile(projectImagePath)
const blob = new Blob([imageData], { type: 'image/png' })
const imageUrl = URL.createObjectURL(blob)
setImageUrl(imageUrl)
}
}
void getNumberOfFiles()
// void setupImageUrl()
void setupImageUrl()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
@ -84,7 +87,7 @@ function ProjectCard({
to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`}
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
>
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
<div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
{imageUrl && (
<img
src={imageUrl}
@ -92,7 +95,7 @@ function ProjectCard({
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
/>
)}
</div> */}
</div>
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
{isEditing ? (
<ProjectCardRenameForm

View File

@ -26,7 +26,7 @@ export const FILE_EXT = '.kcl'
/** Default file to open when a project is opened */
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
/** Thumbnail file name */
export const PROJECT_IMAGE_NAME = `main.jpg` as const
export const PROJECT_IMAGE_NAME = `thumbnail.png` as const
/** The localStorage key for last-opened projects */
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
/** The default name given to new kcl files in a project */

View File

@ -10,6 +10,7 @@ import {
import {
PROJECT_ENTRYPOINT,
PROJECT_FOLDER,
PROJECT_IMAGE_NAME,
PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME,
TELEMETRY_FILE_NAME,
@ -625,3 +626,19 @@ export const getUser = async (
}
return Promise.reject(new Error('unreachable'))
}
export const writeProjectThumbnailFile = async (
dataUrl: string,
projectDirectoryPath: string
) => {
const filePath = window.electron.path.join(
projectDirectoryPath,
PROJECT_IMAGE_NAME
)
const data = atob(dataUrl.substring('data:image/png;base64,'.length))
const asArray = new Uint8Array(data.length)
for (let i = 0, len = data.length; i < len; ++i) {
asArray[i] = data.charCodeAt(i)
}
return window.electron.writeFile(filePath, asArray)
}

View File

@ -1,4 +1,4 @@
function takeScreenshotOfVideoStreamCanvas() {
export function takeScreenshotOfVideoStreamCanvas() {
const canvas = document.querySelector('[data-engine]')
const video = document.getElementById('video-stream')
if (