Skip to content

Commit

Permalink
feature(electron): init
Browse files Browse the repository at this point in the history
  • Loading branch information
lcomplete committed Mar 27, 2023
1 parent 84f84f8 commit 3c3f755
Show file tree
Hide file tree
Showing 10 changed files with 2,805 additions and 0 deletions.
39 changes: 39 additions & 0 deletions app/electron/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/renderer/.next/
/renderer/out/

# production
/main
/dist

.idea/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel

db.sqlite*
36 changes: 36 additions & 0 deletions app/electron/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"private": true,
"main": "main/index.js",
"productName": "huntly",
"scripts": {
"clean": "rimraf dist main",
"dev": "npm run build-electron && electron .",
"build-electron": "tsc -p src",
"build": "npm run build-electron",
"pack-app": "npm run build && electron-builder --dir",
"dist": "npm run build && electron-builder",
"type-check": "tsc -p src/tsconfig.json",
"lint": "eslint ."
},
"dependencies": {
"app-root-path": "^3.1.0",
"electron-is-dev": "^1.2.0",
"electron-window-state": "^5.0.3"
},
"devDependencies": {
"@types/node": "^17.0.40",
"@typescript-eslint/eslint-plugin": "^5.27.1",
"@typescript-eslint/parser": "^5.27.1",
"electron": "^19.0.3",
"electron-builder": "^23.0.3",
"eslint": "^8.17.0",
"rimraf": "^3.0.0",
"typescript": "^4.7.3"
},
"build": {
"asar": true,
"files": [
"main"
]
}
}
135 changes: 135 additions & 0 deletions app/electron/src/eventHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { ipcMain, shell, dialog, app, session, clipboard } from "electron"
import { WindowManager } from "./window"

export function setIpcMainEventHandler(manager: WindowManager) {
async function openExternal(url: string, background = false) {
if (url.startsWith("https://") || url.startsWith("http://")) {
if (background && process.platform === "darwin") {
shell.openExternal(url, { activate: false })
} else if (background && manager.hasWindow()) {
manager.mainWindow.setAlwaysOnTop(true)
await shell.openExternal(url)
setTimeout(() => manager.mainWindow.setAlwaysOnTop(false), 1000)
} else {
shell.openExternal(url)
}
}
}

app.on("web-contents-created", (_, contents) => {
contents.setWindowOpenHandler(details => {
if (contents.getType() === "webview")
openExternal(
details.url,
details.disposition === "background-tab"
)
return {
action: manager.hasWindow() ? "deny" : "allow",
}
})
contents.on("will-navigate", (event, url) => {
event.preventDefault()
if (contents.getType() === "webview") openExternal(url)
})
})

ipcMain.on("get-version", event => {
event.returnValue = app.getVersion()
})

ipcMain.handle("open-external", (_, url: string, background: boolean) => {
openExternal(url, background)
})

ipcMain.handle("show-error-box", (_, title, content) => {
dialog.showErrorBox(title, content)
})

ipcMain.handle(
"show-message-box",
async (_, title, message, confirm, cancel, defaultCancel, type) => {
if (manager.hasWindow()) {
let response = await dialog.showMessageBox(manager.mainWindow, {
type: type,
title: title,
message: message,
buttons:
process.platform === "win32"
? ["Yes", "No"]
: [confirm, cancel],
cancelId: 1,
defaultId: defaultCancel ? 1 : 0,
})
return response.response === 0
} else {
return false
}
}
)


ipcMain.handle("get-cache", async () => {
return await session.defaultSession.getCacheSize()
})

ipcMain.handle("clear-cache", async () => {
await session.defaultSession.clearCache()
})

ipcMain.handle("write-clipboard", (_, text) => {
clipboard.writeText(text)
})

ipcMain.handle("close-window", () => {
if (manager.hasWindow()) manager.mainWindow.close()
})

ipcMain.handle("minimize-window", () => {
if (manager.hasWindow()) manager.mainWindow.minimize()
})

ipcMain.handle("maximize-window", () => {
manager.zoom()
})

ipcMain.on("is-maximized", event => {
event.returnValue =
Boolean(manager.mainWindow) && manager.mainWindow.isMaximized()
})

ipcMain.on("is-focused", event => {
event.returnValue =
manager.hasWindow() && manager.mainWindow.isFocused()
})

ipcMain.on("is-fullscreen", event => {
event.returnValue =
manager.hasWindow() && manager.mainWindow.isFullScreen()
})

ipcMain.handle("request-focus", () => {
if (manager.hasWindow()) {
const win = manager.mainWindow
if (win.isMinimized()) win.restore()
if (process.platform === "win32") {
win.setAlwaysOnTop(true)
win.setAlwaysOnTop(false)
}
win.focus()
}
})

ipcMain.handle("request-attention", () => {
if (manager.hasWindow() && !manager.mainWindow.isFocused()) {
if (process.platform === "win32") {
manager.mainWindow.flashFrame(true)
manager.mainWindow.once("focus", () => {
manager.mainWindow.flashFrame(false)
})
} else if (process.platform === "darwin") {
app.dock.bounce()
}
}
})

}
50 changes: 50 additions & 0 deletions app/electron/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Native

