diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9db57aa3..24a0b30aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,15 +15,15 @@ on: env: JOB_TRANSFER_ARTIFACT: build-artifacts + CHANGELOG_ARTIFACTS: changelog jobs: - build: if: github.repository == 'arduino/arduino-ide' strategy: matrix: config: - - os: windows-latest + - os: windows-2019 - os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259 - os: macos-latest runs-on: ${{ matrix.config.os }} @@ -33,16 +33,16 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install Node.js 12.x + - name: Install Node.js 14.x uses: actions/setup-node@v1 with: - node-version: '12.14.1' + node-version: '14.x' registry-url: 'https://registry.npmjs.org' - - name: Install Python 2.7 + - name: Install Python 3.x uses: actions/setup-python@v2 with: - python-version: '2.7' + python-version: '3.x' - name: Package shell: bash @@ -50,35 +50,36 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} AC_USERNAME: ${{ secrets.AC_USERNAME }} AC_PASSWORD: ${{ secrets.AC_PASSWORD }} + AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }} IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }} IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }} run: | - # See: https://www.electron.build/code-signing - if [ $IS_FORK = true ]; then - echo "Skipping the app signing: building from a fork." - else - if [ "${{ runner.OS }}" = "macOS" ]; then - export CSC_LINK="${{ runner.temp }}/signing_certificate.p12" - # APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from: - # https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate - echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK" - - export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}" - - elif [ "${{ runner.OS }}" = "Windows" ]; then - export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx" - npm config set msvs_version 2017 --global - echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK" - - export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}" - fi + # See: https://www.electron.build/code-signing + if [ $IS_FORK = true ]; then + echo "Skipping the app signing: building from a fork." + else + if [ "${{ runner.OS }}" = "macOS" ]; then + export CSC_LINK="${{ runner.temp }}/signing_certificate.p12" + # APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from: + # https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate + echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK" + + export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}" + + elif [ "${{ runner.OS }}" = "Windows" ]; then + export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx" + npm config set msvs_version 2017 --global + echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK" + + export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}" fi + fi - yarn --cwd ./electron/packager/ - yarn --cwd ./electron/packager/ package + yarn --cwd ./electron/packager/ + yarn --cwd ./electron/packager/ package - name: Upload [GitHub Actions] uses: actions/upload-artifact@v2 @@ -95,15 +96,19 @@ jobs: strategy: matrix: artifact: - - path: "*Linux_64bit.zip" - name: Linux_X86-64 - - path: "*macOS_64bit.dmg" - name: macOS - - path: "*Windows_64bit.exe" + - path: '*Linux_64bit.zip' + name: Linux_X86-64_zip + - path: '*Linux_64bit.AppImage' + name: Linux_X86-64_app_image + - path: '*macOS_64bit.dmg' + name: macOS_dmg + - path: '*macOS_64bit.zip' + name: macOS_zip + - path: '*Windows_64bit.exe' name: Windows_X86-64_interactive_installer - - path: "*Windows_64bit.msi" + - path: '*Windows_64bit.msi' name: Windows_X86-64_MSI - - path: "*Windows_64bit.zip" + - path: '*Windows_64bit.zip' name: Windows_X86-64_zip steps: @@ -112,7 +117,7 @@ jobs: with: name: ${{ env.JOB_TRANSFER_ARTIFACT }} path: ${{ env.JOB_TRANSFER_ARTIFACT }} - + - name: Upload tester build artifact uses: actions/upload-artifact@v2 with: @@ -135,24 +140,24 @@ jobs: env: IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }} run: | - export LATEST_TAG=$(git describe --abbrev=0) - export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g') - if [ "$IS_RELEASE" = true ]; then - export BODY=$(echo -e "$GIT_LOG") - else - export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)") - if [ -z "$GIT_LOG" ]; then - export BODY="There were no changes since version $LATEST_TAG_WITH_LINK." - else - export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG") - fi + export LATEST_TAG=$(git describe --abbrev=0) + export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g') + if [ "$IS_RELEASE" = true ]; then + export BODY=$(echo -e "$GIT_LOG") + else + export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)") + if [ -z "$GIT_LOG" ]; then + export BODY="There were no changes since version $LATEST_TAG_WITH_LINK." + else + export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG") fi - echo -e "$BODY" - OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}" - OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}" - OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}" - echo "::set-output name=BODY::$OUTPUT_SAFE_BODY" - echo "$BODY" > CHANGELOG.txt + fi + echo -e "$BODY" + OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}" + OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}" + OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}" + echo "::set-output name=BODY::$OUTPUT_SAFE_BODY" + echo "$BODY" > CHANGELOG.txt - name: Upload Changelog [GitHub Actions] if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') @@ -175,9 +180,9 @@ jobs: - name: Publish Nightly [S3] uses: docker://plugins/s3 env: - PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*" - PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/" - PLUGIN_TARGET: "/arduino-ide/nightly" + PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*' + PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/' + PLUGIN_TARGET: '/arduino-ide/nightly' PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -211,9 +216,9 @@ jobs: - name: Publish Release [S3] uses: docker://plugins/s3 env: - PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*" - PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/" - PLUGIN_TARGET: "/arduino-ide" + PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*' + PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/' + PLUGIN_TARGET: '/arduino-ide' PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/check-i18n-task.yml b/.github/workflows/check-i18n-task.yml index 121a9a844..e8c01a8b6 100644 --- a/.github/workflows/check-i18n-task.yml +++ b/.github/workflows/check-i18n-task.yml @@ -25,10 +25,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Install Node.js 12.x + - name: Install Node.js 14.x uses: actions/setup-node@v2 with: - node-version: '12.14.1' + node-version: '14.x' registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.github/workflows/compose-full-changelog.yaml b/.github/workflows/compose-full-changelog.yaml new file mode 100644 index 000000000..987841fcc --- /dev/null +++ b/.github/workflows/compose-full-changelog.yaml @@ -0,0 +1,45 @@ +name: Compose full changelog + +on: + release: + types: [created, edited] + +env: + CHANGELOG_ARTIFACTS: changelog + +jobs: + create-changelog: + if: github.repository == 'arduino/arduino-ide' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Get Tag + id: tag_name + run: | + echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/} + + - name: Create full changelog + id: full-changelog + run: | + mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}" + + # Get the changelog file name to build + CHANGELOG_FILE_NAME="${{ steps.tag_name.outputs.TAG_NAME }}-$(date --iso-8601=s).md" + + # Create manifest file pointing to latest changelog file name + echo "$CHANGELOG_FILE_NAME" >> "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/latest.txt" + + # Compose changelog + yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME" + + - name: Publish Changelog [S3] + uses: docker://plugins/s3 + env: + PLUGIN_SOURCE: '${{ env.CHANGELOG_ARTIFACTS }}/*' + PLUGIN_STRIP_PREFIX: '${{ env.CHANGELOG_ARTIFACTS }}/' + PLUGIN_TARGET: '/arduino-ide/changelog' + PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/i18n-nightly-push.yml b/.github/workflows/i18n-nightly-push.yml index c62f16a7f..670cf3184 100644 --- a/.github/workflows/i18n-nightly-push.yml +++ b/.github/workflows/i18n-nightly-push.yml @@ -12,10 +12,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install Node.js 12.x + - name: Install Node.js 14.x uses: actions/setup-node@v2 with: - node-version: '12.14.1' + node-version: '14.x' registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.github/workflows/i18n-weekly-pull.yml b/.github/workflows/i18n-weekly-pull.yml index 1a361febe..d6db2312c 100644 --- a/.github/workflows/i18n-weekly-pull.yml +++ b/.github/workflows/i18n-weekly-pull.yml @@ -12,10 +12,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Install Node.js 12.x + - name: Install Node.js 14.x uses: actions/setup-node@v2 with: - node-version: '12.14.1' + node-version: '14.x' registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c336c081..d6ed25954 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,13 @@ "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" }, + { + "type": "chrome", + "request": "attach", + "name": "Attach to Electron Frontend", + "port": 9222, + "webRoot": "${workspaceFolder}/electron-app" + }, { "type": "node", "request": "launch", @@ -104,5 +111,14 @@ "program": "${workspaceRoot}/electron/packager/index.js", "cwd": "${workspaceFolder}/electron/packager" } + ], + "compounds": [ + { + "name": "Launch Electron Backend & Frontend", + "configurations": [ + "App (Electron)", + "Attach to Electron Frontend" + ] + } ] } diff --git a/BUILDING.md b/BUILDING.md index 27d1cbfe2..a9e10f47e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -40,22 +40,31 @@ The _frontend_ is running as an Electron renderer process and can invoke service ## Build from source If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the -project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions. +project, you should be able to build the Arduino IDE locally. +Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions. +> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above. -### Build -```sh -yarn -``` +Once you have all the tools installed, you can build the editor following these steps -### Rebuild the native dependencies -```sh -yarn rebuild:electron -``` +1. Install the dependencies and build + ```sh + yarn + ``` -### Start -```sh -yarn start -``` +2. Rebuild the dependencies + ```sh + yarn rebuild:browser + ``` + +3. Rebuild the electron dependencies + ```sh + yarn rebuild:electron + ``` + +4. Start the application + ```sh + yarn start + ``` ### CI @@ -117,7 +126,7 @@ git add . \ git tag -a 0.2.0 -m "0.2.0" \ && git push origin 0.2.0 ``` - - The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases). + - The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases). - If you do not want to release the `EXE` and `MSI` installers, wipe them manually. - If you do not like the generated changelog, modify it and update the GH release. diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index f1e56c4ea..38cec42c6 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -1,11 +1,12 @@ { "name": "arduino-ide-extension", - "version": "2.0.0-rc3", + "version": "2.0.0-rc4", "description": "An extension for Theia building the Arduino IDE", "license": "AGPL-3.0-or-later", "scripts": { "prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-serial-plotter && yarn clean && yarn download-examples && yarn build && yarn test", "clean": "rimraf lib", + "compose-changelog": "node ./scripts/compose-changelog.js", "download-cli": "node ./scripts/download-cli.js", "download-fwuploader": "node ./scripts/download-fwuploader.js", "copy-serial-plotter": "npx ncp ../node_modules/arduino-serial-plotter-webapp ./build/arduino-serial-plotter-webapp", @@ -20,22 +21,23 @@ }, "dependencies": { "@grpc/grpc-js": "^1.3.7", - "@theia/application-package": "1.19.0", - "@theia/core": "1.19.0", - "@theia/editor": "1.19.0", - "@theia/editor-preview": "1.19.0", - "@theia/filesystem": "1.19.0", - "@theia/git": "1.19.0", - "@theia/keymaps": "1.19.0", - "@theia/markers": "1.19.0", - "@theia/monaco": "1.19.0", - "@theia/navigator": "1.19.0", - "@theia/outline-view": "1.19.0", - "@theia/output": "1.19.0", - "@theia/preferences": "1.19.0", - "@theia/search-in-workspace": "1.19.0", - "@theia/terminal": "1.19.0", - "@theia/workspace": "1.19.0", + "@theia/application-package": "1.22.1", + "@theia/core": "1.22.1", + "@theia/editor": "1.22.1", + "@theia/editor-preview": "1.22.1", + "@theia/electron": "1.22.1", + "@theia/filesystem": "1.22.1", + "@theia/git": "1.22.1", + "@theia/keymaps": "1.22.1", + "@theia/markers": "1.22.1", + "@theia/monaco": "1.22.1", + "@theia/navigator": "1.22.1", + "@theia/outline-view": "1.22.1", + "@theia/output": "1.22.1", + "@theia/preferences": "1.22.1", + "@theia/search-in-workspace": "1.22.1", + "@theia/terminal": "1.22.1", + "@theia/workspace": "1.22.1", "@tippyjs/react": "^4.2.5", "@types/atob": "^2.1.2", "@types/auth0-js": "^9.14.0", @@ -63,6 +65,7 @@ "css-element-queries": "^1.2.0", "dateformat": "^3.0.3", "deepmerge": "2.0.1", + "electron-updater": "^4.6.5", "fuzzy": "^0.1.3", "glob": "^7.1.6", "google-protobuf": "^3.11.4", @@ -80,6 +83,7 @@ "ps-tree": "^1.2.0", "query-string": "^7.0.1", "react-disable": "^0.1.0", + "react-markdown": "^8.0.0", "react-select": "^3.0.4", "react-tabs": "^3.1.2", "react-window": "^1.8.6", @@ -92,6 +96,7 @@ "which": "^1.3.1" }, "devDependencies": { + "@octokit/rest": "^18.12.0", "@types/chai": "^4.2.7", "@types/chai-string": "^1.4.2", "@types/mocha": "^5.2.7", @@ -151,10 +156,16 @@ ], "arduino": { "cli": { - "version": "0.20.2" + "version": "0.21.0" }, "fwuploader": { "version": "2.0.0" + }, + "clangd": { + "version": "13.0.0" + }, + "languageServer": { + "version": "0.6.0" } } -} +} \ No newline at end of file diff --git a/arduino-ide-extension/scripts/compose-changelog.js b/arduino-ide-extension/scripts/compose-changelog.js new file mode 100755 index 000000000..aba9dae46 --- /dev/null +++ b/arduino-ide-extension/scripts/compose-changelog.js @@ -0,0 +1,116 @@ +// @ts-check + +(async () => { + const { Octokit } = require('@octokit/rest'); + const fs = require('fs'); + const path = require('path'); + + const octokit = new Octokit({ + userAgent: 'Arduino IDE compose-changelog.js', + }); + + const response = await octokit.rest.repos + .listReleases({ + owner: 'arduino', + repo: 'arduino-ide', + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); + + const releases = response.data; + + let fullChangelog = releases.reduce((acc, item, index) => { + // Process each line separately + const body = item.body.split('\n').map(processLine).join('\n'); + // item.name is the name of the release changelog + return ( + acc + + `## ${item.name}\n\n${body}${ + index !== releases.length - 1 ? '\n\n---\n\n' : '\n' + }` + ); + }, ''); + + const args = process.argv.slice(2); + if (args.length == 0) { + console.error('Missing argument to destination file'); + process.exit(1); + } + const changelogFile = path.resolve(args[0]); + + await fs.writeFile( + changelogFile, + fullChangelog, + { + flag: 'w+', + }, + (err) => { + if (err) { + console.error(err); + process.exit(1); + } + console.log('Changelog written to', changelogFile); + } + ); +})(); + +// processLine applies different substitutions to line string. +// We're assuming that there are no more than one substitution +// per line to be applied. +const processLine = (line) => { + // Check if a link with one of the following format exists: + // * [#123](https://github.com/arduino/arduino-ide/pull/123) + // * [#123](https://github.com/arduino/arduino-ide/issues/123) + // * [#123](https://github.com/arduino/arduino-ide/pull/123/) + // * [#123](https://github.com/arduino/arduino-ide/issues/123/) + // If it does return the line as is. + let r = + /(\(|\[)#\d+(\)|\])(\(|\[)https:\/\/fanyv88.com:443\/https\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?(\)|\])/gm; + if (r.test(line)) { + return line; + } + + // Check if a issue or PR link with the following format exists: + // * #123 + // If it does it's changed to: + // * [#123](https://github.com/arduino/arduino-ide/pull/123) + r = /(? { - const DEFAULT_ALS_VERSION = '0.5.0'; - const DEFAULT_CLANGD_VERSION = 'snapshot_20210124'; - const path = require('path'); const shell = require('shelljs'); const downloader = require('./downloader'); + const [DEFAULT_ALS_VERSION, DEFAULT_CLANGD_VERSION] = (() => { + const pkg = require(path.join(__dirname, '..', 'package.json')); + if (!pkg) return undefined; + + const { arduino } = pkg; + if (!arduino) return undefined; + + const { languageServer, clangd } = arduino; + if (!languageServer) return undefined; + if (!clangd) return undefined; + + return [languageServer.version, clangd.version]; + })(); + + if (!DEFAULT_ALS_VERSION) { + shell.echo( + `Could not retrieve Arduino Language Server version info from the 'package.json'.` + ); + shell.exit(1); + } + + if (!DEFAULT_CLANGD_VERSION) { + shell.echo( + `Could not retrieve clangd version info from the 'package.json'.` + ); + shell.exit(1); + } + const yargs = require('yargs') .option('ls-version', { alias: 'lv', @@ -20,7 +45,7 @@ .option('clangd-version', { alias: 'cv', default: DEFAULT_CLANGD_VERSION, - choices: ['snapshot_20210124'], + choices: [DEFAULT_CLANGD_VERSION, 'snapshot_20210124'], describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`, }) .option('force-download', { @@ -35,32 +60,32 @@ const clangdVersion = yargs['clangd-version']; const force = yargs['force-download']; const { platform, arch } = process; - + const platformArch = platform + '-' + arch; const build = path.join(__dirname, '..', 'build'); const lsExecutablePath = path.join( build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}` ); + let clangdExecutablePath, lsSuffix, clangdSuffix; - let clangdExecutablePath, lsSuffix, clangdPrefix; - switch (platform) { - case 'darwin': - clangdExecutablePath = path.join(build, 'bin', 'clangd'); + switch (platformArch) { + case 'darwin-x64': + clangdExecutablePath = path.join(build, 'clangd'); lsSuffix = 'macOS_64bit.tar.gz'; - clangdPrefix = 'mac'; + clangdSuffix = 'macOS_64bit'; break; - case 'linux': - clangdExecutablePath = path.join(build, 'bin', 'clangd'); + case 'linux-x64': + clangdExecutablePath = path.join(build, 'clangd'); lsSuffix = 'Linux_64bit.tar.gz'; - clangdPrefix = 'linux'; + clangdSuffix = 'Linux_64bit'; break; - case 'win32': - clangdExecutablePath = path.join(build, 'bin', 'clangd.exe'); + case 'win32-x64': + clangdExecutablePath = path.join(build, 'clangd.exe'); lsSuffix = 'Windows_64bit.zip'; - clangdPrefix = 'windows'; + clangdSuffix = 'Windows_64bit'; break; } - if (!lsSuffix) { + if (!lsSuffix || !clangdSuffix) { shell.echo( `The arduino-language-server is not available for ${platform} ${arch}.` ); @@ -74,7 +99,7 @@ }_${lsSuffix}`; downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force); - const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd-${clangdPrefix}-${clangdVersion}.zip`; + const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`; downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, { strip: 1, }); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder. diff --git a/arduino-ide-extension/scripts/downloader.js b/arduino-ide-extension/scripts/downloader.js index 6e81d480d..775aa0435 100644 --- a/arduino-ide-extension/scripts/downloader.js +++ b/arduino-ide-extension/scripts/downloader.js @@ -5,16 +5,17 @@ const download = require('download'); const decompress = require('decompress'); const unzip = require('decompress-unzip'); const untargz = require('decompress-targz'); +const untarbz2 = require('decompress-tarbz2'); process.on('unhandledRejection', (reason, _) => { - shell.echo(String(reason)); - shell.exit(1); - throw reason; + shell.echo(String(reason)); + shell.exit(1); + throw reason; }); -process.on('uncaughtException', error => { - shell.echo(String(error)); - shell.exit(1); - throw error; +process.on('uncaughtException', (error) => { + shell.echo(String(error)); + shell.exit(1); + throw error; }); /** @@ -23,55 +24,62 @@ process.on('uncaughtException', error => { * @param filePrefix {string} Prefix of the file name found in the archive * @param force {boolean} Whether to download even if the target file exists. `false` by default. */ -exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) => { - if (fs.existsSync(targetFile) && !force) { - shell.echo(`Skipping download because file already exists: ${targetFile}`); - return; - } - if (!fs.existsSync(path.dirname(targetFile))) { - if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) { - shell.echo('Could not create new directory.'); - shell.exit(1); - } +exports.downloadUnzipFile = async ( + url, + targetFile, + filePrefix, + force = false +) => { + if (fs.existsSync(targetFile) && !force) { + shell.echo(`Skipping download because file already exists: ${targetFile}`); + return; + } + if (!fs.existsSync(path.dirname(targetFile))) { + if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) { + shell.echo('Could not create new directory.'); + shell.exit(1); } + } - const downloads = path.join(__dirname, '..', 'downloads'); - if (shell.rm('-rf', targetFile, downloads).code !== 0) { - shell.exit(1); - } + const downloads = path.join(__dirname, '..', 'downloads'); + if (shell.rm('-rf', targetFile, downloads).code !== 0) { + shell.exit(1); + } - shell.echo(`>>> Downloading from '${url}'...`); - const data = await download(url); - shell.echo(`<<< Download succeeded.`); + shell.echo(`>>> Downloading from '${url}'...`); + const data = await download(url); + shell.echo(`<<< Download succeeded.`); - shell.echo('>>> Decompressing...'); - const files = await decompress(data, downloads, { - plugins: [ - unzip(), - untargz() - ] - }); - if (files.length === 0) { - shell.echo('Error ocurred while decompressing the archive.'); - shell.exit(1); - } - const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix)); - if (fileIndex === -1) { - shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`); - shell.exit(1); - } - shell.echo('<<< Decompressing succeeded.'); + shell.echo('>>> Decompressing...'); + const files = await decompress(data, downloads, { + plugins: [unzip(), untargz(), untarbz2()], + }); + if (files.length === 0) { + shell.echo('Error ocurred while decompressing the archive.'); + shell.exit(1); + } + const fileIndex = files.findIndex((f) => f.path.startsWith(filePrefix)); + if (fileIndex === -1) { + shell.echo( + `The downloaded artifact does not contain any file with prefix ${filePrefix}.` + ); + shell.exit(1); + } + shell.echo('<<< Decompressing succeeded.'); - if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) { - shell.echo(`Could not move file to target path: ${targetFile}`); - shell.exit(1); - } - if (!fs.existsSync(targetFile)) { - shell.echo(`Could not find file: ${targetFile}`); - shell.exit(1); - } - shell.echo(`Done: ${targetFile}`); -} + if ( + shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile) + .code !== 0 + ) { + shell.echo(`Could not move file to target path: ${targetFile}`); + shell.exit(1); + } + if (!fs.existsSync(targetFile)) { + shell.echo(`Could not find file: ${targetFile}`); + shell.exit(1); + } + shell.echo(`Done: ${targetFile}`); +}; /** * @param url {string} Download URL @@ -79,42 +87,45 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) = * @param targetFile {string} Path to the main file expected after decompressing * @param force {boolean} Whether to download even if the target file exists */ -exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressOptions = undefined) => { - if (fs.existsSync(targetFile) && !force) { - shell.echo(`Skipping download because file already exists: ${targetFile}`); - return; - } - if (!fs.existsSync(targetDir)) { - if (shell.mkdir('-p', targetDir).code !== 0) { - shell.echo('Could not create new directory.'); - shell.exit(1); - } +exports.downloadUnzipAll = async ( + url, + targetDir, + targetFile, + force, + decompressOptions = undefined +) => { + if (fs.existsSync(targetFile) && !force) { + shell.echo(`Skipping download because file already exists: ${targetFile}`); + return; + } + if (!fs.existsSync(targetDir)) { + if (shell.mkdir('-p', targetDir).code !== 0) { + shell.echo('Could not create new directory.'); + shell.exit(1); } + } - shell.echo(`>>> Downloading from '${url}'...`); - const data = await download(url); - shell.echo(`<<< Download succeeded.`); + shell.echo(`>>> Downloading from '${url}'...`); + const data = await download(url); + shell.echo(`<<< Download succeeded.`); - shell.echo('>>> Decompressing...'); - let options = { - plugins: [ - unzip(), - untargz() - ] - }; - if (decompressOptions) { - options = Object.assign(options, decompressOptions) - } - const files = await decompress(data, targetDir, options); - if (files.length === 0) { - shell.echo('Error ocurred while decompressing the archive.'); - shell.exit(1); - } - shell.echo('<<< Decompressing succeeded.'); + shell.echo('>>> Decompressing...'); + let options = { + plugins: [unzip(), untargz(), untarbz2()], + }; + if (decompressOptions) { + options = Object.assign(options, decompressOptions); + } + const files = await decompress(data, targetDir, options); + if (files.length === 0) { + shell.echo('Error ocurred while decompressing the archive.'); + shell.exit(1); + } + shell.echo('<<< Decompressing succeeded.'); - if (!fs.existsSync(targetFile)) { - shell.echo(`Could not find file: ${targetFile}`); - shell.exit(1); - } - shell.echo(`Done: ${targetFile}`); -} + if (!fs.existsSync(targetFile)) { + shell.echo(`Could not find file: ${targetFile}`); + shell.exit(1); + } + shell.echo(`Done: ${targetFile}`); +}; diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index b72ba4e29..d9e65fd48 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -1,9 +1,8 @@ import { inject, injectable, postConstruct } from 'inversify'; import * as React from 'react'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { BoardsService, - Port, SketchesService, ExecutableService, Sketch, @@ -69,8 +68,11 @@ import { ArduinoPreferences } from './arduino-preferences'; import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl'; import { SaveAsSketch } from './contributions/save-as-sketch'; import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution'; +import { IDEUpdaterDialog } from './dialogs/ide-updater/ide-updater-dialog'; +import { IDEUpdater } from '../common/protocol/ide-updater'; const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages'; +export const SKIP_IDE_VERSION = 'skipIDEVersion'; @injectable() export class ArduinoFrontendContribution @@ -79,8 +81,7 @@ export class ArduinoFrontendContribution TabBarToolbarContribution, CommandContribution, MenuContribution, - ColorContribution -{ + ColorContribution { @inject(ILogger) protected logger: ILogger; @@ -158,6 +159,12 @@ export class ArduinoFrontendContribution @inject(LocalStorageService) protected readonly localStorageService: LocalStorageService; + @inject(IDEUpdater) + protected readonly updater: IDEUpdater; + + @inject(IDEUpdaterDialog) + protected readonly updaterDialog: IDEUpdaterDialog; + protected invalidConfigPopup: | Promise | undefined; @@ -216,7 +223,7 @@ export class ArduinoFrontendContribution ? nls.localize( 'arduino/common/selectedOn', 'on {0}', - Port.toString(selectedPort) + selectedPort.address ) : nls.localize('arduino/common/notConnected', '[not connected]'), className: 'arduino-selected-port', @@ -252,7 +259,7 @@ export class ArduinoFrontendContribution }); } - onStart(app: FrontendApplication): void { + async onStart(app: FrontendApplication): Promise { // Initialize all `pro-mode` widgets. This is a NOOP if in normal mode. for (const viewContribution of [ this.fileNavigatorContributions, @@ -267,6 +274,31 @@ export class ArduinoFrontendContribution viewContribution.initializeLayout(app); } } + + this.updater + .init( + this.arduinoPreferences.get('arduino.ide.updateChannel'), + this.arduinoPreferences.get('arduino.ide.updateBaseUrl') + ) + .then(() => this.updater.checkForUpdates(true)) + .then(async (updateInfo) => { + if (!updateInfo) return; + const versionToSkip = await this.localStorageService.getData( + SKIP_IDE_VERSION + ); + if (versionToSkip === updateInfo.version) return; + this.updaterDialog.open(updateInfo); + }) + .catch((e) => { + this.messageService.error( + nls.localize( + 'arduino/ide-updater/errorCheckingForUpdates', + 'Error while checking for Arduino IDE updates.\n{0}', + e.message + ) + ); + }); + const start = async ({ selectedBoard }: BoardsConfig.Config) => { if (selectedBoard) { const { name, fqbn } = selectedBoard; @@ -277,11 +309,25 @@ export class ArduinoFrontendContribution }; this.boardsServiceClientImpl.onBoardsConfigChanged(start); this.arduinoPreferences.onPreferenceChanged((event) => { - if ( - event.preferenceName === 'arduino.language.log' && - event.newValue !== event.oldValue - ) { - start(this.boardsServiceClientImpl.boardsConfig); + if (event.newValue !== event.oldValue) { + switch (event.preferenceName) { + case 'arduino.language.log': + start(this.boardsServiceClientImpl.boardsConfig); + break; + case 'arduino.window.zoomLevel': + if (typeof event.newValue === 'number') { + const webContents = remote.getCurrentWebContents(); + webContents.setZoomLevel(event.newValue || 0); + } + break; + case 'arduino.ide.updateChannel': + case 'arduino.ide.updateBaseUrl': + this.updater.init( + this.arduinoPreferences.get('arduino.ide.updateChannel'), + this.arduinoPreferences.get('arduino.ide.updateBaseUrl') + ); + break; + } } }); this.arduinoPreferences.ready.then(() => { @@ -289,16 +335,7 @@ export class ArduinoFrontendContribution const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel'); webContents.setZoomLevel(zoomLevel); }); - this.arduinoPreferences.onPreferenceChanged((event) => { - if ( - event.preferenceName === 'arduino.window.zoomLevel' && - typeof event.newValue === 'number' && - event.newValue !== event.oldValue - ) { - const webContents = remote.getCurrentWebContents(); - webContents.setZoomLevel(event.newValue || 0); - } - }); + app.shell.leftPanelHandler.removeBottomMenu('settings-menu'); } diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 700012bbd..5ceae9179 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -262,6 +262,19 @@ import { UserFieldsDialogWidget, } from './dialogs/user-fields/user-fields-dialog'; import { nls } from '@theia/core/lib/common'; +import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands'; +import { + IDEUpdater, + IDEUpdaterClient, + IDEUpdaterPath, +} from '../common/protocol/ide-updater'; +import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl'; +import { + IDEUpdaterDialog, + IDEUpdaterDialogProps, + IDEUpdaterDialogWidget, +} from './dialogs/ide-updater/ide-updater-dialog'; +import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -407,8 +420,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(SerialService) .toDynamicValue((context) => { const connection = context.container.get(WebSocketConnectionProvider); - const client = - context.container.get(SerialServiceClient); + const client = context.container.get( + SerialServiceClient + ); return connection.createProxy(SerialServicePath, client); }) .inSingletonScope(); @@ -472,12 +486,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { .inSingletonScope(); rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope(); rebind(TabBarToolbarFactory).toFactory( - ({ container: parentContainer }) => - () => { - const container = parentContainer.createChild(); - container.bind(TabBarToolbar).toSelf().inSingletonScope(); - return container.get(TabBarToolbar); - } + ({ container: parentContainer }) => () => { + const container = parentContainer.createChild(); + container.bind(TabBarToolbar).toSelf().inSingletonScope(); + return container.get(TabBarToolbar); + } ); bind(OutputWidget).toSelf().inSingletonScope(); rebind(TheiaOutputWidget).toService(OutputWidget); @@ -642,13 +655,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Enable the dirty indicator on uncloseable widgets. rebind(TabBarRendererFactory).toFactory((context) => () => { - const contextMenuRenderer = - context.container.get(ContextMenuRenderer); + const contextMenuRenderer = context.container.get( + ContextMenuRenderer + ); const decoratorService = context.container.get( TabBarDecoratorService ); - const iconThemeService = - context.container.get(IconThemeService); + const iconThemeService = context.container.get( + IconThemeService + ); return new TabBarRenderer( contextMenuRenderer, decoratorService, @@ -756,9 +771,32 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { title: 'UploadCertificate', }); + bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope(); + bind(IDEUpdaterDialog).toSelf().inSingletonScope(); + bind(IDEUpdaterDialogProps).toConstantValue({ + title: 'IDEUpdater', + }); + bind(UserFieldsDialogWidget).toSelf().inSingletonScope(); bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialogProps).toConstantValue({ title: 'UserFields', }); + + bind(IDEUpdaterCommands).toSelf().inSingletonScope(); + bind(CommandContribution).toService(IDEUpdaterCommands); + + // Frontend binding for the IDE Updater service + bind(IDEUpdaterClientImpl).toSelf().inSingletonScope(); + bind(IDEUpdaterClient).toService(IDEUpdaterClientImpl); + bind(IDEUpdater) + .toDynamicValue((context) => { + const client = context.container.get(IDEUpdaterClientImpl); + return ElectronIpcConnectionProvider.createProxy( + context.container, + IDEUpdaterPath, + client + ); + }) + .inSingletonScope(); }); diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts index 5e1013a1d..ba6bc83c4 100644 --- a/arduino-ide-extension/src/browser/arduino-preferences.ts +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -9,6 +9,11 @@ import { import { nls } from '@theia/core/lib/common'; import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol'; +export enum UpdateChannel { + Stable = 'stable', + Nightly = 'nightly', +} + export const ArduinoConfigSchema: PreferenceSchema = { type: 'object', properties: { @@ -64,13 +69,22 @@ export const ArduinoConfigSchema: PreferenceSchema = { ), default: 0, }, - 'arduino.ide.autoUpdate': { - type: 'boolean', + 'arduino.ide.updateChannel': { + type: 'string', + enum: Object.values(UpdateChannel) as UpdateChannel[], + default: UpdateChannel.Stable, description: nls.localize( - 'arduino/preferences/ide.autoUpdate', - 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.' + 'arduino/preferences/ide.updateChannel', + "Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build." + ), + }, + 'arduino.ide.updateBaseUrl': { + type: 'string', + default: 'https://downloads.arduino.cc/arduino-ide', + description: nls.localize( + 'arduino/preferences/ide.updateBaseUrl', + `The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'` ), - default: true, }, 'arduino.board.certificates': { type: 'string', @@ -171,7 +185,8 @@ export interface ArduinoConfiguration { 'arduino.upload.verify': boolean; 'arduino.window.autoScale': boolean; 'arduino.window.zoomLevel': number; - 'arduino.ide.autoUpdate': boolean; + 'arduino.ide.updateChannel': UpdateChannel; + 'arduino.ide.updateBaseUrl': string; 'arduino.board.certificates': string; 'arduino.sketchbook.showAllFiles': boolean; 'arduino.cloud.enabled': boolean; @@ -188,16 +203,10 @@ export interface ArduinoConfiguration { export const ArduinoPreferences = Symbol('ArduinoPreferences'); export type ArduinoPreferences = PreferenceProxy; -export function createArduinoPreferences( - preferences: PreferenceService -): ArduinoPreferences { - return createPreferenceProxy(preferences, ArduinoConfigSchema); -} - export function bindArduinoPreferences(bind: interfaces.Bind): void { bind(ArduinoPreferences).toDynamicValue((ctx) => { const preferences = ctx.container.get(PreferenceService); - return createArduinoPreferences(preferences); + return createPreferenceProxy(preferences, ArduinoConfigSchema); }); bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema, diff --git a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts index 8158808b4..547e44229 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts @@ -1,5 +1,4 @@ -import { toUnix } from 'upath'; -import URI from '@theia/core/lib/common/uri'; +import { URI } from '@theia/core/shared/vscode-uri'; import { isWindows } from '@theia/core/lib/common/os'; import { notEmpty } from '@theia/core/lib/common/objects'; import { MaybePromise } from '@theia/core/lib/common/types'; @@ -61,12 +60,8 @@ export class ArduinoWorkspaceRootResolver { // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423 protected hashToUri(hash: string | undefined): string | undefined { if (hash && hash.length > 1 && hash.startsWith('#')) { - const path = hash.slice(1); // Trim the leading `#`. - return new URI( - toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)) - ) - .withScheme('file') - .toString(); + const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators + return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString(); } return undefined; } diff --git a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts index 0ef4bad3c..545e8067a 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts +++ b/arduino-ide-extension/src/browser/boards/boards-config-dialog.ts @@ -84,7 +84,7 @@ export class BoardsConfigDialog extends AbstractDialog { ), nls.localize( 'arduino/board/configDialog2', - 'If you only select a Board you will be able just to compile, but not to upload your sketch.' + 'If you only select a Board you will be able to compile, but not to upload your sketch.' ), ]) { const p = document.createElement('div'); diff --git a/arduino-ide-extension/src/browser/boards/boards-config.tsx b/arduino-ide-extension/src/browser/boards/boards-config.tsx index 6d614745c..392710c6d 100644 --- a/arduino-ide-extension/src/browser/boards/boards-config.tsx +++ b/arduino-ide-extension/src/browser/boards/boards-config.tsx @@ -167,7 +167,7 @@ export class BoardsConfig extends React.Component< this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => { let { selectedPort } = this.state; // If the currently selected port is not available anymore, unset the selected port. - if (removedPorts.some((port) => Port.equals(port, selectedPort))) { + if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) { selectedPort = undefined; } this.setState({ knownPorts, selectedPort }, () => @@ -213,11 +213,11 @@ export class BoardsConfig extends React.Component< } else if (left.protocol === right.protocol) { // We show ports, including those that have guessed // or unrecognized boards, so we must sort those too. - const leftBoard = this.availableBoards.find((board) => - Port.sameAs(board.port, left) + const leftBoard = this.availableBoards.find( + (board) => board.port === left ); - const rightBoard = this.availableBoards.find((board) => - Port.sameAs(board.port, right) + const rightBoard = this.availableBoards.find( + (board) => board.port === right ); if (leftBoard && !rightBoard) { return -1; @@ -348,10 +348,10 @@ export class BoardsConfig extends React.Component<
{ports.map((port) => ( - key={Port.toString(port)} + key={`${port.id}`} item={port} label={Port.toString(port)} - selected={Port.equals(this.state.selectedPort, port)} + selected={Port.sameAs(this.state.selectedPort, port)} onClick={this.selectPort} /> ))} @@ -410,7 +410,7 @@ export namespace BoardsConfig { return options.default; } const { name } = selectedBoard; - return `${name}${port ? ' at ' + Port.toString(port) : ''}`; + return `${name}${port ? ` at ${port.address}` : ''}`; } export function setConfig( diff --git a/arduino-ide-extension/src/browser/boards/boards-data-store.ts b/arduino-ide-extension/src/browser/boards/boards-data-store.ts index fea413eb0..d1a9de5d0 100644 --- a/arduino-ide-extension/src/browser/boards/boards-data-store.ts +++ b/arduino-ide-extension/src/browser/boards/boards-data-store.ts @@ -1,7 +1,6 @@ import { injectable, inject, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; import { deepClone } from '@theia/core/lib/common/objects'; -import { MaybePromise } from '@theia/core/lib/common/types'; import { Event, Emitter } from '@theia/core/lib/common/event'; import { FrontendApplicationContribution, @@ -11,7 +10,6 @@ import { notEmpty } from '../../common/utils'; import { BoardsService, ConfigOption, - Installable, BoardDetails, Programmer, } from '../../common/protocol'; @@ -36,16 +34,12 @@ export class BoardsDataStore implements FrontendApplicationContribution { onStart(): void { this.notificationCenter.onPlatformInstalled(async ({ item }) => { - const { installedVersion: version } = item; - if (!version) { - return; - } let shouldFireChanged = false; for (const fqbn of item.boards .map(({ fqbn }) => fqbn) .filter(notEmpty) .filter((fqbn) => !!fqbn)) { - const key = this.getStorageKey(fqbn, version); + const key = this.getStorageKey(fqbn); let data = await this.storageService.getData< ConfigOption[] | undefined >(key); @@ -72,33 +66,20 @@ export class BoardsDataStore implements FrontendApplicationContribution { async appendConfigToFqbn( fqbn: string | undefined, - boardsPackageVersion: MaybePromise< - Installable.Version | undefined - > = this.getBoardsPackageVersion(fqbn) ): Promise { if (!fqbn) { return undefined; } - - const { configOptions } = await this.getData(fqbn, boardsPackageVersion); + const { configOptions } = await this.getData(fqbn); return ConfigOption.decorate(fqbn, configOptions); } - async getData( - fqbn: string | undefined, - boardsPackageVersion: MaybePromise< - Installable.Version | undefined - > = this.getBoardsPackageVersion(fqbn) - ): Promise { + async getData(fqbn: string | undefined): Promise { if (!fqbn) { return BoardsDataStore.Data.EMPTY; } - const version = await boardsPackageVersion; - if (!version) { - return BoardsDataStore.Data.EMPTY; - } - const key = this.getStorageKey(fqbn, version); + const key = this.getStorageKey(fqbn); let data = await this.storageService.getData< BoardsDataStore.Data | undefined >(key, undefined); @@ -124,25 +105,16 @@ export class BoardsDataStore implements FrontendApplicationContribution { fqbn, selectedProgrammer, }: { fqbn: string; selectedProgrammer: Programmer }, - boardsPackageVersion: MaybePromise< - Installable.Version | undefined - > = this.getBoardsPackageVersion(fqbn) ): Promise { - const data = deepClone(await this.getData(fqbn, boardsPackageVersion)); + const data = deepClone(await this.getData(fqbn)); const { programmers } = data; if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) { return false; } - const version = await boardsPackageVersion; - if (!version) { - return false; - } - await this.setData({ fqbn, data: { ...data, selectedProgrammer }, - version, }); this.fireChanged(); return true; @@ -153,12 +125,9 @@ export class BoardsDataStore implements FrontendApplicationContribution { fqbn, option, selectedValue, - }: { fqbn: string; option: string; selectedValue: string }, - boardsPackageVersion: MaybePromise< - Installable.Version | undefined - > = this.getBoardsPackageVersion(fqbn) + }: { fqbn: string; option: string; selectedValue: string } ): Promise { - const data = deepClone(await this.getData(fqbn, boardsPackageVersion)); + const data = deepClone(await this.getData(fqbn)); const { configOptions } = data; const configOption = configOptions.find((c) => c.option === option); if (!configOption) { @@ -176,12 +145,7 @@ export class BoardsDataStore implements FrontendApplicationContribution { if (!updated) { return false; } - const version = await boardsPackageVersion; - if (!version) { - return false; - } - - await this.setData({ fqbn, data, version }); + await this.setData({ fqbn, data }); this.fireChanged(); return true; } @@ -189,18 +153,16 @@ export class BoardsDataStore implements FrontendApplicationContribution { protected async setData({ fqbn, data, - version, }: { fqbn: string; data: BoardsDataStore.Data; - version: Installable.Version; }): Promise { - const key = this.getStorageKey(fqbn, version); + const key = this.getStorageKey(fqbn); return this.storageService.setData(key, data); } - protected getStorageKey(fqbn: string, version: Installable.Version): string { - return `.arduinoIDE-configOptions-${version}-${fqbn}`; + protected getStorageKey(fqbn: string): string { + return `.arduinoIDE-configOptions-${fqbn}`; } protected async getBoardDetailsSafe( @@ -231,21 +193,6 @@ export class BoardsDataStore implements FrontendApplicationContribution { protected fireChanged(): void { this.onChangedEmitter.fire(); } - - protected async getBoardsPackageVersion( - fqbn: string | undefined - ): Promise { - if (!fqbn) { - return undefined; - } - const boardsPackage = await this.boardsService.getContainerBoardPackage({ - fqbn, - }); - if (!boardsPackage) { - return undefined; - } - return boardsPackage.installedVersion; - } } export namespace BoardsDataStore { diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index f1edefe14..190d5de3a 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -185,8 +185,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { const selectedAvailableBoard = AvailableBoard.is(selectedBoard) ? selectedBoard : this._availableBoards.find((availableBoard) => - Board.sameAs(availableBoard, selectedBoard) - ); + Board.sameAs(availableBoard, selectedBoard) + ); if ( selectedAvailableBoard && selectedAvailableBoard.selected && @@ -230,7 +230,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { )) { if ( this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn && - this.latestValidBoardsConfig.selectedBoard.name === board.name + this.latestValidBoardsConfig.selectedBoard.name === board.name && + this.latestValidBoardsConfig.selectedPort.protocol === board.port?.protocol ) { this.boardsConfig = { ...this.latestValidBoardsConfig, @@ -244,7 +245,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { } set boardsConfig(config: BoardsConfig.Config) { - this.doSetBoardsConfig(config); + this.setBoardsConfig(config); this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig) @@ -256,7 +257,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { return this._boardsConfig; } - protected doSetBoardsConfig(config: BoardsConfig.Config): void { + protected setBoardsConfig(config: BoardsConfig.Config): void { this.logger.info('Board config changed: ', JSON.stringify(config)); this._boardsConfig = config; this.latestBoardsConfig = this._boardsConfig; @@ -370,19 +371,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) => haystack.find( (board) => - Board.equals(needle, board) && Port.equals(needle.port, board.port) + Board.equals(needle, board) && Port.sameAs(needle.port, board.port) ); const timeoutTask = !!timeout && timeout > 0 ? new Promise((_, reject) => - setTimeout( - () => reject(new Error(`Timeout after ${timeout} ms.`)), - timeout - ) + setTimeout( + () => reject(new Error(`Timeout after ${timeout} ms.`)), + timeout ) + ) : new Promise(() => { - /* never */ - }); + /* never */ + }); const waitUntilTask = new Promise((resolve) => { let candidate = find(what, this.availableBoards); if (candidate) { @@ -409,7 +410,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { Port.sameAs(port, this.boardsConfig.selectedPort) ) ) { - this.doSetBoardsConfig({ + this.setBoardsConfig({ selectedBoard: this.boardsConfig.selectedBoard, selectedPort: undefined, }); @@ -533,9 +534,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { protected getLastSelectedBoardOnPortKey(port: Port | string): string { // TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`. - return `last-selected-board-on-port:${ - typeof port === 'string' ? port : Port.toString(port) - }`; + return `last-selected-board-on-port:${typeof port === 'string' ? port : port.address + }`; } protected async loadState(): Promise { diff --git a/arduino-ide-extension/src/browser/components/ProgressBar.tsx b/arduino-ide-extension/src/browser/components/ProgressBar.tsx new file mode 100644 index 000000000..f91c9f991 --- /dev/null +++ b/arduino-ide-extension/src/browser/components/ProgressBar.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; + +export type ProgressBarProps = { + percent?: number; + showPercentage?: boolean; +}; + +export default function ProgressBar({ + percent = 0, + showPercentage = false, +}: ProgressBarProps): React.ReactElement { + const roundedPercent = Math.round(percent); + return ( +
+
+
+
+ {showPercentage && ( +
+
{roundedPercent}%
+
+ )} +
+ ); +} diff --git a/arduino-ide-extension/src/browser/contributions/about.ts b/arduino-ide-extension/src/browser/contributions/about.ts index 662781a00..3f93adba2 100644 --- a/arduino-ide-extension/src/browser/contributions/about.ts +++ b/arduino-ide-extension/src/browser/contributions/about.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'inversify'; import * as moment from 'moment'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { isOSX, isWindows } from '@theia/core/lib/common/os'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; diff --git a/arduino-ide-extension/src/browser/contributions/add-file.ts b/arduino-ide-extension/src/browser/contributions/add-file.ts index d6155d927..94316a1f4 100644 --- a/arduino-ide-extension/src/browser/contributions/add-file.ts +++ b/arduino-ide-extension/src/browser/contributions/add-file.ts @@ -1,5 +1,5 @@ import { inject, injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { ArduinoMenus } from '../menu/arduino-menus'; import { SketchContribution, diff --git a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts index 6b97c3ec2..a03d056f2 100644 --- a/arduino-ide-extension/src/browser/contributions/add-zip-library.ts +++ b/arduino-ide-extension/src/browser/contributions/add-zip-library.ts @@ -1,5 +1,5 @@ import { inject, injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import URI from '@theia/core/lib/common/uri'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; diff --git a/arduino-ide-extension/src/browser/contributions/archive-sketch.ts b/arduino-ide-extension/src/browser/contributions/archive-sketch.ts index 315ad5156..2ab62dc22 100644 --- a/arduino-ide-extension/src/browser/contributions/archive-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/archive-sketch.ts @@ -1,5 +1,5 @@ import { injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import * as dateFormat from 'dateformat'; import URI from '@theia/core/lib/common/uri'; import { ArduinoMenus } from '../menu/arduino-menus'; diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index bcca5a8ce..9dc085fbf 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -1,5 +1,5 @@ import { inject, injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { DisposableCollection, @@ -204,10 +204,9 @@ PID: ${PID}`; const packageLabel = packageName + - `${ - manuallyInstalled - ? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)') - : '' + `${manuallyInstalled + ? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)') + : '' }`; // Platform submenu const platformMenuPath = [...boardsPackagesGroup, packageId]; @@ -255,8 +254,8 @@ PID: ${PID}`; protocolOrder: number, ports: AvailablePorts ) => { - const addresses = Object.keys(ports); - if (!addresses.length) { + const portIDs = Object.keys(ports); + if (!portIDs.length) { return; } @@ -279,27 +278,26 @@ PID: ${PID}`; // First we show addresses with recognized boards connected, // then all the rest. - const sortedAddresses = Object.keys(ports); - sortedAddresses.sort((left: string, right: string): number => { + const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => { const [, leftBoards] = ports[left]; const [, rightBoards] = ports[right]; return rightBoards.length - leftBoards.length; }); - for (let i = 0; i < sortedAddresses.length; i++) { - const address = sortedAddresses[i]; - const [port, boards] = ports[address]; - let label = `${address}`; + for (let i = 0; i < sortedIDs.length; i++) { + const portID = sortedIDs[i]; + const [port, boards] = ports[portID]; + let label = `${port.address}`; if (boards.length) { const boardsList = boards.map((board) => board.name).join(', '); label = `${label} (${boardsList})`; } - const id = `arduino-select-port--${address}`; + const id = `arduino-select-port--${portID}`; const command = { id }; const handler = { execute: () => { if ( - !Port.equals( + !Port.sameAs( port, this.boardsServiceProvider.boardsConfig.selectedPort ) @@ -312,7 +310,7 @@ PID: ${PID}`; } }, isToggled: () => - Port.equals( + Port.sameAs( port, this.boardsServiceProvider.boardsConfig.selectedPort ), diff --git a/arduino-ide-extension/src/browser/contributions/close.ts b/arduino-ide-extension/src/browser/contributions/close.ts index b134873f2..1b335fa82 100644 --- a/arduino-ide-extension/src/browser/contributions/close.ts +++ b/arduino-ide-extension/src/browser/contributions/close.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'inversify'; import { toArray } from '@phosphor/algorithm'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; diff --git a/arduino-ide-extension/src/browser/contributions/help.ts b/arduino-ide-extension/src/browser/contributions/help.ts index 51cbd4a2a..f25c4ba89 100644 --- a/arduino-ide-extension/src/browser/contributions/help.ts +++ b/arduino-ide-extension/src/browser/contributions/help.ts @@ -13,6 +13,7 @@ import { KeybindingRegistry, } from './contribution'; import { nls } from '@theia/core/lib/common'; +import { IDEUpdaterCommands } from '../ide-updater/ide-updater-commands'; @injectable() export class Help extends Contribution { @@ -115,6 +116,10 @@ export class Help extends Contribution { commandId: Help.Commands.VISIT_ARDUINO.id, order: '6', }); + registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, { + commandId: IDEUpdaterCommands.CHECK_FOR_UPDATES.id, + order: '7', + }); } registerKeybindings(registry: KeybindingRegistry): void { diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch-external.ts b/arduino-ide-extension/src/browser/contributions/open-sketch-external.ts index 0f66a27ea..976902588 100644 --- a/arduino-ide-extension/src/browser/contributions/open-sketch-external.ts +++ b/arduino-ide-extension/src/browser/contributions/open-sketch-external.ts @@ -1,5 +1,5 @@ import { injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import URI from '@theia/core/lib/common/uri'; import { ArduinoMenus } from '../menu/arduino-menus'; import { diff --git a/arduino-ide-extension/src/browser/contributions/open-sketch.ts b/arduino-ide-extension/src/browser/contributions/open-sketch.ts index 1c77491ca..879ab144f 100644 --- a/arduino-ide-extension/src/browser/contributions/open-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/open-sketch.ts @@ -1,5 +1,5 @@ import { inject, injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { MaybePromise } from '@theia/core/lib/common/types'; import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser'; import { @@ -190,7 +190,7 @@ export class OpenSketch extends SketchContribution { ], message: nls.localize( 'arduino/sketch/movingMsg', - 'The file "{0}" needs to be inside a sketch folder named as "{1}".\nCreate this folder, move the file, and continue?', + 'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?', nameWithExt, name ), diff --git a/arduino-ide-extension/src/browser/contributions/quit-app.ts b/arduino-ide-extension/src/browser/contributions/quit-app.ts index a37bad9a6..c0e784726 100644 --- a/arduino-ide-extension/src/browser/contributions/quit-app.ts +++ b/arduino-ide-extension/src/browser/contributions/quit-app.ts @@ -1,5 +1,5 @@ import { injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import { isOSX } from '@theia/core/lib/common/os'; import { Contribution, diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts index fbf81f8af..0c265d0c2 100644 --- a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -1,5 +1,5 @@ import { injectable } from 'inversify'; -import { remote } from 'electron'; +import * as remote from '@theia/core/electron-shared/@electron/remote'; import * as dateFormat from 'dateformat'; import { ArduinoMenus } from '../menu/arduino-menus'; import { diff --git a/arduino-ide-extension/src/browser/contributions/upload-firmware.ts b/arduino-ide-extension/src/browser/contributions/upload-firmware.ts index 6b706a551..43af8b14f 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-firmware.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-firmware.ts @@ -45,7 +45,7 @@ export namespace UploadFirmware { id: 'arduino-upload-firmware-open', label: nls.localize( 'arduino/firmware/updater', - 'WiFi101 / WiFiNINA Firmware Updater' + 'WiFi101 / WiFiNINA Firmware Updater' ), category: 'Arduino', }; diff --git a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx index 04a4c8e11..8e5c6d32d 100644 --- a/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/firmware-uploader/firmware-uploader-component.tsx @@ -201,7 +201,7 @@ export const FirmwareUploaderComponent = ({ {nls.localize( 'arduino/firmware/successfullyInstalled', - 'Firmware succesfully installed.' + 'Firmware successfully installed.' )}
)} diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx new file mode 100644 index 000000000..d8b94bd94 --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-component.tsx @@ -0,0 +1,210 @@ +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { nls } from '@theia/core/lib/common'; +import { shell } from 'electron'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import ReactMarkdown from 'react-markdown'; +import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater'; +import ProgressBar from '../../components/ProgressBar'; + +export type IDEUpdaterComponentProps = { + updateInfo: UpdateInfo; + windowService: WindowService; + downloadFinished?: boolean; + downloadStarted?: boolean; + progress?: ProgressInfo; + error?: Error; + onDownload: () => void; + onClose: () => void; + onSkipVersion: () => void; + onCloseAndInstall: () => void; +}; + +export const IDEUpdaterComponent = ({ + updateInfo: { version, releaseNotes }, + downloadStarted = false, + downloadFinished = false, + windowService, + progress, + error, + onDownload, + onClose, + onSkipVersion, + onCloseAndInstall, +}: IDEUpdaterComponentProps): React.ReactElement => { + const changelogDivRef = React.useRef() as React.MutableRefObject< + HTMLDivElement + >; + React.useEffect(() => { + if (!!releaseNotes) { + let changelog: string; + if (typeof releaseNotes === 'string') changelog = releaseNotes; + else + changelog = releaseNotes.reduce((acc, item) => { + return item.note ? (acc += `${item.note}\n\n`) : acc; + }, ''); + ReactDOM.render( + ( + href && shell.openExternal(href)} {...props}> + {children} + + ), + }} + > + {changelog} + , + changelogDivRef.current + ); + } + }, [releaseNotes]); + const closeButton = ( + + ); + + const DownloadCompleted: () => React.ReactElement = () => ( +
+
+ {nls.localize( + 'arduino/ide-updater/versionDownloaded', + 'Arduino IDE {0} has been downloaded.', + version + )} +
+
+ {nls.localize( + 'arduino/ide-updater/closeToInstallNotice', + 'Close the software and install the update on your machine.' + )} +
+
+ {closeButton} + +
+
+ ); + + const DownloadStarted: () => React.ReactElement = () => ( +
+
+ {nls.localize( + 'arduino/ide-updater/downloadingNotice', + 'Downloading the latest version of the Arduino IDE.' + )} +
+ +
+ ); + + const PreDownload: () => React.ReactElement = () => ( +
+
+
+
+
+
+
+ {nls.localize( + 'arduino/ide-updater/updateAvailable', + 'Update Available' + )} +
+
+
+ {nls.localize( + 'arduino/ide-updater/newVersionAvailable', + 'A new version of Arduino IDE ({0}) is available for download.', + version + )} +
+ {releaseNotes && ( +
+
+
+ )} +
+ +
+ {closeButton} + +
+
+
+ ); + + const onGoToDownloadClick = ( + event: React.SyntheticEvent + ) => { + const { target } = event.nativeEvent; + if (target instanceof HTMLAnchorElement) { + event.nativeEvent.preventDefault(); + windowService.openNewWindow(target.href, { external: true }); + onClose(); + } + }; + + const GoToDownloadPage: () => React.ReactElement = () => ( +
+
+ {nls.localize( + 'arduino/ide-updater/goToDownloadPage', + "An update for the Arduino IDE is available, but we're not able to download and install it automatically. Please go to the download page and download the latest version from there." + )} +
+ +
+ ); + + return ( +
+ {!!error ? ( + + ) : downloadFinished ? ( + + ) : downloadStarted ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx new file mode 100644 index 000000000..97642b39d --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import { inject, injectable } from 'inversify'; +import { DialogProps } from '@theia/core/lib/browser/dialogs'; +import { AbstractDialog } from '../../theia/dialogs/dialogs'; +import { Widget } from '@phosphor/widgets'; +import { Message } from '@phosphor/messaging'; +import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; +import { nls } from '@theia/core'; +import { IDEUpdaterComponent } from './ide-updater-component'; + +import { + IDEUpdater, + IDEUpdaterClient, + ProgressInfo, + UpdateInfo, +} from '../../../common/protocol/ide-updater'; +import { LocalStorageService } from '@theia/core/lib/browser'; +import { SKIP_IDE_VERSION } from '../../arduino-frontend-contribution'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; + +@injectable() +export class IDEUpdaterDialogWidget extends ReactWidget { + protected isOpen = new Object(); + updateInfo: UpdateInfo; + progressInfo: ProgressInfo | undefined; + error: Error | undefined; + downloadFinished: boolean; + downloadStarted: boolean; + onClose: () => void; + + @inject(IDEUpdater) + protected readonly updater: IDEUpdater; + + @inject(IDEUpdaterClient) + protected readonly updaterClient: IDEUpdaterClient; + + @inject(LocalStorageService) + protected readonly localStorageService: LocalStorageService; + + @inject(WindowService) + protected windowService: WindowService; + + init(updateInfo: UpdateInfo, onClose: () => void): void { + this.updateInfo = updateInfo; + this.progressInfo = undefined; + this.error = undefined; + this.downloadStarted = false; + this.downloadFinished = false; + this.onClose = onClose; + + this.updaterClient.onError((e) => { + this.error = e; + this.update(); + }); + this.updaterClient.onDownloadProgressChanged((e) => { + this.progressInfo = e; + this.update(); + }); + this.updaterClient.onDownloadFinished((e) => { + this.downloadFinished = true; + this.update(); + }); + } + + async onSkipVersion(): Promise { + this.localStorageService.setData( + SKIP_IDE_VERSION, + this.updateInfo.version + ); + this.close(); + } + + close(): void { + super.close(); + this.onClose(); + } + + onDispose(): void { + if (this.downloadStarted && !this.downloadFinished) + this.updater.stopDownload(); + } + + async onDownload(): Promise { + this.progressInfo = undefined; + this.downloadStarted = true; + this.error = undefined; + this.updater.downloadUpdate(); + this.update(); + } + + onCloseAndInstall(): void { + this.updater.quitAndInstall(); + } + + protected render(): React.ReactNode { + return !!this.updateInfo ? ( +
+ + + ) : null; + } +} + +@injectable() +export class IDEUpdaterDialogProps extends DialogProps {} + +@injectable() +export class IDEUpdaterDialog extends AbstractDialog { + @inject(IDEUpdaterDialogWidget) + protected readonly widget: IDEUpdaterDialogWidget; + + constructor( + @inject(IDEUpdaterDialogProps) + protected readonly props: IDEUpdaterDialogProps + ) { + super({ + title: nls.localize( + 'arduino/ide-updater/ideUpdaterDialog', + 'Software Update' + ), + }); + this.contentNode.classList.add('ide-updater-dialog'); + this.acceptButton = undefined; + } + + get value(): UpdateInfo { + return this.widget.updateInfo; + } + + protected onAfterAttach(msg: Message): void { + if (this.widget.isAttached) { + Widget.detach(this.widget); + } + Widget.attach(this.widget, this.contentNode); + super.onAfterAttach(msg); + this.update(); + } + + async open( + data: UpdateInfo | undefined = undefined + ): Promise { + if (data && data.version) { + this.widget.init(data, this.close.bind(this)); + return super.open(); + } + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.widget.update(); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.widget.activate(); + } + + close(): void { + this.widget.dispose(); + super.close(); + } +} diff --git a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx index 62c166c5e..e7fa7a060 100644 --- a/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/settings/settings-component.tsx @@ -260,18 +260,6 @@ export class SettingsComponent extends React.Component< 'Verify code after upload' )} -