Skip to content

Commit

Permalink
Merge pull request #113 from arduino/bugfix/about-page
Browse files Browse the repository at this point in the history
Fix about page and split `index.js` into backend helper files
  • Loading branch information
murilopolese authored Apr 17, 2024
2 parents 9160342 + 8792acc commit 2b560af
Show file tree
Hide file tree
Showing 9 changed files with 417 additions and 304 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ This project is sponsored by Arduino, based on original work by [Murilo Polese](

## Technical overview

Arduino Lab for MicroPython is an [Electron](https://www.electronjs.org/) app that has its main purpose to communicate over serial with a microprocessor running [MicroPython](https://micropython.org/). All Electron code is at `/index.js`.
Arduino Lab for MicroPython is an [Electron](https://www.electronjs.org/) app that has its main purpose to communicate over serial with a microprocessor running [MicroPython](https://micropython.org/). The Electron code is at `/index.js` and inside the folder `/backend`.

All operations over serial are abstracted and packaged on `/micropython.js` which is an attempt of porting `pyboard.py`. The port has its [own repository](https://github.com/arduino/micropython.js) but for the sake of simplicity and transparency, `micropython.js` is committed as source code.
All operations over serial are abstracted and packaged on `micropython.js` which is an attempt of porting `pyboard.py`. The module has its [own repository](https://github.com/arduino/micropython.js) with documentation and examples of usage.

The User Interface (UI) source code stays inside `/ui` folder and is completely independent of the Electron code.

Expand All @@ -49,6 +49,7 @@ At the root of the repository you will find:
- `/build_resources`: Icons and other assets used during the build process.
- `/ui`: Available user interfaces.
- `/index.js`: Main Electron code.
- `/backend`: Electron helpers.
- `/preload.js`: Creates Disk, Serial and Window APIs on Electron's main process and exposes it to Electron's renderer process (context bridge).

## User interface
Expand Down
62 changes: 62 additions & 0 deletions backend/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const { dialog } = require('electron')
const fs = require('fs')
const path = require('path')

async function openFolderDialog(win) {
// https://stackoverflow.com/questions/46027287/electron-open-folder-dialog
let dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] })
return dir.filePaths[0] || null
}

function listFolder(folder) {
files = fs.readdirSync(path.resolve(folder))
// Filter out directories
files = files.filter(f => {
let filePath = path.resolve(folder, f)
return !fs.lstatSync(filePath).isDirectory()
})
return files
}

function ilistFolder(folder) {
let files = fs.readdirSync(path.resolve(folder))
files = files.filter(f => {
let filePath = path.resolve(folder, f)
return !fs.lstatSync(filePath).isSymbolicLink()
})
files = files.map(f => {
let filePath = path.resolve(folder, f)
return {
path: f,
type: fs.lstatSync(filePath).isDirectory() ? 'folder' : 'file'
}
})
// Filter out dot files
files = files.filter(f => f.path.indexOf('.') !== 0)
return files
}

function getAllFiles(dirPath, arrayOfFiles) {
// https://coderrocketfuel.com/article/recursively-list-all-the-files-in-a-directory-using-node-js
files = ilistFolder(dirPath)
arrayOfFiles = arrayOfFiles || []
files.forEach(function(file) {
const p = path.join(dirPath, file.path)
const stat = fs.statSync(p)
arrayOfFiles.push({
path: p,
type: stat.isDirectory() ? 'folder' : 'file'
})
if (stat.isDirectory()) {
arrayOfFiles = getAllFiles(p, arrayOfFiles)
}
})
return arrayOfFiles
}

module.exports = {
openFolderDialog,
listFolder,
ilistFolder,
getAllFiles
}
110 changes: 110 additions & 0 deletions backend/ipc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const fs = require('fs')
const {
openFolderDialog,
listFolder,
ilistFolder,
getAllFiles
} = require('./helpers.js')

module.exports = function registerIPCHandlers(win, ipcMain) {
ipcMain.handle('open-folder', async (event) => {
console.log('ipcMain', 'open-folder')
const folder = await openFolderDialog(win)
let files = []
if (folder) {
files = listFolder(folder)
}
return { folder, files }
})

ipcMain.handle('list-files', async (event, folder) => {
console.log('ipcMain', 'list-files', folder)
if (!folder) return []
return listFolder(folder)
})

ipcMain.handle('ilist-files', async (event, folder) => {
console.log('ipcMain', 'ilist-files', folder)
if (!folder) return []
return ilistFolder(folder)
})

ipcMain.handle('ilist-all-files', (event, folder) => {
console.log('ipcMain', 'ilist-all-files', folder)
if (!folder) return []
return getAllFiles(folder)
})

ipcMain.handle('load-file', (event, filePath) => {
console.log('ipcMain', 'load-file', filePath)
let content = fs.readFileSync(filePath)
return content
})

ipcMain.handle('save-file', (event, filePath, content) => {
console.log('ipcMain', 'save-file', filePath, content)
fs.writeFileSync(filePath, content, 'utf8')
return true
})

ipcMain.handle('update-folder', (event, folder) => {
console.log('ipcMain', 'update-folder', folder)
let files = fs.readdirSync(path.resolve(folder))
// Filter out directories
files = files.filter(f => {
let filePath = path.resolve(folder, f)
return !fs.lstatSync(filePath).isDirectory()
})
return { folder, files }
})

ipcMain.handle('remove-file', (event, filePath) => {
console.log('ipcMain', 'remove-file', filePath)
fs.unlinkSync(filePath)
return true
})

ipcMain.handle('rename-file', (event, filePath, newFilePath) => {
console.log('ipcMain', 'rename-file', filePath, newFilePath)
fs.renameSync(filePath, newFilePath)
return true
})

ipcMain.handle('create-folder', (event, folderPath) => {
console.log('ipcMain', 'create-folder', folderPath)
try {
fs.mkdirSync(folderPath, { recursive: true })
} catch(e) {
console.log('error', e)
return false
}
return true
})

ipcMain.handle('remove-folder', (event, folderPath) => {
console.log('ipcMain', 'remove-folder', folderPath)
fs.rmdirSync(folderPath, { recursive: true, force: true })
return true
})

ipcMain.handle('file-exists', (event, filePath) => {
console.log('ipcMain', 'file-exists', filePath)
try {
fs.accessSync(filePath, fs.constants.F_OK)
return true
} catch(err) {
return false
}
})
// WINDOW MANAGEMENT

ipcMain.handle('set-window-size', (event, minWidth, minHeight) => {
console.log('ipcMain', 'set-window-size', minWidth, minHeight)
if (!win) {
console.log('No window defined')
return false
}

win.setMinimumSize(minWidth, minHeight)
})
}
143 changes: 143 additions & 0 deletions backend/menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
const { app, Menu } = require('electron')
const path = require('path')
const openAboutWindow = require('about-window').default

module.exports = function registerMenu(win) {
const isMac = process.platform === 'darwin'
const isDev = !app.isPackaged
const template = [
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about'},
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
{
label: 'File',
submenu: [
isMac ? { role: 'close' } : { role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
...(isMac ? [
{ role: 'pasteAndMatchStyle' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [
{ role: 'startSpeaking' },
{ role: 'stopSpeaking' }
]
}
] : [
{ type: 'separator' },
{ role: 'selectAll' }
])
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
...(isDev ? [
{ type: 'separator' },
{ role: 'toggleDevTools' },
]:[
])
]
},
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'zoom' },
...(isMac ? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' }
] : [
{ role: 'close' }
])
]
},
{
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://github.com/arduino/lab-micropython-editor')
}
},
{
label: 'Report an issue',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://github.com/arduino/lab-micropython-editor/issues')
}
},
{
label:'Info about this app',
click: () => {
openAboutWindow({
icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'),
css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'),
// about_page_dir: path.resolve(__dirname, '../ui/arduino/views/'),
copyright: '© Arduino SA 2022',
package_json_dir: path.resolve(__dirname, '..'),
bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues",
bug_link_text: "report an issue",
homepage: "https://labs.arduino.cc",
use_version_info: false,
win_options: {
parent: win,
modal: true,
},
show_close_button: 'Close',
})
}
},
]
}
]

const menu = Menu.buildFromTemplate(template)

app.setAboutPanelOptions({
applicationName: app.name,
applicationVersion: app.getVersion(),
copyright: app.copyright,
credits: '(See "Info about this app" in the Help menu)',
authors: ['Arduino'],
website: 'https://arduino.cc',
iconPath: path.join(__dirname, '../assets/image.png'),
})

Menu.setApplicationMenu(menu)

}
Loading

0 comments on commit 2b560af

Please sign in to comment.