Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop/UI #2

Merged
merged 12 commits into from
Jul 14, 2024
Merged
347 changes: 326 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"version": "4.0.0",
"description": "A foundation for scalable desktop apps",
"keywords": [
"electron",
Expand Down Expand Up @@ -78,7 +79,8 @@
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
"ace-builds": "<rootDir>/node_modules/ace-builds"
},
"setupFiles": [
"./.erb/scripts/check-build-exists.ts"
Expand All @@ -96,11 +98,15 @@
}
},
"dependencies": {
"@reduxjs/toolkit": "^2.2.6",
"ace-builds": "^1.35.1",
"electron-debug": "^3.2.0",
"electron-log": "^4.4.8",
"electron-updater": "^6.1.4",
"react": "^18.2.0",
"react-ace": "^12.0.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.16.0"
},
"devDependencies": {
Expand Down Expand Up @@ -149,6 +155,7 @@
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdoc": "^4.0.3",
"mini-css-extract-plugin": "^2.7.6",
"prettier": "^3.0.3",
"react-refresh": "^0.14.0",
Expand Down
10 changes: 10 additions & 0 deletions src/common/AppConsoleMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default class AppConsoleMessage {
#type: string;

#text: string;

constructor(type: string, text: string) {
this.#type = type;
this.#text = text;
}
}
164 changes: 164 additions & 0 deletions src/main/MainApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { dialog, ipcMain } from 'electron';
import type { BrowserWindow } from 'electron';
import fs from 'fs';
import { version as dawnVersion } from '../../package.json';
import AppConsoleMessage from '../common/AppConsoleMessage';

const WATCH_DEBOUNCE_MS = 3000;
const CODE_FILE_FILTERS = [
{ name: 'Python Files', extensions: ['py'] },
{ name: 'All Files', extensions: ['*'] },
];

export default class MainApp {
readonly #mainWindow: BrowserWindow;

#savePath: string | null;

#watcher: fs.FSWatcher | null;

#watchDebounce: boolean;

#preventQuit: boolean;

constructor(mainWindow: BrowserWindow) {
this.#mainWindow = mainWindow;
this.#savePath = null;
this.#watcher = null;
this.#watchDebounce = true;
this.#preventQuit = true;
mainWindow.on('close', (e) => {
if (this.#preventQuit) {
e.preventDefault();
this.#sendToRenderer('renderer-quit-request');
}
});
ipcMain.on('main-file-control', (_event, data) => {
if (data.type === 'save') {
this.#saveCodeFile(data.content as string, data.forceDialog as boolean);
} else if (data.type === 'load') {
this.#openCodeFile();
} else {
// eslint-disable-next-line no-console
console.error(`Unknown data.type for main-file-control ${data.type}`);
}
});
ipcMain.on('main-quit', () => {
this.#preventQuit = false;
this.#mainWindow.close();
});
}

#sendToRenderer(...args) {
this.#mainWindow.webContents.send(...args);
}

onPresent() {
this.#watcher?.close();
this.#savePath = null;
this.#sendToRenderer('renderer-init', { dawnVersion });
}

promptSaveCodeFile(forceDialog: boolean) {
this.#sendToRenderer('renderer-file-control', {
type: 'promptSave',
forceDialog,
});
}

promptLoadCodeFile() {
this.#sendToRenderer('renderer-file-control', { type: 'promptLoad' });
}

#saveCodeFile(code: string, forceDialog: boolean) {
let success = true;
if (this.#savePath === null || forceDialog) {
success = this.#showCodePathDialog('save');
}
if (success) {
// Temporarily disable watcher
this.#watcher?.close();
this.#watcher = null;
fs.writeFile(
this.#savePath,
code,
{ encoding: 'utf8', flag: 'w' },
(err) => {
if (err) {
this.#sendToRenderer(
'renderer-post-console',
new AppConsoleMessage(
'dawn-err',
`Failed to save code to ${this.#savePath}. ${err}`,
),
);
} else {
this.#sendToRenderer('renderer-file-control', { type: 'didSave' });
}
this.#watchCodeFile();
},
);
}
}

#openCodeFile() {
const success = this.#showCodePathDialog('load');
if (success) {
this.#sendToRenderer('renderer-file-control', {
type: 'didOpen',
content: fs.readFileSync(this.#savePath, {
encoding: 'utf8',
flag: 'r',
}),
});
}
}

#showCodePathDialog(mode: 'save' | 'load') {
let result: string | string[] | undefined;
if (mode === 'save') {
result = dialog.showSaveDialogSync(this.#mainWindow, {
filters: CODE_FILE_FILTERS,
...(this.#savePath === null ? {} : { defaultPath: this.#savePath }),
});
} else {
result = dialog.showOpenDialogSync(this.#mainWindow, {
filters: CODE_FILE_FILTERS,
properties: ['openFile'],
});
}
if (result && result.length) {
this.#savePath = typeof result === 'string' ? result : result[0];
this.#sendToRenderer('renderer-file-control', {
type: 'didChangePath',
path: this.#savePath,
});
if (mode === 'load') {
this.#watchCodeFile();
}
return true;
}
return false;
}

