From 417875d4eada35ace5606f3fa5785b6c880343a8 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 7 Aug 2023 07:26:49 -0700 Subject: [PATCH] feat: add splash screen on top of ctx refactor (#2548) * chore: update ctx refactor * fix: tests * feat: add context.spec.js tests * fix: startup issues * feat: app context supports lazy functions * chore: self-PR-review cleanup * test: fix e2e tests * feat: add splash screen * tmp: trying to fix e2e tests * chore(deps): upgrade playwright * fix: revert daemonReady function * Update src/splash/splash.html * Update src/splash/splash.html * Update src/splash/splash.html * Update src/webui/index.js * Update test/e2e/launch.e2e.test.js * Update test/e2e/launch.e2e.test.js * Update test/e2e/launch.e2e.test.js * Update test/e2e/launch.e2e.test.js * Update test/e2e/launch.e2e.test.js * fix: do not prompt for new port when using CI * fix: handle errors that were breaking e2e tests * feat: splashscreen page background is transparent * make keyframe animation dir normal * remove visual artifacts of drop-shadow on size reduction * make drop-shadow animation better * feat: splash screen animation polish * chore: removed webkit keyframes definition * chore: remove debugging code * chore: remove extra line Co-authored-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> * chore(splash-screen): repositioning awaits Co-authored-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> * chore: fix after pr comment change * chore: move splash screen css to separate file --------- Co-authored-by: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> --- assets/pages/splash.css | 67 ++++++++++++++++++++++++++++++ assets/pages/splash.html | 12 ++++++ src/context.js | 10 ++++- src/daemon/config.js | 7 +++- src/index.js | 2 + src/splash/create-splash-screen.js | 35 ++++++++++++++++ src/types.d.ts | 1 + src/webui/index.js | 59 ++++++++++++++++++++++---- test/e2e/launch.e2e.test.js | 11 +++++ 9 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 assets/pages/splash.css create mode 100644 assets/pages/splash.html create mode 100644 src/splash/create-splash-screen.js create mode 100644 src/types.d.ts diff --git a/assets/pages/splash.css b/assets/pages/splash.css new file mode 100644 index 000000000..2a6289383 --- /dev/null +++ b/assets/pages/splash.css @@ -0,0 +1,67 @@ +body { + border-radius: 5vh; +} + +.loader { + background-image: url('../../assets/icons/tray/others/on-large.png'); + width: 200px; + height: 200px; + -webkit-animation: heartbeat 1.5s ease-in-out 0s infinite normal both; + animation: heartbeat 1.5s ease-in-out 0s infinite normal both; + -webkit-transform-origin: center center; + transform-origin: center center; + margin: auto; + left: 0; + right: 0; + top: 0px; + bottom: 0; + position: fixed; + background-size: cover; + filter: drop-shadow(0 0 0 #378085); +} + +@keyframes heartbeat { + 0% { + animation-timing-function: ease-out; + transform: scale(1); + transform-origin: center center; + filter: drop-shadow(0 0 0 #378085); + } + + 45% { + animation-timing-function: ease-in; + transform: scale(1); + filter: drop-shadow(0 0 0rem #378085); + } + + 50% { + animation-timing-function: ease-in; + transform: scale(1); + filter: drop-shadow(0 0 0.5rem #378085); + } + + 55% { + animation-timing-function: ease-out; + transform: scale(1); + /* Size should be increased on drop-shadow so when scaling down, drop-shadow compensates for size reduction, and we don't get any visual artifacts */ + filter: drop-shadow(0 0 1rem #378085); + } + + 67% { + animation-timing-function: ease-in; + transform: scale(1.13); + filter: drop-shadow(0 0 0.75rem #378085); + } + + 83% { + animation-timing-function: ease-out; + transform: scale(1.02); + filter: drop-shadow(0 0 0.50rem #378085); + } + + 90% { + animation-timing-function: ease-in; + transform: scale(1.09); + filter: drop-shadow(0 0 0.75rem #378085); + } +} diff --git a/assets/pages/splash.html b/assets/pages/splash.html new file mode 100644 index 000000000..938c951c9 --- /dev/null +++ b/assets/pages/splash.html @@ -0,0 +1,12 @@ + + +
+ + + + + + + + + diff --git a/src/context.js b/src/context.js index 678694498..e4ca42b54 100644 --- a/src/context.js +++ b/src/context.js @@ -3,7 +3,7 @@ const pDefer = require('p-defer') const logger = require('./common/logger') /** - * @typedef {'tray-menu' | 'tray' | 'tray-menu-state' | 'tray.update-menu' | 'countlyDeviceId' | 'manualCheckForUpdates' | 'startIpfs' | 'stopIpfs' | 'restartIpfs' | 'getIpfsd' | 'launchWebUI' | 'webui'} ContextProperties + * @typedef {'tray-menu' | 'tray' | 'tray-menu-state' | 'tray.update-menu' | 'countlyDeviceId' | 'manualCheckForUpdates' | 'startIpfs' | 'stopIpfs' | 'restartIpfs' | 'getIpfsd' | 'launchWebUI' | 'webui' | 'splashScreen'} ContextProperties */ /** @@ -101,7 +101,13 @@ class Context { return async (...args) => { const originalFn = await originalFnPromise - return originalFn(...args) + try { + return await originalFn(...args) + } catch (err) { + logger.error(`[ctx] Error calling ${String(propertyName)}`) + logger.error(err) + throw err + } } } diff --git a/src/daemon/config.js b/src/daemon/config.js index 110d55d6b..9eb0c2d33 100644 --- a/src/daemon/config.js +++ b/src/daemon/config.js @@ -369,7 +369,12 @@ async function checkPorts (ipfsd) { } // two "0" in config mean "pick free ports without any prompt" - const promptUser = (apiPort !== 0 || gatewayPort !== 0) + let promptUser = (apiPort !== 0 || gatewayPort !== 0) + + if (process.env.NODE_ENV === 'test' || process.env.CI != null) { + logger.info('[daemon] CI or TEST mode, skipping busyPortDialog') + promptUser = false + } if (promptUser) { let useAlternativePorts = null diff --git a/src/index.js b/src/index.js index 26abc5a11..c4593ff8f 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,7 @@ const setupAnalytics = require('./analytics') const setupSecondInstance = require('./second-instance') const { analyticsKeys } = require('./analytics/keys') const handleError = require('./handleError') +const createSplashScreen = require('./splash/create-splash-screen') // Hide Dock if (app.dock) app.dock.hide() @@ -65,6 +66,7 @@ async function run () { try { await Promise.all([ + createSplashScreen(), setupDaemon(), // ctx.getIpfsd, startIpfs, stopIpfs, restartIpfs setupAnalytics(), // ctx.countlyDeviceId setupI18n(), diff --git a/src/splash/create-splash-screen.js b/src/splash/create-splash-screen.js new file mode 100644 index 000000000..7dca7efe7 --- /dev/null +++ b/src/splash/create-splash-screen.js @@ -0,0 +1,35 @@ +/** + * A splash screen for the application that is shown while the webui is loading. + * + * This is to prevent the user from seeing the `Could not connect to the IPFS API` error + * while we're still booting up the daemon. + */ +const { BrowserWindow } = require('electron') +const getCtx = require('../context') +const logger = require('../common/logger') +const path = require('node:path') + +module.exports = async function createSplashScreen () { + const ctx = getCtx() + const splashScreen = new BrowserWindow({ + title: 'IPFS Desktop splash screen', + width: 250, + height: 275, + transparent: true, + frame: false, + alwaysOnTop: true, + show: false + }) + + try { + await splashScreen.loadFile(path.join(__dirname, '../../assets/pages/splash.html')) + } catch (err) { + logger.error('[splashScreen] loadFile failed') + logger.error(err) + return + } + + splashScreen.center() + + ctx.setProp('splashScreen', splashScreen) +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 000000000..217a77092 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1 @@ +export type CONFIG_KEYS = 'AUTO_LAUNCH' | 'AUTO_GARBAGE_COLLECTOR' | 'SCREENSHOT_SHORTCUT' | 'OPEN_WEBUI_LAUNCH' | 'MONOCHROME_TRAY_ICON' | 'EXPERIMENT_PUBSUB' | 'EXPERIMENT_PUBSUB_NAMESYS' diff --git a/src/webui/index.js b/src/webui/index.js index 055950fac..cb5cbfdd6 100644 --- a/src/webui/index.js +++ b/src/webui/index.js @@ -18,9 +18,14 @@ const Countly = require('countly-sdk-nodejs') const { analyticsKeys } = require('../analytics/keys') const ipcMainEvents = require('../common/ipc-main-events') const getCtx = require('../context') +const { STATUS } = require('../daemon/consts') serve({ scheme: 'webui', directory: join(__dirname, '../../assets/webui') }) +/** + * + * @returns {BrowserWindow} + */ const createWindow = () => { logger.info('[webui] creating window') const dimensions = screen.getPrimaryDisplay() @@ -107,6 +112,7 @@ const createWindow = () => { }) app.on('before-quit', () => { + logger.info('[web ui] app-quit requested') // Makes sure the app quits even though we prevent // the closing of this window. window.removeAllListeners('close') @@ -143,6 +149,10 @@ module.exports = async function () { url.searchParams.set('deviceId', await ctx.getProp('countlyDeviceId')) ctx.setProp('launchWebUI', async (path, { focus = true, forceRefresh = false } = {}) => { + if (window.isDestroyed()) { + logger.error(`[web ui] window is destroyed, not launching web ui with ${path}`) + return + } if (forceRefresh) window.webContents.reload() if (!path) { logger.info('[web ui] launching web ui', { withAnalytics: analyticsKeys.FN_LAUNCH_WEB_UI }) @@ -165,8 +175,10 @@ module.exports = async function () { } const getIpfsd = ctx.getFn('getIpfsd') - ipcMain.on(ipcMainEvents.IPFSD, async () => { + let ipfsdStatus = null + ipcMain.on(ipcMainEvents.IPFSD, async (status) => { const ipfsd = await getIpfsd(true) + ipfsdStatus = status if (ipfsd && ipfsd.apiAddr !== apiAddress) { apiAddress = ipfsd.apiAddr @@ -183,18 +195,47 @@ module.exports = async function () { }) const launchWebUI = ctx.getFn('launchWebUI') + const splashScreen = await ctx.getProp('splashScreen') + if (store.get(CONFIG_KEY)) { + // we're supposed to show the window on startup, display the splash screen + splashScreen.show() + } else { + // we don't need the splash screen, ignore it. + splashScreen.destroy() + } + let splashScreenTimeoutId = null + window.on('close', () => { + if (splashScreenTimeoutId) { + clearTimeout(splashScreenTimeoutId) + splashScreenTimeoutId = null + } + }) + const handleSplashScreen = async () => { + if ([null, STATUS.STARTING_STARTED].includes(ipfsdStatus)) { + splashScreenTimeoutId = setTimeout(handleSplashScreen, 500) + return + } + + await launchWebUI('/') + try { + splashScreen.destroy() + } catch (err) { + logger.error('[web ui] failed to hide splash screen') + logger.error(err) + } + } return /** @type {Promise