From fe192b5ccb9f461827a0be53093bd1ec11b7858e Mon Sep 17 00:00:00 2001 From: Jordan Muir Date: Thu, 14 Sep 2023 10:00:45 -0400 Subject: [PATCH] workspace overlays --- app/workspace/Spaces/Command/index.js | 41 +++-- app/workspaceDock/Dock/index.js | 194 ++++++++++++++++++++++++ app/workspaceDock/index.dev.html | 23 +++ app/workspaceDock/index.html | 24 +++ app/workspaceDock/index.js | 44 ++++++ main/windows/frames/frameInstances.ts | 14 ++ main/windows/frames/index.ts | 46 +++++- main/windows/frames/overlayInstances.ts | 83 ++++++++++ main/windows/frames/viewInstances.ts | 8 +- main/windows/window.ts | 26 ++++ package.json | 7 +- scripts/bundler.mjs | 1 + 12 files changed, 479 insertions(+), 32 deletions(-) create mode 100644 app/workspaceDock/Dock/index.js create mode 100644 app/workspaceDock/index.dev.html create mode 100644 app/workspaceDock/index.html create mode 100644 app/workspaceDock/index.js create mode 100644 main/windows/frames/overlayInstances.ts diff --git a/app/workspace/Spaces/Command/index.js b/app/workspace/Spaces/Command/index.js index 5d0e27fee..28dd7e83b 100644 --- a/app/workspace/Spaces/Command/index.js +++ b/app/workspace/Spaces/Command/index.js @@ -5,8 +5,6 @@ import React from 'react' import { Cluster, ClusterRow, ClusterValue } from '../../../../resources/Components/Cluster' -import Dock from './Dock' - const Container = styled.div` position: relative; display: flex; @@ -160,27 +158,24 @@ const AssetList = styled.div` ` const Home = ({ data }) => ( - <> - - {data.station === 'command' ? ( - - ) : data.station === 'dashboard' ? ( - <> - - Value - Assets - Inventory - - -
{'Account activity'}
-
- - ) : ( -
{JSON.stringify(data, null, 4)}
- )} -
- - + + {data.station === 'command' ? ( + + ) : data.station === 'dashboard' ? ( + <> + + Value + Assets + Inventory + + +
{'Account activity'}
+
+ + ) : ( +
{JSON.stringify(data, null, 4)}
+ )} +
) export default Home diff --git a/app/workspaceDock/Dock/index.js b/app/workspaceDock/Dock/index.js new file mode 100644 index 000000000..cc85d6485 --- /dev/null +++ b/app/workspaceDock/Dock/index.js @@ -0,0 +1,194 @@ +import svg from '../../../resources/svg' +import styled, { createGlobalStyle } from 'styled-components' +import link from '../../../resources/link' +import useStore from '../../../resources/Hooks/useStore.js' +import { useState, useEffect } from 'react' + +const GlobalStyle = createGlobalStyle` + body { + width: 100%; + height: 100%; + margin: 0; + padding: 0px; + font-family: 'MainFont'; + font-weight: 400; + color: var(--outerspace); + } +` + +const Dock = styled.div` + border-radius: 16px; + background: var(--ghostA); + display: flex; + justify-content: center; + align-items: center; + padding: 12px 0px 12px 12px; +` + +const DappRow = styled.div` + display: flex; + align-items: center; + justify-content: center; +` + +const DappIcon = styled.div` + width: 42px; + height: 42px; + margin-right: 12px; + /* margin: 0px 2px 2px 12px; */ + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background: var(--ghostB); + border-radius: 8px; + + * { + pointer-events: none; + } + + &:hover { + background: var(--ghostC); + transform: scale(1.2); + transition: transform 0.2s ease-in-out; + } +` + +const DappIconBreak = styled.div` + width: 3px; + height: 42px; + background: var(--ghostZ); + border-radius: 1.5px; + margin: 0px 12px 0px 8px; +` +const DockWrap = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + transform: ${({ hide }) => (hide ? 'translateY(100px)' : 'translateY(0)')}; + transition: transform 0.4s ease-in-out; +` + +const Wrap = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; +` + +const DockHandle = styled.div` + position: absolute; + left: 50%; + bottom: 2px; + height: 4px; + width: 32px; + margin-left: -16px; + border-radius: 2px; + background: var(--outerspace); + display: none; +` + +const RoundedElement = styled.div` + position: absolute; + left: 8px; + bottom: 8px; + + width: 5px; + height: 5px; + background-color: var(--ghostAZ); /* Your actual background color */ + + /* The mask */ + -webkit-mask-box-image: radial-gradient(circle at top right, transparent 0% 5px, black 5px); + mask-box-image: radial-gradient(circle at top right, transparent 0% 5px, black 5px); +` + +const RoundedElementRight = styled.div` + position: absolute; + right: 8px; + bottom: 8px; + + width: 5px; + height: 5px; + background-color: var(--ghostAZ); /* Your actual background color */ + + /* The mask */ + -webkit-mask-box-image: radial-gradient(circle at top left, transparent 0% 5px, black 5px); + mask-box-image: radial-gradient(circle at top left, transparent 0% 5px, black 5px); +` +let hideTimeout +export default () => { + const frameState = useStore('windows.workspaces', window.frameId) + const nav = frameState?.nav[0] || { space: 'command', data: {} } + if (!nav || !nav.space) return null + + const [hideDockWrap, setHideDockWrap] = useState(false) + + const setHide = () => { + setHideDockWrap(true) + clearTimeout(hideTimeout) + hideTimeout = setTimeout(() => { + link.send('workspace:nav:update:data', window.frameId, { hidden: true }) + }, 500) + } + + const setShow = () => { + clearTimeout(hideTimeout) + setHideDockWrap(false) + link.send('workspace:nav:update:data', window.frameId, { hidden: false }) + } + + const hidden = (nav.space === 'dapp' && hideDockWrap) || (nav.space !== 'dapp' && nav.space !== 'command') + + return ( + setShow()}> + + + + + { + setHide() + }} + > + + { + link.send('workspace:nav', window.frameId, 'command', { station: 'command' }) + }} + > + {'C'} + + { + link.send('workspace:nav', window.frameId, 'command', { station: 'dashboard' }) + }} + > + {'D'} + + + { + // link.send('workspace:nav:update:data', window.frameId, { station: 'dapp' }) + link.send('workspace:run', 'dapp', {}, ['send.frame.eth']) + }} + > + {svg.send(15)} + + {'-'} + {'-'} + {'-'} + {'-'} + {'-'} + + + + ) +} diff --git a/app/workspaceDock/index.dev.html b/app/workspaceDock/index.dev.html new file mode 100644 index 000000000..87bbdf775 --- /dev/null +++ b/app/workspaceDock/index.dev.html @@ -0,0 +1,23 @@ + + + + + Dock + + + +
+ + + diff --git a/app/workspaceDock/index.html b/app/workspaceDock/index.html new file mode 100644 index 000000000..03a1ba8bb --- /dev/null +++ b/app/workspaceDock/index.html @@ -0,0 +1,24 @@ + + + + + Dock + + + +
+ + + diff --git a/app/workspaceDock/index.js b/app/workspaceDock/index.js new file mode 100644 index 000000000..0bc778bb7 --- /dev/null +++ b/app/workspaceDock/index.js @@ -0,0 +1,44 @@ +import * as Sentry from '@sentry/electron' +import { createRoot } from 'react-dom/client' + +import link from '../../resources/link' +import restore from '../store' + +import { StoreContext } from '../../resources/Hooks/useStore' + +import Dock from './Dock' + +Sentry.init({ dsn: 'https://7b09a85b26924609bef5882387e2c4dc@o1204372.ingest.sentry.io/6331069' }) + +document.addEventListener('dragover', (e) => e.preventDefault()) +document.addEventListener('drop', (e) => e.preventDefault()) + +if (process.env.NODE_ENV !== 'development') { + window.eval = global.eval = () => { + throw new Error(`This app does not support window.eval()`) + } // eslint-disable-line +} + +link.rpc('getFrameId', (err, frameId) => { + if (err) return console.error('Could not get frameId from main', err) + window.frameId = frameId + link.rpc('getState', (err, state) => { + if (err) return console.error('Could not get initial state from main') + const store = restore(state) + store.observer(() => { + document.body.classList.remove('dark', 'light') + document.body.classList.add('clip', store('main.colorway')) + setTimeout(() => { + document.body.classList.remove('clip') + }, 100) + }) + const root = createRoot(document.getElementById('dock')) + root.render( + + + + ) + }) +}) + +document.addEventListener('contextmenu', (e) => link.send('*:contextmenu', e.clientX, e.clientY)) diff --git a/main/windows/frames/frameInstances.ts b/main/windows/frames/frameInstances.ts index 32c33dc72..da1df231f 100644 --- a/main/windows/frames/frameInstances.ts +++ b/main/windows/frames/frameInstances.ts @@ -12,6 +12,8 @@ const isDev = process.env.NODE_ENV === 'development' export interface FrameInstance extends BrowserWindow { frameId?: string views?: Record + overlays?: Record + overlay?: BrowserView showingView?: string } @@ -77,6 +79,18 @@ export default { // }) frameInstance.on('resize', () => { + // TODO: reflect correct state of dock + if (frameInstance.overlay) { + const { width, height } = frameInstance.getBounds() + + frameInstance.overlay.setBounds({ + y: height - 96, + x: 0, + width: width, + height: 96 + }) + } + Object.values(frameInstance.views || {}).forEach((viewInstance) => { const { frameId } = frameInstance // const { fullscreen } = store('windows.workspaces', frameId) diff --git a/main/windows/frames/index.ts b/main/windows/frames/index.ts index e1397cdef..ad89754e0 100644 --- a/main/windows/frames/index.ts +++ b/main/windows/frames/index.ts @@ -12,6 +12,7 @@ import store from '../../store' import frameInstances, { FrameInstance } from './frameInstances.js' import viewInstances from './viewInstances' +import overlayInstances from './overlayInstances' import { Workspace, Nav, View } from '../workspace/types' @@ -46,12 +47,47 @@ export default class WorkspaceManager { const frames = getFrames() this.manageFrames(frames, inFocus) this.manageViews(frames) - // this.manageOverlays(frames) + this.manageOverlays(frames) }) } - manageOverlays(frames: Record, inFocus: string) { - // If there is an overlay, create a frame instance for it + manageOverlays(frames: Record) { + const frameIds = Object.keys(frames) + + frameIds.forEach((frameId) => { + const frameInstance = this.frameInstances[frameId] + if (!frameInstance) return log.error('Instance not found when managing views') + + // Frame definition in the state + const frame = frames[frameId] + + // Current Nav + const currentNav = frame?.nav[0] + + if (!frameInstance.overlay) { + frameInstance.overlay = overlayInstances.create(frameInstance) + } + const { width, height } = frameInstance.getBounds() + if (currentNav?.space === 'dapp' && currentNav?.data.hidden === true) { + frameInstance.overlay.setBounds({ + y: height - 13, + x: 0, + width: width, + height: 13 + }) + } else { + frameInstance.overlay.setBounds({ + y: height - 96, + x: 0, + width: width, + height: 96 + }) + } + + // We could track this on the instance to add it only when necessary + frameInstance.addBrowserView(frameInstance.overlay) + frameInstance.setTopBrowserView(frameInstance.overlay) + }) } manageFrames(frames: Record, inFocus: string) { @@ -175,7 +211,7 @@ export default class WorkspaceManager { if (!viewInstance) return log.error('View instance not found when managing views') // Get view stats - const viewMeta = { ready: true } //TODO: store('workspacesMeta', frame.id, 'views', view.id) + const viewMeta = { ready: true } // TODO: store('workspacesMeta', frame.id, 'views', view.id) // Show all in the current nav if (viewMeta.ready && currentNavViewIds.includes(view.id)) { frameInstance.addBrowserView(viewInstance) @@ -223,6 +259,8 @@ export default class WorkspaceManager { if (frameInstance && !frameInstance.isDestroyed()) { const webContents = frameInstance.webContents if (webContents) webContents.send(channel, ...args) + const overlayWebContents = frameInstance.overlay?.webContents + if (overlayWebContents) overlayWebContents.send(channel, ...args) } else { log.error( new Error( diff --git a/main/windows/frames/overlayInstances.ts b/main/windows/frames/overlayInstances.ts new file mode 100644 index 000000000..6c9ccfe9a --- /dev/null +++ b/main/windows/frames/overlayInstances.ts @@ -0,0 +1,83 @@ +import { URL } from 'url' +import log from 'electron-log' + +import { FrameInstance } from './frameInstances' +import store from '../../store' +import server from '../../dapps/server' +import { createViewInstance, createOverlayInstance } from '../window' + +import type { Nav, View } from '../workspace/types' + +const isDev = process.env.NODE_ENV === 'development' + +export default { + hide: (frameInstance: FrameInstance) => { + const overlayInstance = frameInstance.overlay + if (frameInstance && !frameInstance.isDestroyed() && overlayInstance) + frameInstance.removeBrowserView(overlayInstance) + }, + show: (frameInstance: FrameInstance) => { + const overlayInstance = frameInstance.overlay + if (frameInstance && !frameInstance.isDestroyed() && overlayInstance) { + frameInstance.addBrowserView(overlayInstance) + frameInstance.setTopBrowserView(overlayInstance) + } + }, + // Create a view instance on a frame + create: (frameInstance: FrameInstance) => { + const overlayInstance = createOverlayInstance() + frameInstance.addBrowserView(overlayInstance) + overlayInstance.webContents.setVisualZoomLevelLimits(1, 1) + + overlayInstance.webContents.loadURL( + isDev + ? 'http://localhost:1234/workspaceDock/index.dev.html' + : `file://${process.env.BUNDLE_LOCATION}/workspaceDock.html` + ) + + const { width, height } = frameInstance.getBounds() + overlayInstance.setBounds({ + y: height - 72, + x: 0, + width: width, + height: 72 + }) + + return overlayInstance + + // frameInstance.removeBrowserView(viewInstance) + // viewInstance.webContents.openDevTools({ mode: 'detach' }) + + // Keep reference to view on frame instance + // frameInstance.overlays = { ...(frameInstance.overlays || {}), [overlay.id]: overlayInstance } + }, + // Destroy a view instance on a frame + destroy: (frameInstance: FrameInstance, viewId: string) => { + const views = frameInstance.views || {} + const { frameId } = frameInstance + + if (frameInstance && !frameInstance.isDestroyed()) frameInstance.removeBrowserView(views[viewId]) + + const webcontents = views[viewId].webContents as any + webcontents.destroy() + + delete views[viewId] + }, + position: (frameInstance: FrameInstance, viewId: string) => { + const { frameId } = frameInstance + const { fullscreen } = store('windows.workspaces', frameId) + const viewInstance = (frameInstance.views || {})[viewId] + + if (viewInstance) { + const { width, height } = frameInstance.getBounds() + viewInstance.setBounds({ + x: 0, + y: 64, + width: width, + height: height - 96 + }) + // viewInstance.setBounds({ x: 73, y: 16, width: width - 73, height: height - 16 }) + // viewInstance.setAutoResize({ width: true, height: true }) + } + } +} diff --git a/main/windows/frames/viewInstances.ts b/main/windows/frames/viewInstances.ts index f1295a87e..6c762939a 100644 --- a/main/windows/frames/viewInstances.ts +++ b/main/windows/frames/viewInstances.ts @@ -130,10 +130,10 @@ export default { // height: fullscreen ? height : height - 32 // }) viewInstance.setBounds({ - x: 0, - y: 64, - width: width, - height: height - 96 + x: 8, + y: 8, + width: width - 16, + height: height - 16 }) // viewInstance.setBounds({ x: 73, y: 16, width: width - 73, height: height - 16 }) // viewInstance.setAutoResize({ width: true, height: true }) diff --git a/main/windows/window.ts b/main/windows/window.ts index 4a2c4163b..dd9533ef1 100644 --- a/main/windows/window.ts +++ b/main/windows/window.ts @@ -90,6 +90,32 @@ export function createViewInstance( return viewInstance } +export function createOverlayInstance( + url = '', + webPreferences: BrowserWindowConstructorOptions['webPreferences'] = {} +) { + const overlayInstance = new BrowserView({ + webPreferences: { + ...webPreferences, + contextIsolation: true, + webviewTag: false, + sandbox: true, + defaultEncoding: 'utf-8', + nodeIntegration: false, + scrollBounce: true, + navigateOnDragDrop: false, + disableBlinkFeatures: 'Auxclick', + preload: path.resolve(process.env.BUNDLE_LOCATION, 'bridge.js') + } + }) + + overlayInstance.webContents.on('will-navigate', (e) => e.preventDefault()) + overlayInstance.webContents.on('will-attach-webview', (e) => e.preventDefault()) + overlayInstance.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) + + return overlayInstance +} + const externalWhitelist = [ 'https://frame.sh', 'https://chrome.google.com/webstore/detail/frame-alpha/ldcoohedfbjoobcadoglnnmmfbdlmmhf', diff --git a/package.json b/package.json index bef6b1652..211a0a762 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "setup:scripts": "allow-scripts", "setup": "npm run setup:deps && husky install", "start": "npm run launch", - "bundle": "npm run bundle:bridge && npm run bundle:tray && npm run bundle:workspace && npm run bundle:onboard && npm run bundle:notify && npm run bundle:workspace:inject", + "bundle": "npm run bundle:bridge && npm run bundle:tray && npm run bundle:workspace && npm run bundle:workspaceDock && npm run bundle:onboard && npm run bundle:notify && npm run bundle:workspace:inject", "bundle:bridge": "parcel build resources/bridge/index.js --target bridge --public-url .", "bundle:tray": "parcel build app/tray/index.html --target tray --public-url .", "bundle:workspace": "parcel build app/workspace/index.html --target workspace --public-url .", + "bundle:workspaceDock": "parcel build app/workspaceDock/index.html --target workspaceDock --public-url .", "bundle:onboard": "parcel build app/onboard/index.html --target onboard --public-url .", "bundle:notify": "parcel build app/notify/index.html --target notify --public-url .", "bundle:workspace:inject": "parcel build main/dapps/server/inject/index.js --target inject --public-url .", @@ -209,6 +210,7 @@ "inject": "./bundle/inject.js", "tray": "./bundle/tray.html", "workspace": "./bundle/workspace.html", + "workspaceDock": "./bundle/workspaceDock.html", "onboard": "./bundle/onboard.html", "notify": "./bundle/notify.html", "targets": { @@ -223,6 +225,9 @@ "workspace": { "context": "browser" }, + "workspaceDock": { + "context": "browser" + }, "onboard": { "context": "browser" }, diff --git a/scripts/bundler.mjs b/scripts/bundler.mjs index b9ba488df..c550b6626 100644 --- a/scripts/bundler.mjs +++ b/scripts/bundler.mjs @@ -10,6 +10,7 @@ const bundler = new Parcel({ entries: [ 'app/tray/index.dev.html', 'app/workspace/index.dev.html', + 'app/workspaceDock/index.dev.html', 'app/onboard/index.dev.html', 'app/notify/index.dev.html' ],