diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 137a1ec..9f35452 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,8 @@ jobs: config: - os: windows-2019 - os: ubuntu-latest - - os: macos-latest + - os: macos-13 + - os: macos-14 runs-on: ${{ matrix.config.os }} timeout-minutes: 90 @@ -99,6 +100,8 @@ jobs: name: Arduino-Lab-for-MicroPython_Linux_X86-64 - path: "*-mac_x64.zip" name: Arduino-Lab-for-MicroPython_macOS_X86-64 + - path: "*-mac_arm64.zip" + name: Arduino-Lab-for-MicroPython_macOS_arm-64 # - path: "*Windows_64bit.exe" # name: Windows_X86-64_interactive_installer # - path: "*Windows_64bit.msi" diff --git a/backend/helpers.js b/backend/helpers.js index 427d360..0976293 100644 --- a/backend/helpers.js +++ b/backend/helpers.js @@ -4,12 +4,12 @@ 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' ] }) + const dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] }) return dir.filePaths[0] || null } function listFolder(folder) { - files = fs.readdirSync(path.resolve(folder)) + let files = fs.readdirSync(path.resolve(folder)) // Filter out directories files = files.filter(f => { let filePath = path.resolve(folder, f) @@ -38,7 +38,7 @@ function ilistFolder(folder) { function getAllFiles(dirPath, arrayOfFiles) { // https://coderrocketfuel.com/article/recursively-list-all-the-files-in-a-directory-using-node-js - files = ilistFolder(dirPath) + let files = ilistFolder(dirPath) arrayOfFiles = arrayOfFiles || [] files.forEach(function(file) { const p = path.join(dirPath, file.path) diff --git a/backend/ipc.js b/backend/ipc.js index c97daba..d4ddc74 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -6,7 +6,7 @@ const { getAllFiles } = require('./helpers.js') -module.exports = function registerIPCHandlers(win, ipcMain) { +module.exports = function registerIPCHandlers(win, ipcMain, app) { ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') const folder = await openFolderDialog(win) @@ -107,4 +107,24 @@ module.exports = function registerIPCHandlers(win, ipcMain) { win.setMinimumSize(minWidth, minHeight) }) + + ipcMain.handle('confirm-close', () => { + console.log('ipcMain', 'confirm-close') + app.exit() + }) + + ipcMain.handle('is-packaged', () => { + return app.isPackaged + }) + + ipcMain.handle('get-app-path', () => { + console.log('ipcMain', 'get-app-path') + return app.getAppPath() + }) + + win.on('close', (event) => { + console.log('BrowserWindow', 'close') + event.preventDefault() + win.webContents.send('check-before-close') + }) } diff --git a/backend/menu.js b/backend/menu.js index 3ee40a6..6b62cdf 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -4,7 +4,6 @@ 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, @@ -56,17 +55,13 @@ module.exports = function registerMenu(win) { label: 'View', submenu: [ { role: 'reload' }, + { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, - ...(isDev ? [ - { type: 'separator' }, - { role: 'toggleDevTools' }, - ]:[ - ]) ] }, { diff --git a/index.js b/index.js index 2992967..b38f989 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,8 @@ const registerIPCHandlers = require('./backend/ipc.js') const registerMenu = require('./backend/menu.js') let win = null // main window +let splash = null +let splashTimestamp = null // START APP function createWindow () { @@ -17,17 +19,42 @@ function createWindow () { nodeIntegration: false, webSecurity: true, enableRemoteModule: false, - preload: path.join(__dirname, "preload.js") + preload: path.join(__dirname, "preload.js"), + show: false } }) // and load the index.html of the app. win.loadFile('ui/arduino/index.html') - registerIPCHandlers(win, ipcMain) - registerMenu(win) -} + // If the app takes a while to open, show splash screen + // Create the splash screen + splash = new BrowserWindow({ + width: 450, + height: 140, + transparent: true, + frame: false, + alwaysOnTop: true + }); + splash.loadFile('ui/arduino/splash.html') + splashTimestamp = Date.now() + + win.once('ready-to-show', () => { + if (Date.now()-splashTimestamp > 1000) { + splash.destroy() + } else { + setTimeout(() => { + splash.destroy() + }, 500) + } + win.show() + }) + registerIPCHandlers(win, ipcMain, app) + registerMenu(win) -// TODO: Loading splash screen + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +} -app.whenReady().then(createWindow) +app.on('ready', createWindow) diff --git a/package-lock.json b/package-lock.json index 61b8548..883d73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "arduino-lab-micropython-ide", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arduino-lab-micropython-ide", - "version": "0.9.0", + "version": "0.10.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ad5c2cd..2d3f773 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "arduino-lab-micropython-ide", "productName": "Arduino Lab for MicroPython", - "version": "0.9.1", + "version": "0.10.0", "description": "Arduino Lab for MicroPython is a project sponsored by Arduino, based on original work by Murilo Polese.\nThis is an experimental pre-release software, please direct any questions exclusively to Github issues.", "main": "index.js", "scripts": { "post-set-shell": "npm config set script-shell bash", "rebuild": "electron-rebuild", "dev": "electron --inspect ./", - "build": "npm run post-set-shell && electron-builder $(if [ $(uname -m) = arm64 ]; then echo --mac --x64; fi)", + "build": "npm run post-set-shell && electron-builder", "postinstall": "npm run post-set-shell && npm run rebuild" }, "devDependencies": { @@ -21,6 +21,7 @@ "build": { "appId": "cc.arduino.micropython-lab", "artifactName": "${productName}-${os}_${arch}.${ext}", + "extraResources": "./ui/arduino/helpers.py", "mac": { "target": "zip", "icon": "build_resources/icon.icns" diff --git a/preload.js b/preload.js index d7b24e8..3388904 100644 --- a/preload.js +++ b/preload.js @@ -13,10 +13,10 @@ const Serial = { return ports.filter(p => p.vendorId && p.productId) }, connect: async (path) => { - return await board.open(path) + return board.open(path) }, disconnect: async () => { - return await board.close() + return board.close() }, run: async (code) => { return board.run(code) @@ -145,15 +145,22 @@ const Disk = { }, fileExists: async (filePath) => { return ipcRenderer.invoke('file-exists', filePath) + }, + getAppPath: () => { + return ipcRenderer.invoke('get-app-path') } } const Window = { setWindowSize: (minWidth, minHeight) => { ipcRenderer.invoke('set-window-size', minWidth, minHeight) - } + }, + beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), + confirmClose: () => ipcRenderer.invoke('confirm-close'), + isPackaged: () => ipcRenderer.invoke('is-packaged') } + contextBridge.exposeInMainWorld('BridgeSerial', Serial) contextBridge.exposeInMainWorld('BridgeDisk', Disk) contextBridge.exposeInMainWorld('BridgeWindow', Window) diff --git a/ui/arduino/index.html b/ui/arduino/index.html index 25f1967..8478cc7 100644 --- a/ui/arduino/index.html +++ b/ui/arduino/index.html @@ -30,6 +30,7 @@ + diff --git a/ui/arduino/main.js b/ui/arduino/main.js index f7d1ef7..bf693df 100644 --- a/ui/arduino/main.js +++ b/ui/arduino/main.js @@ -19,27 +19,26 @@ function App(state, emit) { ` } - let overlay = html`
` - - if (state.diskFiles == null) { - emit('load-disk-files') - overlay = html`

