2020-08-02 04:06:18 +00:00
|
|
|
const { app, dialog, ipcMain, ipcRenderer, Menu, BrowserWindow } = require('electron')
|
2020-07-30 19:43:55 +00:00
|
|
|
const modpath = require('path')
|
|
|
|
const fs = require('fs')
|
|
|
|
const {URLSearchParams} = require('url')
|
|
|
|
const isMac = process.platform === 'darwin'
|
|
|
|
const chokidar = require('chokidar')
|
2020-08-02 04:06:18 +00:00
|
|
|
const Store = require('electron-store');
|
|
|
|
const store = new Store();
|
2020-08-27 01:42:18 +00:00
|
|
|
const WSMETA_FILENAME = "8bitworkshop.json"
|
|
|
|
const README_FILENAME = "README.md"
|
|
|
|
|
|
|
|
// call updater
|
|
|
|
require('update-electron-app')()
|
|
|
|
|
|
|
|
// show error dialog
|
|
|
|
function showError(msg, detail) {
|
|
|
|
msg = msg.message || msg;
|
|
|
|
dialog.showMessageBoxSync({
|
|
|
|
type: "error",
|
|
|
|
message: msg,
|
|
|
|
detail: detail
|
|
|
|
});
|
|
|
|
}
|
2020-07-30 19:43:55 +00:00
|
|
|
|
|
|
|
// file watcher
|
|
|
|
class Workspace {
|
2020-08-29 22:30:39 +00:00
|
|
|
constructor(directory, meta) {
|
2020-07-30 19:43:55 +00:00
|
|
|
this.directory = directory;
|
2020-08-27 01:42:18 +00:00
|
|
|
this.mainfile = meta.mainfile;
|
|
|
|
this.platform = meta.platform;
|
|
|
|
if (!this.mainfile) throw new Error(`The "mainfile" key is missing in ${WSMETA_FILENAME}.`)
|
|
|
|
if (!this.platform) throw new Error(`The "platform" key is missing in ${WSMETA_FILENAME}.`)
|
2020-08-29 22:30:39 +00:00
|
|
|
var mainfilepath = this.getMainFilePath();
|
2020-08-27 01:42:18 +00:00
|
|
|
if (!fs.existsSync(mainfilepath)) throw new Error(`The file "${mainfilepath}" is missing.`);
|
2020-08-29 22:30:39 +00:00
|
|
|
console.log("workspace opened", this.directory, this.mainfile);
|
|
|
|
}
|
|
|
|
getMainFilePath() {
|
|
|
|
return modpath.join(this.directory, this.mainfile);
|
|
|
|
}
|
|
|
|
close() {
|
|
|
|
this.unwatch();
|
|
|
|
console.log("workspace closed", this.directory, this.mainfile);
|
|
|
|
}
|
|
|
|
watch(wnd) {
|
|
|
|
this.watcher = chokidar.watch(this.directory, {
|
|
|
|
awaitWriteFinish: false
|
|
|
|
});
|
2020-07-30 19:43:55 +00:00
|
|
|
this.watcher.on('all', (event, path) => {
|
|
|
|
switch (event) {
|
2020-08-02 04:06:18 +00:00
|
|
|
case 'change':
|
2020-08-29 22:30:39 +00:00
|
|
|
console.log(event, path);
|
2020-08-02 04:06:18 +00:00
|
|
|
wnd.webContents.send('fileChanged', {
|
|
|
|
path: modpath.relative(this.directory, path),
|
2020-07-30 19:43:55 +00:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
2020-08-29 22:30:39 +00:00
|
|
|
console.log("watching workspace");
|
2020-08-02 04:06:18 +00:00
|
|
|
}
|
2020-08-29 22:30:39 +00:00
|
|
|
unwatch() {
|
2020-08-02 04:06:18 +00:00
|
|
|
if (this.watcher) {
|
|
|
|
this.watcher.close();
|
|
|
|
this.watcher = null;
|
2020-08-29 22:30:39 +00:00
|
|
|
console.log("un-watching workspace");
|
2020-08-02 04:06:18 +00:00
|
|
|
}
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-27 01:42:18 +00:00
|
|
|
function readWorkspaceMetadata(directory) {
|
|
|
|
// check README file
|
|
|
|
var readmepath = modpath.join(directory, README_FILENAME);
|
|
|
|
if (fs.existsSync(readmepath)) {
|
|
|
|
let readme = fs.readFileSync(readmepath, 'utf8');
|
|
|
|
let sess = {};
|
|
|
|
// check README for main file
|
|
|
|
const re8main = /8bitworkshop.com[^)]+file=([^)&]+)/;
|
|
|
|
m = re8main.exec(readme);
|
|
|
|
if (m && m[1]) {
|
|
|
|
sess.mainfile = m[1];
|
|
|
|
}
|
|
|
|
// check README for proper platform
|
|
|
|
// unless we use githubURL=
|
|
|
|
const re8plat = /8bitworkshop.com[^)]+platform=([A-Za-z0-9._\-]+)/;
|
|
|
|
m = re8plat.exec(readme);
|
|
|
|
if (m) {
|
|
|
|
sess.platform = m[1];
|
|
|
|
}
|
|
|
|
if (sess.mainfile != null && sess.platform != null) return sess;
|
|
|
|
}
|
|
|
|
// check JSON file
|
|
|
|
var metapath = modpath.join(directory, WSMETA_FILENAME);
|
|
|
|
if (fs.existsSync(metapath)) {
|
|
|
|
return JSON.parse(fs.readFileSync(metapath, 'utf8'));
|
2020-08-02 04:06:18 +00:00
|
|
|
}
|
2020-08-27 01:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function writeWorkspaceMetadata(directory, meta) {
|
|
|
|
var metapath = modpath.join(directory, WSMETA_FILENAME);
|
|
|
|
fs.writeFileSync(metapath, JSON.stringify(meta), 'utf8');
|
|
|
|
}
|
|
|
|
|
|
|
|
function openWorkspace(wnd, ws) {
|
2020-08-03 01:22:20 +00:00
|
|
|
if (wnd.workspace) { wnd.workspace.close(); }
|
2020-07-30 19:43:55 +00:00
|
|
|
wnd.workspace = ws;
|
2020-08-02 04:06:18 +00:00
|
|
|
wnd.on('closed', () => {
|
|
|
|
ws.close();
|
|
|
|
});
|
2020-07-30 19:43:55 +00:00
|
|
|
var qs = new URLSearchParams();
|
2020-08-02 04:06:18 +00:00
|
|
|
qs.set('electron_ws', 1);
|
2020-08-03 01:22:20 +00:00
|
|
|
qs.set('repo', ws.directory);
|
|
|
|
qs.set('file', ws.mainfile);
|
2020-08-27 01:42:18 +00:00
|
|
|
qs.set('platform', ws.platform);
|
|
|
|
console.log(qs);
|
2020-08-02 04:06:18 +00:00
|
|
|
wnd.loadURL(`file://${__dirname}/electron.html?${qs}`).then(() => {
|
2020-08-03 01:22:20 +00:00
|
|
|
wnd.webContents.send('setWorkspaceRoot', {root:ws.directory});
|
2020-08-02 04:06:18 +00:00
|
|
|
});
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
|
|
|
|
2020-08-29 22:30:39 +00:00
|
|
|
function getActiveWorkspace() {
|
|
|
|
var wnd = BrowserWindow.getFocusedWindow();
|
|
|
|
return wnd && wnd.workspace;
|
|
|
|
}
|
|
|
|
|
2020-08-03 01:22:20 +00:00
|
|
|
// TODO: doesn't work if browser window reloads itself
|
|
|
|
function reloadCurrentWindow() {
|
|
|
|
var wnd = BrowserWindow.getFocusedWindow();
|
|
|
|
if (wnd.workspace) {
|
|
|
|
openWorkspace(wnd, wnd.workspace);
|
|
|
|
} else {
|
|
|
|
wnd.reload();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-27 01:42:18 +00:00
|
|
|
// TODO: better way to get this?
|
|
|
|
function getCurrentPlatform(wnd) {
|
|
|
|
var url = wnd.webContents.getURL();
|
|
|
|
if (url != null) {
|
|
|
|
console.log(url);
|
|
|
|
var m = /platform=([^&]+)/.exec(url);
|
|
|
|
if (m) return m[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function openWorkspaceWindow(wspath) {
|
|
|
|
try {
|
|
|
|
// replace current window
|
|
|
|
var wnd = BrowserWindow.getFocusedWindow() || createWindow();
|
|
|
|
// read metadata file
|
|
|
|
var meta = readWorkspaceMetadata(wspath);
|
|
|
|
// does it exist?
|
|
|
|
if (meta == null) {
|
|
|
|
// create a new workspace?
|
|
|
|
var cancel = dialog.showMessageBoxSync(wnd, {
|
|
|
|
type: 'question',
|
|
|
|
title: 'Create Workspace?',
|
|
|
|
message: `Project metadata not found. Create new ${getCurrentPlatform(wnd)||""} project in this directory?`,
|
|
|
|
detail: wspath,
|
|
|
|
buttons: ['Create', 'Cancel'],
|
|
|
|
});
|
|
|
|
if (!cancel) {
|
|
|
|
var platform = getCurrentPlatform(wnd);
|
|
|
|
// choose main file
|
|
|
|
var files = dialog.showOpenDialogSync({
|
|
|
|
message: `Choose the main file of your ${platform} project, or create one.`,
|
|
|
|
defaultPath: wspath,
|
|
|
|
properties: ['openFile','promptToCreate'],
|
|
|
|
});
|
|
|
|
if (files != null) {
|
|
|
|
var mainfile = modpath.relative(wspath, files[0]);
|
|
|
|
// write new metadata
|
|
|
|
meta = {
|
|
|
|
platform: platform,
|
|
|
|
mainfile: mainfile,
|
|
|
|
};
|
|
|
|
console.log(meta);
|
|
|
|
if (meta.platform == null) {
|
|
|
|
showError("Can't determine current platform.");
|
|
|
|
} else {
|
|
|
|
writeWorkspaceMetadata(wspath, meta);
|
|
|
|
openWorkspaceWindow(wspath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log(meta);
|
2020-08-29 22:30:39 +00:00
|
|
|
var ws = new Workspace(wspath, meta);
|
2020-08-27 01:42:18 +00:00
|
|
|
openWorkspace(wnd, ws);
|
|
|
|
app.addRecentDocument(wspath);
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
2020-08-27 01:42:18 +00:00
|
|
|
} catch (e) {
|
|
|
|
showError(e);
|
|
|
|
}
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
|
|
|
|
2020-08-27 01:42:18 +00:00
|
|
|
function openDefaultWorkspace() {
|
2020-08-03 01:22:20 +00:00
|
|
|
if (process.argv.length >= 3) {
|
2020-08-27 01:42:18 +00:00
|
|
|
openWorkspaceWindow(modpath.resolve(process.argv[2]));
|
|
|
|
} else {
|
|
|
|
createWindow();
|
2020-08-02 04:06:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-27 01:42:18 +00:00
|
|
|
function openWorkspaceDialog() {
|
|
|
|
dialog.showOpenDialog({
|
2020-07-30 19:43:55 +00:00
|
|
|
title: "Open Workspace",
|
2020-08-27 01:42:18 +00:00
|
|
|
properties: ['openDirectory'],
|
|
|
|
message: "Choose the directory that holds your source files.",
|
|
|
|
}).then((rtn) => {
|
|
|
|
if (!rtn.canceled && rtn.filePaths && rtn.filePaths.length > 0) {
|
|
|
|
var path = rtn.filePaths[0];
|
|
|
|
openWorkspaceWindow(path);
|
|
|
|
}
|
|
|
|
});
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function openURL(url) {
|
|
|
|
return async () => {
|
|
|
|
const { shell } = require('electron')
|
|
|
|
await shell.openExternal(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildMenu() {
|
|
|
|
|
|
|
|
const template = [
|
|
|
|
// { role: 'appMenu' }
|
|
|
|
...(isMac ? [{
|
|
|
|
label: app.name,
|
|
|
|
submenu: [
|
|
|
|
{ role: 'about' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'services' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'hide' },
|
|
|
|
{ role: 'hideothers' },
|
|
|
|
{ role: 'unhide' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'quit' }
|
|
|
|
]
|
|
|
|
}] : []),
|
|
|
|
// { role: 'fileMenu' }
|
|
|
|
{
|
|
|
|
label: 'File',
|
|
|
|
submenu: [
|
|
|
|
{
|
2020-08-27 01:42:18 +00:00
|
|
|
label: 'New Playground',
|
2020-08-29 22:30:39 +00:00
|
|
|
click: createWindow,
|
2020-08-27 01:42:18 +00:00
|
|
|
accelerator: 'CmdOrCtrl+N',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: 'Open Workspace...',
|
|
|
|
click: openWorkspaceDialog,
|
2020-07-30 19:43:55 +00:00
|
|
|
accelerator: 'CmdOrCtrl+O',
|
|
|
|
},
|
2020-08-02 04:06:18 +00:00
|
|
|
// When a file is requested from the recent documents menu, the open-file event of app module will be emitted for it.
|
|
|
|
{
|
|
|
|
"label":"Open Recent",
|
|
|
|
"role":"recentdocuments",
|
|
|
|
"submenu":[
|
|
|
|
{
|
|
|
|
"label":"Clear Recent",
|
|
|
|
"role":"clearrecentdocuments"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
2020-07-30 19:43:55 +00:00
|
|
|
{ type: 'separator' },
|
|
|
|
isMac ? { role: 'close' } : { role: 'quit' }
|
|
|
|
]
|
|
|
|
},
|
|
|
|
// { role: 'editMenu' }
|
|
|
|
{
|
|
|
|
label: 'Edit',
|
|
|
|
submenu: [
|
|
|
|
{ role: 'undo' },
|
|
|
|
{ role: 'redo' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'cut' },
|
|
|
|
{ role: 'copy' },
|
|
|
|
{ role: 'paste' },
|
|
|
|
...(isMac ? [
|
|
|
|
{ role: 'pasteAndMatchStyle' },
|
|
|
|
{ role: 'delete' },
|
|
|
|
{ role: 'selectAll' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{
|
|
|
|
label: 'Speech',
|
|
|
|
submenu: [
|
|
|
|
{ role: 'startspeaking' },
|
|
|
|
{ role: 'stopspeaking' }
|
|
|
|
]
|
|
|
|
}
|
|
|
|
] : [
|
|
|
|
{ role: 'delete' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'selectAll' }
|
|
|
|
])
|
|
|
|
]
|
|
|
|
},
|
|
|
|
// { role: 'viewMenu' }
|
|
|
|
{
|
|
|
|
label: 'View',
|
|
|
|
submenu: [
|
2020-08-03 01:22:20 +00:00
|
|
|
{
|
|
|
|
label: 'Reload Window',
|
|
|
|
click: reloadCurrentWindow
|
|
|
|
},
|
2020-07-30 19:43:55 +00:00
|
|
|
{ role: 'toggledevtools' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'resetzoom' },
|
|
|
|
{ role: 'zoomin' },
|
|
|
|
{ role: 'zoomout' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'togglefullscreen' }
|
|
|
|
]
|
|
|
|
},
|
|
|
|
// { role: 'windowMenu' }
|
|
|
|
{
|
|
|
|
label: 'Window',
|
|
|
|
submenu: [
|
|
|
|
{ role: 'minimize' },
|
|
|
|
{ role: 'zoom' },
|
|
|
|
...(isMac ? [
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'front' },
|
|
|
|
{ type: 'separator' },
|
|
|
|
{ role: 'window' }
|
|
|
|
] : [
|
|
|
|
{ role: 'close' }
|
|
|
|
])
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
role: 'help',
|
|
|
|
submenu: [
|
|
|
|
{
|
|
|
|
label: 'IDE Help',
|
|
|
|
click: openURL('https://8bitworkshop.com/blog/docs/ide.md.html')
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: 'Latest News',
|
|
|
|
click: openURL('https://8bitworkshop.com/blog/')
|
|
|
|
},
|
2020-08-27 01:42:18 +00:00
|
|
|
{
|
|
|
|
label: 'Report an Issue',
|
|
|
|
click: openURL('https://github.com/sehugg/8bitworkshop/issues/new')
|
|
|
|
},
|
|
|
|
{ type: 'separator' },
|
2020-07-30 19:43:55 +00:00
|
|
|
{
|
|
|
|
label: 'Follow @8bitworkshop on Twitter',
|
|
|
|
click: openURL('https://twitter.com/8bitworkshop')
|
|
|
|
},
|
2020-08-31 14:26:37 +00:00
|
|
|
{
|
|
|
|
label: 'Browse Books on Amazon',
|
|
|
|
click: openURL('https://www.amazon.com/s?k=8bitworkshop&i=stripbooks&dc&qid=1598884483&tag=pzp-20')
|
|
|
|
},
|
2020-07-30 19:43:55 +00:00
|
|
|
{
|
2020-08-27 01:42:18 +00:00
|
|
|
label: 'Become a Patreon',
|
|
|
|
click: openURL('https://www.patreon.com/8bitworkshop')
|
2020-07-30 19:43:55 +00:00
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
const menu = Menu.buildFromTemplate(template)
|
|
|
|
Menu.setApplicationMenu(menu)
|
|
|
|
}
|
|
|
|
|
|
|
|
function createWindow () {
|
|
|
|
// Create the browser window.
|
|
|
|
const win = new BrowserWindow({
|
|
|
|
width: 1024,
|
|
|
|
height: 768,
|
|
|
|
backgroundColor: '#fff',
|
|
|
|
webPreferences: {
|
|
|
|
preload: modpath.join(__dirname, './electron-preload.js'),
|
|
|
|
nodeIntegration: false,
|
|
|
|
enableRemoteModule: false,
|
2020-08-02 04:06:18 +00:00
|
|
|
contextIsolation: false,
|
|
|
|
sandbox: false,
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// and load the index.html of the app.
|
2020-08-02 04:06:18 +00:00
|
|
|
win.loadFile('electron.html', {
|
|
|
|
search: 'repo=/'
|
|
|
|
})
|
2020-07-30 19:43:55 +00:00
|
|
|
|
|
|
|
// Open the DevTools.
|
|
|
|
//win.webContents.openDevTools()
|
|
|
|
|
|
|
|
// Maximize
|
|
|
|
win.maximize();
|
|
|
|
return win;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2020-08-27 01:42:18 +00:00
|
|
|
app.whenReady().then(buildMenu).then(openDefaultWorkspace)
|
2020-07-30 19:43:55 +00:00
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
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) {
|
2020-08-27 01:42:18 +00:00
|
|
|
openDefaultWorkspace()
|
2020-07-30 19:43:55 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-08-29 22:30:39 +00:00
|
|
|
app.on('browser-window-focus', (e) => {
|
|
|
|
var ws = e.sender.workspace;
|
|
|
|
if (ws) ws.unwatch();
|
|
|
|
})
|
|
|
|
|
|
|
|
app.on('browser-window-blur', (e) => {
|
|
|
|
var ws = e.sender.workspace;
|
|
|
|
if (ws) ws.watch(e.sender);
|
|
|
|
})
|
|
|
|
|
2020-08-02 04:06:18 +00:00
|
|
|
app.on('open-file', (event, path) => {
|
2020-08-27 01:42:18 +00:00
|
|
|
openWorkspaceWindow(path);
|
2020-08-02 04:06:18 +00:00
|
|
|
})
|
|
|
|
|
2020-07-30 19:43:55 +00:00
|
|
|
// 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.
|
|
|
|
|
|
|
|
// Register IPC messages
|
|
|
|
// https://stackoverflow.com/questions/52236641/electron-ipc-and-nodeintegration
|
|
|
|
/*
|
|
|
|
ipcMain.on('hello', (event, message) => {
|
|
|
|
console.log(event);
|
|
|
|
});
|
|
|
|
*/
|
2020-08-03 01:22:20 +00:00
|
|
|
|