#watchCodeFile() {
this.#watcher?.close();
this.#watchDebounce = true;
this.#watcher = fs.watch(
this.#savePath,
{ persistent: false, encoding: 'utf8' },
() => {
// Don't care what the event type is
if (this.#watchDebounce) {
this.#watchDebounce = false;
setTimeout(() => {
this.#watchDebounce = true;
}, WATCH_DEBOUNCE_MS);
this.#sendToRenderer('renderer-file-control', {
type: 'didExternalChange',
});
}
},
);
}
}
14 changes: 5 additions & 9 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron';
import { app, BrowserWindow, shell } from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
import { resolveHtmlPath } from './util';
import MainApp from './MainApp';

class AppUpdater {
constructor() {
Expand All @@ -25,12 +26,6 @@ class AppUpdater {

let mainWindow: BrowserWindow | null = null;

ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
console.log(msgTemplate(arg));
event.reply('ipc-example', msgTemplate('pong'));
});

if (process.env.NODE_ENV === 'production') {
const sourceMapSupport = require('source-map-support');
sourceMapSupport.install();
Expand Down Expand Up @@ -80,13 +75,14 @@ const createWindow = async () => {
: path.join(__dirname, '../../.erb/dll/preload.js'),
},
});
const mainApp = new MainApp(mainWindow);

mainWindow.loadURL(resolveHtmlPath('index.html'));

mainWindow.on('ready-to-show', () => {
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
mainApp.onPresent();
if (process.env.START_MINIMIZED) {
mainWindow.minimize();
} else {
Expand All @@ -98,7 +94,7 @@ const createWindow = async () => {
mainWindow = null;
});

const menuBuilder = new MenuBuilder(mainWindow);
const menuBuilder = new MenuBuilder(mainApp, mainWindow);
menuBuilder.buildMenu();

// Open urls in the user's browser
Expand Down
28 changes: 21 additions & 7 deletions src/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import {
BrowserWindow,
MenuItemConstructorOptions,
} from 'electron';
import type MainApp from './MainApp';

interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
selector?: string;
submenu?: DarwinMenuItemConstructorOptions[] | Menu;
}

export default class MenuBuilder {
mainApp: MainApp;

mainWindow: BrowserWindow;

constructor(mainWindow: BrowserWindow) {
constructor(mainApp: MainApp, mainWindow: BrowserWindow) {
this.mainApp = mainApp;
this.mainWindow = mainWindow;
}

Expand Down Expand Up @@ -200,12 +204,22 @@ export default class MenuBuilder {
{
label: '&Open',
accelerator: 'Ctrl+O',
click: () => {
this.mainApp.promptLoadCodeFile();
},
},
{
label: '&Close',
accelerator: 'Ctrl+W',
label: '&Save',
accelerator: 'Ctrl+S',
click: () => {
this.mainWindow.close();
this.mainApp.promptSaveCodeFile(false);
},
},
{
label: 'Save &As',
accelerator: 'Ctrl+Shift+S',
click: () => {
this.mainApp.promptSaveCodeFile(true);
},
},
],
Expand Down Expand Up @@ -234,7 +248,7 @@ export default class MenuBuilder {
},
{
label: 'Toggle &Developer Tools',
accelerator: 'Alt+Ctrl+I',
accelerator: 'Ctrl+Shift+I',
click: () => {
this.mainWindow.webContents.toggleDevTools();
},
Expand All @@ -252,7 +266,7 @@ export default class MenuBuilder {
},
],
},
{
/* {
label: 'Help',
submenu: [
{
Expand Down Expand Up @@ -282,7 +296,7 @@ export default class MenuBuilder {
},
},
],
},
}, */
];

return templateDefault;
Expand Down
9 changes: 8 additions & 1 deletion src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
/* eslint no-unused-vars: off */
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';

export type Channels = 'ipc-example';
export type Channels =
| 'renderer-init'
| 'renderer-robot-update'
| 'renderer-post-console'
| 'renderer-file-control'
| 'renderer-quit-request'
| 'main-file-control'
| 'main-quit';

const electronHandler = {
ipcRenderer: {
Expand Down
31 changes: 27 additions & 4 deletions src/renderer/App.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
/*
* @NOTE: Prepend a `~` to css file paths that are in your node_modules
* See https://github.com/webpack-contrib/sass-loader#imports
*/
body {
margin: 0;
}

.App {
display: flex;
flex-direction: column;
height: 100vh;
}
.App-cols {
display: flex;
flex-direction: row;
flex-shrink: 0;
}
.App-modal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 50%);
display: none;
z-index: 4;
}
.App-modal-container:has(.modal-active) {
display: block;
}
Loading
Loading