Loading files...

` + if (state.view == 'file-manager') { + return html` +
+ ${FileManagerView(state, emit)} + ${Overlay(state, emit)} +
+ ` + } else { + return html` +
+ ${EditorView(state, emit)} + ${Overlay(state, emit)} +
+ ` } - - if (state.isRemoving) overlay = html`

Removing...

` - if (state.isConnecting) overlay = html`

Connecting...

` - if (state.isLoadingFiles) overlay = html`

Loading files...

` - if (state.isSaving) overlay = html`

Saving file... ${state.savingProgress}

` - if (state.isTransferring) overlay = html`

Transferring file... ${state.transferringProgress}

` - - const view = state.view == 'editor' ? EditorView(state, emit) : FileManagerView(state, emit) return html`
- ${view} - ${overlay} + ${Overlay(state, emit)}
` - } window.addEventListener('load', () => { @@ -49,7 +48,9 @@ window.addEventListener('load', () => { app.mount('#app') app.emitter.on('DOMContentLoaded', () => { - app.emitter.emit('refresh-files') + if (app.state.diskNavigationRoot) { + app.emitter.emit('refresh-files') + } }) }) diff --git a/ui/arduino/splash.html b/ui/arduino/splash.html new file mode 100644 index 0000000..15ae0b4 --- /dev/null +++ b/ui/arduino/splash.html @@ -0,0 +1,21 @@ + + + + + Arduino Lab for MicroPython + + + + Arduino Lab For MicroPython Logo + + diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 80656a5..f008c4b 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1021,17 +1021,19 @@ async function store(state, emitter) { emitter.on('toggle-file-selection', (file, source, event) => { log('toggle-file-selection', file, source, event) + let parentFolder = source == 'board' ? state.boardNavigationPath : state.diskNavigationPath // Single file selection unless holding keyboard key if (event && !event.ctrlKey && !event.metaKey) { state.selectedFiles = [{ fileName: file.fileName, type: file.type, source: source, - parentFolder: file.parentFolder + parentFolder: parentFolder }] emitter.emit('render') return } + const isSelected = state.selectedFiles.find((f) => { return f.fileName === file.fileName && f.source === source }) @@ -1044,79 +1046,89 @@ async function store(state, emitter) { fileName: file.fileName, type: file.type, source: source, - parentFolder: file.parentFolder + parentFolder: parentFolder }) } emitter.emit('render') }) emitter.on('open-selected-files', async () => { log('open-selected-files') - let files = [] + let filesToOpen = [] + let filesAlreadyOpen = [] for (let i in state.selectedFiles) { let selectedFile = state.selectedFiles[i] - let openFile = null if (selectedFile.type == 'folder') { // Don't open folders continue } - if (selectedFile.source == 'board') { - const fileContent = await serial.loadFile( - serial.getFullPath( - '/', - state.boardNavigationPath, - selectedFile.fileName + // ALl good until here + + const alreadyOpen = state.openFiles.find((f) => { + return f.fileName == selectedFile.fileName + && f.source == selectedFile.source + && f.parentFolder == selectedFile.parentFolder + }) + console.log('already open', alreadyOpen) + + if (!alreadyOpen) { + // This file is not open yet, + // load content and append it to the list of files to open + let file = null + if (selectedFile.source == 'board') { + const fileContent = await serial.loadFile( + serial.getFullPath( + state.boardNavigationRoot, + state.boardNavigationPath, + selectedFile.fileName + ) ) - ) - openFile = createFile({ - parentFolder: state.boardNavigationPath, - fileName: selectedFile.fileName, - source: selectedFile.source, - content: fileContent - }) - openFile.editor.onChange = function() { - openFile.hasChanges = true - emitter.emit('render') - } - } else if (selectedFile.source == 'disk') { - const fileContent = await disk.loadFile( - disk.getFullPath( - state.diskNavigationRoot, - state.diskNavigationPath, - selectedFile.fileName + file = createFile({ + parentFolder: state.boardNavigationPath, + fileName: selectedFile.fileName, + source: selectedFile.source, + content: fileContent + }) + file.editor.onChange = function() { + file.hasChanges = true + emitter.emit('render') + } + } else if (selectedFile.source == 'disk') { + const fileContent = await disk.loadFile( + disk.getFullPath( + state.diskNavigationRoot, + state.diskNavigationPath, + selectedFile.fileName + ) ) - ) - openFile = createFile({ - parentFolder: state.diskNavigationPath, - fileName: selectedFile.fileName, - source: selectedFile.source, - content: fileContent - }) - openFile.editor.onChange = function() { - openFile.hasChanges = true - emitter.emit('render') + file = createFile({ + parentFolder: state.diskNavigationPath, + fileName: selectedFile.fileName, + source: selectedFile.source, + content: fileContent + }) + file.editor.onChange = function() { + file.hasChanges = true + emitter.emit('render') + } } + filesToOpen.push(file) + } else { + // This file is already open, + // append it to the list of files that are already open + filesAlreadyOpen.push(alreadyOpen) } - files.push(openFile) } - files = files.filter((f) => { // find files to open - let isAlready = false - state.openFiles.forEach((g) => { // check if file is already open - if ( - g.fileName == f.fileName - && g.source == f.source - && g.parentFolder == f.parentFolder - ) { - isAlready = true - } - }) - return !isAlready - }) - - if (files.length > 0) { - state.openFiles = state.openFiles.concat(files) - state.editingFile = files[0].id + // If opening an already open file, switch to its tab + if (filesAlreadyOpen.length > 0) { + state.editingFile = filesAlreadyOpen[0].id } + // If there are new files to open, they take priority + if (filesToOpen.length > 0) { + state.editingFile = filesToOpen[0].id + } + + state.openFiles = state.openFiles.concat(filesToOpen) state.view = 'editor' emitter.emit('render') @@ -1303,6 +1315,15 @@ async function store(state, emitter) { emitter.emit('render') }) + win.beforeClose(async () => { + const hasChanges = !!state.openFiles.find(f => f.parentFolder && f.hasChanges) + if (hasChanges) { + const response = await confirm('You may have unsaved changes. Are you sure you want to proceed?', 'Yes', 'Cancel') + if (!response) return false + } + await win.confirmClose() + }) + function createFile(args) { const { source, @@ -1467,26 +1488,9 @@ function canEdit({ selectedFiles }) { return files.length != 0 } -function toggleFileSelection({ fileName, source, selectedFiles }) { - let result = [] - let file = selectedFiles.find((f) => { - return f.fileName === fileName && f.source === source - }) - if (file) { - // filter file out - result = selectedFiles.filter((f) => { - return f.fileName !== fileName && f.source !== source - }) - } else { - // push file - selectedFiles.push({ fileName, source }) - } - return result -} - async function removeBoardFolder(fullPath) { // TODO: Replace with getting the file tree from the board and deleting one by one - let output = await serial.execFile('./ui/arduino/helpers.py') + let output = await serial.execFile(await getHelperFullPath()) await serial.run(`delete_folder('${fullPath}')`) } @@ -1518,7 +1522,7 @@ async function uploadFolder(srcPath, destPath, dataConsumer) { async function downloadFolder(srcPath, destPath, dataConsumer) { dataConsumer = dataConsumer || function() {} await disk.createFolder(destPath) - let output = await serial.execFile('./ui/arduino/helpers.py') + let output = await serial.execFile(await getHelperFullPath()) output = await serial.run(`ilist_all('${srcPath}')`) let files = [] try { @@ -1546,3 +1550,20 @@ async function downloadFolder(srcPath, destPath, dataConsumer) { } } } + +async function getHelperFullPath() { + const appPath = await disk.getAppPath() + if (await win.isPackaged()) { + return disk.getFullPath( + appPath, + '..', + 'ui/arduino/helpers.py' + ) + } else { + return disk.getFullPath( + appPath, + 'ui/arduino/helpers.py', + '' + ) + } +} diff --git a/ui/arduino/views/components/elements/editor.js b/ui/arduino/views/components/elements/editor.js index 81d6fcb..091c6fa 100644 --- a/ui/arduino/views/components/elements/editor.js +++ b/ui/arduino/views/components/elements/editor.js @@ -3,26 +3,43 @@ class CodeMirrorEditor extends Component { super() this.editor = null this.content = '# empty file' + this.scrollTop = 0 } + createElement(content) { + if (content) this.content = content + return html`
` + } + + load(el) { const onCodeChange = (update) => { - // console.log('code change', this.content) this.content = update.state.doc.toString() this.onChange() } this.editor = createEditor(this.content, el, onCodeChange) - } - createElement(content) { - if (content) this.content = content - return html`
` + setTimeout(() => { + this.editor.scrollDOM.addEventListener('scroll', this.updateScrollPosition.bind(this)) + this.editor.scrollDOM.scrollTo({ + top: this.scrollTop, + left: 0 + }) + }, 10) } update() { return false } + unload() { + this.editor.scrollDOM.removeEventListener('scroll', this.updateScrollPosition) + } + + updateScrollPosition(e) { + this.scrollTop = e.target.scrollTop + } + onChange() { return false } diff --git a/ui/arduino/views/components/elements/tab.js b/ui/arduino/views/components/elements/tab.js index f0070f3..6036d6b 100644 --- a/ui/arduino/views/components/elements/tab.js +++ b/ui/arduino/views/components/elements/tab.js @@ -57,7 +57,7 @@ function Tab(args) { } function selectTab(e) { - if(e.target.tagName === 'BUTTON' || e.target.tagName === 'IMG') return + if(e.target.classList.contains('close-tab')) return onSelectTab(e) } @@ -71,9 +71,9 @@ function Tab(args) {
${hasChanges ? '*' : ''} ${text}
-
-
diff --git a/ui/arduino/views/components/overlay.js b/ui/arduino/views/components/overlay.js new file mode 100644 index 0000000..1b9389c --- /dev/null +++ b/ui/arduino/views/components/overlay.js @@ -0,0 +1,16 @@ +function Overlay(state, emit) { + let overlay = html`
` + + if (state.diskFiles == null) { + emit('load-disk-files') + overlay = html`

Loading files...

` + } + + if (state.isRemoving) overlay = html`

Removing...

` + if (state.isConnecting) overlay = html`

Connecting...

` + if (state.isLoadingFiles) overlay = html`

Loading files...

` + if (state.isSaving) overlay = html`

Saving file... ${state.savingProgress}

` + if (state.isTransferring) overlay = html`

Transferring file... ${state.transferringProgress}

` + + return overlay +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy