Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 4 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,6 @@
# Vite + React + Typescript + Electron - Starter
# Onlook - The first devtool for designer

![Vite + React + Typescript + Tailwind + Electron Starter](Screenshot.jpg)
![Preview screenshot](public/Screenshot.png)

> It is a simple starter template without unnecessary packages.

This very simple Starter template, utilizes [Vite](https://github.com/vitejs/vite), [Tailwind](https://tailwindcss.com/), [React](https://reactjs.org/), [Typescript](https://www.typescriptlang.org/) and [Electron](https://electronjs.org/).

By default, the React framework is used for the interface, but you can easily use any other framework such as Vue, Preact, Angular, Svelte or anything else.

> Vite is framework agnostic

## Installation

Clone this repo and install all dependencies
`yarn` or `npm install`

## Development

`yarn dev` or `npm run dev`

## Build

`yarn build` or `npm run build`

## Publish

`yarn dist` or `npm run dist`

## More advanced templates

If you are looking for more advanced templates than this, please go to one of the following links (these are some other links out of the **[awesome-vite](https://github.com/vitejs/awesome-vite)** repo) or you can help me make this template better 🙂

Links:
- [vite-react-electron](https://github.com/caoxiemeihao/vite-react-electron)
- [electron-vite-react](https://github.com/twstyled/electron-vite-react)
- [vite-electron-esbuild-starter](https://github.com/jctaoo/vite-electron-esbuild-starter)
## About
More coming soon [@Onlook](https://onlook.dev/)
Binary file removed Screenshot.jpg
Binary file not shown.
Binary file modified bun.lockb
Binary file not shown.
85 changes: 33 additions & 52 deletions electron/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Native
import { join } from 'path';

// Packages
import { BrowserWindow, IpcMainEvent, app, ipcMain, screen } from 'electron';
import isDev from 'electron-is-dev';
import { join } from 'path';
import { MainChannel } from '../src/lib/constants';

function loadWindowHtml(window: BrowserWindow) {
const port = process.env.PORT || 3000;
const url = isDev ? `http://localhost:${port}` : join(__dirname, '../src/out/index.html');
if (isDev) {
window?.loadURL(url);
} else {
window?.loadFile(url);
}
}

function createWindow() {
const { width, height } = screen.getPrimaryDisplay().workAreaSize;

// Create the browser window.
const window = new BrowserWindow({
width,
height,
Expand All @@ -22,59 +28,34 @@ function createWindow() {
}
});

const port = process.env.PORT || 3000;
const url = isDev ? `http://localhost:${port}` : join(__dirname, '../src/out/index.html');
loadWindowHtml(window);

// and load the index.html of the app.
if (isDev) {
window?.loadURL(url);
} else {
window?.loadFile(url);
}
if (isDev)
window.webContents.openDevTools();
}

window.webContents.openDevTools();
function initializeApp() {
app.whenReady().then(() => {
createWindow();

// For AppBar
ipcMain.on('minimize', () => {
// eslint-disable-next-line no-unused-expressions
window.isMinimized() ? window.restore() : window.minimize();
// or alternatively: win.isVisible() ? win.hide() : win.show()
});
ipcMain.on('maximize', () => {
// eslint-disable-next-line no-unused-expressions
window.isMaximized() ? window.restore() : window.maximize();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});

ipcMain.on('close', () => {
window.close();
app.on('window-all-closed', () => {
const macOS = process.platform === 'darwin';
if (macOS) app.quit();
});
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow();

app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
// TODO: Move this to new module
function setMessageListeners() {
ipcMain.on(MainChannel.NOTIFICATION, (event: IpcMainEvent, message: any) => {
console.log(message);
setTimeout(() => event.sender.send('message', 'hi from electron'), 500);
});
});

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
}

// listen the channel `message` and resend the received message to the renderer process
ipcMain.on('message', (event: IpcMainEvent, message: any) => {
console.log(message);
setTimeout(() => event.sender.send('message', 'hi from electron'), 500);
});
initializeApp();
setMessageListeners();
79 changes: 46 additions & 33 deletions electron/preload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ipcRenderer, contextBridge } from 'electron';
import { IpcRendererEvent, contextBridge, ipcRenderer } from 'electron';
import { MainChannel } from '../src/lib/constants';

declare global {
interface Window {
Expand All @@ -7,39 +8,51 @@ declare global {
}
}

const api = {
/**
* Here you can expose functions to the renderer process
* so they can interact with the main (electron) side
* without security problems.
*
* The function below can accessed using `window.Main.sayHello`
*/
sendMessage: (message: string) => {
ipcRenderer.send('message', message);
},
/**
Here function for AppBar
*/
Minimize: () => {
ipcRenderer.send('minimize');
},
Maximize: () => {
ipcRenderer.send('maximize');
},
Close: () => {
ipcRenderer.send('close');
},
/**
* Provide an easier way to listen to events
*/
on: (channel: string, callback: (data: any) => void) => {
ipcRenderer.on(channel, (_, data) => callback(data));
const store = {
get(val: any) {
return ipcRenderer.sendSync('electron-store-get', val);
},
set(property: string, val: any) {
ipcRenderer.send('electron-store-set', property, val);
},
has(val: any) {
return ipcRenderer.sendSync('electron-store-has', val);
}
}

const api = {
store: store,

sendMessage<T>(channel: MainChannel, args: T[]) {
ipcRenderer.send(channel, args);
},

on<T>(channel: MainChannel, func: (...args: T[]) => void) {
const subscription = (_event: IpcRendererEvent, ...args: T[]) =>
func(...args);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
},

once<T>(channel: MainChannel, func: (...args: T[]) => void) {
ipcRenderer.once(channel, (_event, ...args) => func(...args));
},

invoke<T, P>(channel: MainChannel, ...args: T[]): Promise<P> {
return ipcRenderer.invoke(channel, ...args);
},

removeListener<T>(channel: MainChannel, listener: (...args: T[]) => void) {
ipcRenderer.removeListener(channel, listener as (event: Electron.IpcRendererEvent, ...args: any[]) => void);
},

removeAllListeners(channel: MainChannel) {
ipcRenderer.removeAllListeners(channel);
},
};

// Expose methods to renderer process
contextBridge.exposeInMainWorld('Main', api);
/**
* Using the ipcRenderer directly in the browser through the contextBridge ist not really secure.
* I advise using the Main/api way !!
*/

// WARN: Using the ipcRenderer directly in the browser through the contextBridge is insecure
contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer);
71 changes: 71 additions & 0 deletions electron/webview-preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ipcRenderer } from 'electron';

const documentBodyInit = () => {
// Browser Sync
const bsScript = window.document.createElement('script');
bsScript.src =
'https://localhost:12719/browser-sync/browser-sync-client.js?v=2.27.10';
bsScript.async = true;
window.document.body.appendChild(bsScript);

// Context Menu
window.addEventListener('contextmenu', (e) => {
e.preventDefault();
ipcRenderer.send('show-context-menu', {
contextMenuMeta: { x: e.x, y: e.y },
});
});

window.addEventListener('wheel', (e) => {
ipcRenderer.sendToHost('pass-scroll-data', {
coordinates: { x: e.deltaX, y: e.deltaY },
innerHeight: document.body.scrollHeight,
innerWidth: window.innerWidth,
});
});

window.addEventListener('dom-ready', () => {
const { body } = document;
const html = document.documentElement;

const height = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);

ipcRenderer.sendToHost('pass-scroll-data', {
coordinates: { x: 0, y: 0 },
innerHeight: height,
innerWidth: window.innerWidth,
});
});
};

ipcRenderer.on('context-menu-command', (_, command) => {
ipcRenderer.sendToHost('context-menu-command', command);
});

const documentBodyWaitHandle = setInterval(() => {
window.onerror = function logError(errorMsg, url, lineNumber) {
// eslint-disable-next-line no-console
console.log(`Unhandled error: ${errorMsg} ${url} ${lineNumber}`);
// Code to run when an error has occurred on the page
};

if (window?.document?.body) {
clearInterval(documentBodyWaitHandle);
try {
documentBodyInit();
} catch (err) {
// eslint-disable-next-line no-console
console.log('Error in documentBodyInit:', err);
}

return;
}
// eslint-disable-next-line no-console
console.log('document.body not ready');
}, 300);
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"name": "@onlook/browser",
"version": "0.0.1",
"main": "main/index.js",
"main": "main/electron/index.js",
"description": "The first-ever devtool for designers",
"license": "Apache-2.0",
"description": "",
"author": {
"name": "Onlook",
"email": "contact@onlook.dev"
},
"keywords": [
"vite",
"react",
Expand All @@ -21,8 +25,8 @@
"build": "npm run build:vite && npm run build:electron",
"build:vite": "vite build",
"build:electron": "tsc -p electron",
"dist": "npm run build && electron-builder",
"pack": "npm run build && electron-builder --dir",
"dist": "npm run build && electron-builder",
"clean": "rimraf dist main src/out",
"type-check": "tsc",
"lint": "eslint . --ext js,jsx,ts,tsx",
Expand Down Expand Up @@ -73,6 +77,10 @@
],
"directories": {
"buildResources": "resources"
},
"mac": {
"category": "public.app-category.developer-tools",
"icon": "public/AppIcon.icns"
}
}
}
Binary file added public/AppIcon.icns
Binary file not shown.
Binary file added public/Screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 1 addition & 32 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import AppBar from './AppBar';
import { ThemeProvider } from './components/theme-provider';
import ProjectEditor from './routes/editor';

function App() {
// console.log(window.ipcRenderer);

const [isOpen, setOpen] = useState(false);
const [isSent, setSent] = useState(false);
const [fromMain, setFromMain] = useState<string | null>(null);

const handleToggle = () => {
if (isOpen) {
setOpen(false);
setSent(false);
} else {
setOpen(true);
setFromMain(null);
}
};
const sendMessageToElectron = () => {
if (window.Main) {
window.Main.sendMessage("Hello I'm from React World");
} else {
setFromMain('You are in a Browser, so no Electron functions are available');
}
setSent(true);
};

useEffect(() => {
if (isSent && window.Main)
window.Main.on('message', (message: string) => {
setFromMain(message);
});
}, [fromMain, isSent]);

return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<div className="flex flex-col h-screen w-screen bg-black">
Expand Down
Loading