// Packages
import {app, BrowserWindow, ipcMain, IpcMainEvent} from 'electron'
import path, {join} from 'path';
import isDev from "electron-is-dev";

import {WindowManager} from "./window";
import {format} from 'url';


app.on("ready", async () => {
const mainWindow = new BrowserWindow({
width: 1600,
height: 900,
minWidth: 1600,
minHeight: 900,
titleBarStyle: 'hidden',
// titleBarOverlay: {height: 44},
trafficLightPosition:{x: 15, y: 15},
webPreferences: {
nodeIntegration: false,
// contextIsolation: false,
preload: join(__dirname, 'preload.js'),
},
})

const windowManager = new WindowManager(mainWindow);
windowManager.init();

const url = isDev
? 'http://localhost:3000/'
: format({
pathname: join(__dirname, '../renderer/out/index.html'),
protocol: 'file:',
slashes: true,
})


mainWindow.loadURL(url)
})

// Quit the app once all windows are closed
app.on('window-all-closed', app.quit)

// 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)
})
16 changes: 16 additions & 0 deletions app/electron/src/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-namespace */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ipcRenderer, IpcRenderer, contextBridge } from 'electron'
import { utilsBridge } from "./utilsBridge";

declare global {
var ipcRenderer: IpcRenderer
}

// Since we disabled nodeIntegration we can reintroduce
// needed node functionality here
process.once('loaded', () => {
global.ipcRenderer = ipcRenderer
})

contextBridge.exposeInMainWorld("electron", { utilsBridge: utilsBridge });
28 changes: 28 additions & 0 deletions app/electron/src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowJs": true,
"alwaysStrict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strictPropertyInitialization": false,
"isolatedModules": true,
"jsx": "preserve",
"lib": ["dom", "es2017"],
"module": "commonjs",
"moduleResolution": "node",
"noEmit": false,
"noFallthroughCasesInSwitch": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
"noImplicitAny": false,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"outDir": "../main"
},
"exclude": ["node_modules"],
"include": ["**/*.ts", "**/*.tsx", "**/*.js"]
}
5 changes: 5 additions & 0 deletions app/electron/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const enum WindowStateListenerType {
Maximized,
Focused,
Fullscreen
}
66 changes: 66 additions & 0 deletions app/electron/src/utilsBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ipcRenderer } from "electron";
import { WindowStateListenerType } from "./types";
import * as process from "node:process";

export const utilsBridge = {
isBrowser: process.type === 'browser',
isMac: process.platform === 'darwin',
isWin: process.platform === 'win32',
isLinux: process.platform === 'linux',
isLinux64: process.platform === 'linux' && process.arch === 'x64',
isLinux32: process.platform === 'linux' && process.arch === 'ia32',
isLinuxARM: process.platform === 'linux' && process.arch === 'arm',
isLinuxARM64: process.platform === 'linux' && process.arch === 'arm64',
addWindowStateListener: (callback: (type: WindowStateListenerType, state: boolean) => void) => {
ipcRenderer.removeAllListeners("maximized")
ipcRenderer.on("maximized", () => {
callback(WindowStateListenerType.Maximized, true)
})
ipcRenderer.removeAllListeners("unmaximized")
ipcRenderer.on("unmaximized", () => {
callback(WindowStateListenerType.Maximized, false)
})
ipcRenderer.removeAllListeners("enter-fullscreen")
ipcRenderer.on("enter-fullscreen", () => {
callback(WindowStateListenerType.Fullscreen, true)
})
ipcRenderer.removeAllListeners("leave-fullscreen")
ipcRenderer.on("leave-fullscreen", () => {
callback(WindowStateListenerType.Fullscreen, false)
})
ipcRenderer.removeAllListeners("window-focus")
ipcRenderer.on("window-focus", () => {
callback(WindowStateListenerType.Focused, true)
})
ipcRenderer.removeAllListeners("window-blur")
ipcRenderer.on("window-blur", () => {
callback(WindowStateListenerType.Focused, false)
})
},
closeWindow: () => {
ipcRenderer.invoke("close-window")
},
minimizeWindow: () => {
ipcRenderer.invoke("minimize-window")
},
maximizeWindow: () => {
ipcRenderer.invoke("maximize-window")
},
isMaximized: () => {
return ipcRenderer.sendSync("is-maximized") as boolean
},
isFullscreen: () => {
return ipcRenderer.sendSync("is-fullscreen") as boolean
},
isFocused: () => {
return ipcRenderer.sendSync("is-focused") as boolean
},
}

declare global {
interface Window {
electron: {
utilsBridge: typeof utilsBridge
}
}
}
Loading

0 comments on commit 3c3f755

Please sign in to comment.