diff --git a/src/constants.ts b/src/constants.ts index 000a14345..6ddb1fc3b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,6 @@ export const MAIN_MIN_WIDTH = 900; export const MAIN_MIN_HEIGHT = 600; +export const SCREENSHOT_WIDTH = 1040; +export const SCREENSHOT_HEIGHT = 1248; export const LIMIT_OF_ZIP_SITES_PER_USER = 10; export const AUTO_UPDATE_INTERVAL_MS = 60 * 60 * 1000; diff --git a/src/screenshot-window.ts b/src/screenshot-window.ts new file mode 100644 index 000000000..075a4fd4d --- /dev/null +++ b/src/screenshot-window.ts @@ -0,0 +1,28 @@ +import crypto from 'crypto'; +import { BrowserWindow, session } from 'electron'; +import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH } from './constants'; + +export function createScreenshotWindow( captureUrl: string ) { + const newSession = session.fromPartition( crypto.randomUUID() ); + + const window = new BrowserWindow( { + height: SCREENSHOT_HEIGHT, + width: SCREENSHOT_WIDTH, + show: false, + webPreferences: { session: newSession }, + } ); + + const finishedLoading = new Promise< void >( ( resolve ) => { + window.webContents.on( 'did-finish-load', () => resolve() ); + } ); + + window.loadURL( captureUrl ); + + const waitForCapture = async () => { + await finishedLoading; + await new Promise( ( resolve ) => setTimeout( resolve, 500 ) ); + return window.webContents.capturePage(); + }; + + return { window, waitForCapture }; +} diff --git a/src/site-server.ts b/src/site-server.ts index 021d7dfcc..9b64889fd 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -1,8 +1,12 @@ import { app } from 'electron'; +import fs from 'fs/promises'; import nodePath from 'path'; +import * as Sentry from '@sentry/electron/main'; import { getWpNowConfig, startServer, type WPNowServer } from '../vendor/wp-now/src'; import { pathExists, recursiveCopyDirectory, isEmptyDir } from './lib/fs-utils'; import { portFinder } from './lib/port-finder'; +import { createScreenshotWindow } from './screenshot-window'; +import { getSiteThumbnailPath } from './storage/paths'; const servers = new Map< string, SiteServer >(); @@ -55,6 +59,8 @@ export class SiteServer { } async delete() { + const thumbnailPath = getSiteThumbnailPath( this.details.id ); + await fs.unlink( thumbnailPath ); await this.stop(); servers.delete( this.details.id ); } @@ -85,6 +91,8 @@ export class SiteServer { port: this.server.options.port, running: true, }; + + await this.updateCachedThumbnail(); } updateSiteDetails( site: SiteDetails ) { @@ -107,4 +115,24 @@ export class SiteServer { const { running, url, ...rest } = this.details; this.details = { running: false, ...rest }; } + + async updateCachedThumbnail() { + if ( ! this.details.running ) { + throw new Error( 'Cannot update thumbnail for a stopped server' ); + } + + const captureUrl = new URL( '/?studio-hide-adminbar', this.details.url ); + const { window, waitForCapture } = createScreenshotWindow( captureUrl.href ); + + const outPath = getSiteThumbnailPath( this.details.id ); + const outDir = nodePath.dirname( outPath ); + + // Continue taking the screenshot asynchronously so we don't prevent the + // UI from showing the server is now available. + fs.mkdir( outDir, { recursive: true } ) + .then( waitForCapture ) + .then( ( image ) => fs.writeFile( outPath, image.toPNG() ) ) + .catch( Sentry.captureException ) + .finally( () => window.destroy() ); + } } diff --git a/src/storage/paths.ts b/src/storage/paths.ts index a78f9ccfd..c78d1fb32 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -12,3 +12,8 @@ export function getServerFilesPath(): string { } export const DEFAULT_SITE_PATH = path.join( app?.getPath( 'home' ) || '', 'Studio' ); + +export function getSiteThumbnailPath( siteId: string ): string { + const appDataPath = app.getPath( 'appData' ); + return path.join( appDataPath, app.getName(), 'thumbnails', `${ siteId }.png` ); +} diff --git a/vendor/wp-now/src/download.ts b/vendor/wp-now/src/download.ts index ae78ac703..24268a34d 100644 --- a/vendor/wp-now/src/download.ts +++ b/vendor/wp-now/src/download.ts @@ -232,6 +232,16 @@ export async function downloadMuPlugins(customMuPluginsPath = '') { }` ); + fs.writeFile( + path.join(muPluginsPath, '0-thumbnails.php'), + `