From 4c536ec8fc66ffc9a0d4404c3b91796692929c25 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 16 Apr 2021 16:47:23 +0200 Subject: [PATCH] [atl-1217] sketchbook explorer local & remote --- .vscode/launch.json | 29 +- .vscode/tasks.json | 4 +- Dockerfile | 19 - arduino-ide-extension/package.json | 18 +- .../browser/arduino-frontend-contribution.tsx | 322 ++++------- .../browser/arduino-ide-frontend-module.ts | 66 +++ .../src/browser/arduino-preferences.ts | 66 +++ .../auth/authentication-client-service.ts | 93 +++ .../src/browser/auth/cloud-user-commands.ts | 18 + .../browser/contributions/sketch-control.ts | 139 ++++- .../src/browser/create/create-api.ts | 544 ++++++++++++++++++ .../src/browser/create/create-fs-provider.ts | 212 +++++++ .../src/browser/create/create-paths.ts | 59 ++ .../src/browser/create/create-uri.ts | 39 ++ .../src/browser/data/arduino.color-theme.json | 7 +- .../dialogs.ts/cloud-share-sketch-dialog.tsx | 175 ++++++ .../src/browser/dialogs.ts/dialogs.ts | 69 +++ .../local-cache/local-cache-fs-provider.ts | 167 ++++++ .../src/browser/settings.tsx | 27 + .../src/browser/style/account-icon.svg | 1 + .../style/cloud-sketchbook-tree-icon.svg | 14 + .../src/browser/style/cloud-sketchbook.css | 184 ++++++ .../browser/style/connected-status-icon.svg | 3 + .../src/browser/style/index.css | 10 +- .../src/browser/style/offline-status-icon.svg | 3 + .../src/browser/style/pull-sketch-icon.svg | 8 + .../src/browser/style/push-sketch-icon.svg | 8 + .../src/browser/style/refresh-icon.svg | 3 + .../src/browser/style/sketch-folder-icon.svg | 4 + .../browser/style/sketchbook-opts-icon.svg | 10 + .../browser/style/sketchbook-tree-icon.svg | 11 + .../src/browser/style/sketchbook.css | 38 ++ .../src/browser/style/sketchbook.svg | 3 + .../core/common-frontend-contribution.ts | 13 +- .../theia/workspace/workspace-commands.ts | 82 ++- .../cloud-sketchbook-composite-widget.tsx | 65 +++ .../cloud-sketchbook-contributions.ts | 393 +++++++++++++ .../cloud-sketchbook-tree-container.ts | 29 + .../cloud-sketchbook-tree-model.ts | 166 ++++++ .../cloud-sketchbook-tree-widget.tsx | 184 ++++++ .../cloud-sketchbook/cloud-sketchbook-tree.ts | 523 +++++++++++++++++ .../cloud-sketchbook-widget.ts | 48 ++ .../cloud-sketchbook/cloud-user-status.tsx | 124 ++++ .../widgets/sketchbook/sketchbook-commands.ts | 33 ++ .../sketchbook/sketchbook-tree-container.ts | 26 + .../sketchbook/sketchbook-tree-model.ts | 105 ++++ .../sketchbook/sketchbook-tree-widget.tsx | 168 ++++++ .../widgets/sketchbook/sketchbook-tree.ts | 92 +++ .../sketchbook-widget-contribution.ts | 156 +++++ .../widgets/sketchbook/sketchbook-widget.tsx | 85 +++ .../common/protocol/authentication-service.ts | 30 + .../src/common/protocol/sketches-service.ts | 5 + .../src/node/arduino-ide-backend-module.ts | 30 + .../src/node/auth/arduino-auth-provider.ts | 344 +++++++++++ .../src/node/auth/authentication-server.ts | 67 +++ .../node/auth/authentication-service-impl.ts | 87 +++ arduino-ide-extension/src/node/auth/body.ts | 134 +++++ .../src/node/auth/keychain.ts | 76 +++ arduino-ide-extension/src/node/auth/types.ts | 26 + arduino-ide-extension/src/node/auth/utils.ts | 105 ++++ .../src/node/config-service-impl.ts | 2 +- .../src/node/sketches-service-impl.ts | 4 + docs/README.md | 43 ++ docs/static/preferences.png | Bin 0 -> 85288 bytes docs/static/remote.png | Bin 0 -> 100086 bytes electron/README.md | 36 -- electron/packager/extensions.json | 3 + electron/packager/index.js | 10 +- electron/packager/package.json | 2 +- electron/packager/yarn.lock | 8 +- electron/static/azure-create-gh-release.jpg | Bin 110099 -> 0 bytes electron/static/edit-gh-release-draft.jpg | Bin 31038 -> 0 bytes electron/static/publish-gh-release.jpg | Bin 24183 -> 0 bytes package.json | 2 +- yarn.lock | 310 ++++++++-- 75 files changed, 5559 insertions(+), 430 deletions(-) delete mode 100644 Dockerfile create mode 100644 arduino-ide-extension/src/browser/auth/authentication-client-service.ts create mode 100644 arduino-ide-extension/src/browser/auth/cloud-user-commands.ts create mode 100644 arduino-ide-extension/src/browser/create/create-api.ts create mode 100644 arduino-ide-extension/src/browser/create/create-fs-provider.ts create mode 100644 arduino-ide-extension/src/browser/create/create-paths.ts create mode 100644 arduino-ide-extension/src/browser/create/create-uri.ts create mode 100644 arduino-ide-extension/src/browser/dialogs.ts/cloud-share-sketch-dialog.tsx create mode 100644 arduino-ide-extension/src/browser/dialogs.ts/dialogs.ts create mode 100644 arduino-ide-extension/src/browser/local-cache/local-cache-fs-provider.ts create mode 100644 arduino-ide-extension/src/browser/style/account-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/cloud-sketchbook-tree-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/cloud-sketchbook.css create mode 100644 arduino-ide-extension/src/browser/style/connected-status-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/offline-status-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/pull-sketch-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/push-sketch-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/refresh-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/sketch-folder-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/sketchbook-opts-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/sketchbook-tree-icon.svg create mode 100644 arduino-ide-extension/src/browser/style/sketchbook.css create mode 100644 arduino-ide-extension/src/browser/style/sketchbook.svg create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-composite-widget.tsx create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-contributions.ts create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-container.ts create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-model.ts create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree-widget.tsx create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-widget.ts create mode 100644 arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-user-status.tsx create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-commands.ts create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-container.ts create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-model.ts create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree-widget.tsx create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-tree.ts create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget-contribution.ts create mode 100644 arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx create mode 100644 arduino-ide-extension/src/common/protocol/authentication-service.ts create mode 100644 arduino-ide-extension/src/node/auth/arduino-auth-provider.ts create mode 100644 arduino-ide-extension/src/node/auth/authentication-server.ts create mode 100644 arduino-ide-extension/src/node/auth/authentication-service-impl.ts create mode 100644 arduino-ide-extension/src/node/auth/body.ts create mode 100644 arduino-ide-extension/src/node/auth/keychain.ts create mode 100644 arduino-ide-extension/src/node/auth/types.ts create mode 100644 arduino-ide-extension/src/node/auth/utils.ts create mode 100644 docs/README.md create mode 100644 docs/static/preferences.png create mode 100644 docs/static/remote.png create mode 100644 electron/packager/extensions.json delete mode 100644 electron/static/azure-create-gh-release.jpg delete mode 100644 electron/static/edit-gh-release-draft.jpg delete mode 100644 electron/static/publish-gh-release.jpg diff --git a/.vscode/launch.json b/.vscode/launch.json index 897576ce..01137e28 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,22 +1,6 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "type": "node", - "request": "attach", - "name": "Attach by Process ID", - "processId": "${command:PickProcess}" - }, - { - "type": "node", - "request": "launch", - "name": "Electron Packager", - "program": "${workspaceRoot}/electron/packager/index.js", - "cwd": "${workspaceFolder}/electron/packager" - }, { "type": "node", "request": "launch", @@ -106,6 +90,19 @@ "smartStep": true, "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" + }, + { + "type": "node", + "request": "attach", + "name": "Attach by Process ID", + "processId": "${command:PickProcess}" + }, + { + "type": "node", + "request": "launch", + "name": "Electron Packager", + "program": "${workspaceRoot}/electron/packager/index.js", + "cwd": "${workspaceFolder}/electron/packager" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d9a87c6f..de5ee90f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,4 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { @@ -35,7 +33,7 @@ "panel": "new", "clear": false } - } + }, { "label": "Arduino IDE - Watch Browser App", "type": "shell", diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7ba5d9d3..00000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM gitpod/workspace-full-vnc - -USER root -RUN apt-get update -q --fix-missing && \ - apt-get install -y -q software-properties-common && \ - apt-get install -y -q --no-install-recommends \ - build-essential \ - libssl-dev \ - golang-go \ - libxkbfile-dev \ - libnss3-dev - -RUN set -ex && \ - tmpdir=$(mktemp -d) && \ - curl -L -o $tmpdir/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip && \ - mkdir -p /usr/lib/protoc && cd /usr/lib/protoc && unzip $tmpdir/protoc.zip && \ - chmod -R 755 /usr/lib/protoc/include/google && \ - ln -s /usr/lib/protoc/bin/* /usr/bin && \ - rm $tmpdir/protoc.zip diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 7fc579cf..64fdc952 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -33,13 +33,18 @@ "@theia/search-in-workspace": "next", "@theia/terminal": "next", "@theia/workspace": "next", + "@types/atob": "^2.1.2", + "@types/auth0-js": "^9.14.0", + "@types/btoa": "^1.2.3", "@types/dateformat": "^3.0.1", "@types/deepmerge": "^2.2.0", "@types/glob": "^5.0.35", "@types/google-protobuf": "^3.7.2", "@types/js-yaml": "^3.12.2", + "@types/keytar": "^4.4.0", "@types/lodash.debounce": "^4.0.6", "@types/ncp": "^2.0.4", + "@types/node-fetch": "^2.5.7", "@types/ps-tree": "^1.1.0", "@types/react-select": "^3.0.0", "@types/react-tabs": "^2.3.2", @@ -48,15 +53,24 @@ "@types/which": "^1.3.1", "ajv": "^6.5.3", "async-mutex": "^0.3.0", + "atob": "^2.1.2", + "auth0-js": "^9.14.0", + "btoa": "^1.2.1", "css-element-queries": "^1.2.0", "dateformat": "^3.0.3", - "deepmerge": "^4.2.2", + "deepmerge": "2.0.1", "fuzzy": "^0.1.3", "glob": "^7.1.6", "google-protobuf": "^3.11.4", - "lodash.debounce": "^4.0.8", + "hash.js": "^1.1.7", + "is-valid-path": "^0.1.1", "js-yaml": "^3.13.1", + "jwt-decode": "^3.1.2", + "keytar": "7.2.0", + "lodash.debounce": "^4.0.8", "ncp": "^2.0.0", + "node-fetch": "^2.6.1", + "open": "^8.0.6", "p-queue": "^5.0.0", "ps-tree": "^1.2.0", "react-disable": "^0.1.0", diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index 6d312946..25871b14 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -1,38 +1,18 @@ import { Mutex } from 'async-mutex'; -import { - MAIN_MENU_BAR, - MenuContribution, - MenuModelRegistry, - SelectionService, - ILogger, - DisposableCollection, -} from '@theia/core'; +import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger, DisposableCollection } from '@theia/core'; import { ContextMenuRenderer, - FrontendApplication, - FrontendApplicationContribution, - OpenerService, - StatusBar, - StatusBarAlignment, + FrontendApplication, FrontendApplicationContribution, + OpenerService, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; -import { - TabBarToolbarContribution, - TabBarToolbarRegistry, -} from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { - CommandContribution, - CommandRegistry, -} from '@theia/core/lib/common/command'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; import { MessageService } from '@theia/core/lib/common/message-service'; import URI from '@theia/core/lib/common/uri'; -import { - EditorMainMenu, - EditorManager, - EditorOpenerOptions, -} from '@theia/editor/lib/browser'; +import { EditorMainMenu, EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser'; import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; @@ -46,14 +26,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import * as React from 'react'; import { remote } from 'electron'; import { MainMenuManager } from '../common/main-menu-manager'; -import { - BoardsService, - CoreService, - Port, - SketchesService, - ExecutableService, - Sketch, -} from '../common/protocol'; +import { BoardsService, CoreService, Port, SketchesService, ExecutableService, Sketch } from '../common/protocol'; import { ArduinoDaemon } from '../common/protocol/arduino-daemon'; import { ConfigService } from '../common/protocol/config-service'; import { FileSystemExt } from '../common/protocol/filesystem-ext'; @@ -77,16 +50,12 @@ import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-c import { SaveAsSketch } from './contributions/save-as-sketch'; import { FileChangeType } from '@theia/filesystem/lib/browser'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution'; @injectable() -export class ArduinoFrontendContribution - implements - FrontendApplicationContribution, - TabBarToolbarContribution, - CommandContribution, - MenuContribution, - ColorContribution -{ +export class ArduinoFrontendContribution implements FrontendApplicationContribution, + TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution { + @inject(ILogger) protected logger: ILogger; @@ -156,6 +125,9 @@ export class ArduinoFrontendContribution @inject(SearchInWorkspaceFrontendContribution) protected readonly siwContribution: SearchInWorkspaceFrontendContribution; + @inject(SketchbookWidgetContribution) + protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution; + @inject(EditorMode) protected readonly editorMode: EditorMode; @@ -195,74 +167,48 @@ export class ArduinoFrontendContribution @inject(FrontendApplicationStateService) protected readonly appStateService: FrontendApplicationStateService; - protected invalidConfigPopup: - | Promise - | undefined; + protected invalidConfigPopup: Promise | undefined; protected toDisposeOnStop = new DisposableCollection(); @postConstruct() protected async init(): Promise { if (!window.navigator.onLine) { // tslint:disable-next-line:max-line-length - this.messageService.warn( - 'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.' - ); + this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'); } - const updateStatusBar = ({ - selectedBoard, - selectedPort, - }: BoardsConfig.Config) => { + const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => { this.statusBar.setElement('arduino-selected-board', { alignment: StatusBarAlignment.RIGHT, - text: selectedBoard - ? `$(microchip) ${selectedBoard.name}` - : '$(close) no board selected', - className: 'arduino-selected-board', + text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected', + className: 'arduino-selected-board' }); if (selectedBoard) { this.statusBar.setElement('arduino-selected-port', { alignment: StatusBarAlignment.RIGHT, - text: selectedPort - ? `on ${Port.toString(selectedPort)}` - : '[not connected]', - className: 'arduino-selected-port', + text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]', + className: 'arduino-selected-port' }); } - }; + } this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar); updateStatusBar(this.boardsServiceClientImpl.boardsConfig); this.appStateService.reachedState('ready').then(async () => { const sketch = await this.sketchServiceClient.currentSketch(); - if (sketch && !(await this.sketchService.isTemp(sketch))) { - this.toDisposeOnStop.push( - this.fileService.watch(new URI(sketch.uri)) - ); - this.toDisposeOnStop.push( - this.fileService.onDidFilesChange(async (event) => { - for (const { type, resource } of event.changes) { - if ( - type === FileChangeType.ADDED && - resource.parent.toString() === sketch.uri - ) { - const reloadedSketch = - await this.sketchService.loadSketch( - sketch.uri - ); - if ( - Sketch.isInSketch(resource, reloadedSketch) - ) { - this.ensureOpened( - resource.toString(), - true, - { mode: 'open' } - ); - } + if (sketch && (!await this.sketchService.isTemp(sketch))) { + this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri))); + this.toDisposeOnStop.push(this.fileService.onDidFilesChange(async event => { + for (const { type, resource } of event.changes) { + if (type === FileChangeType.ADDED && resource.parent.toString() === sketch.uri) { + const reloadedSketch = await this.sketchService.loadSketch(sketch.uri) + if (Sketch.isInSketch(resource, reloadedSketch)) { + this.ensureOpened(resource.toString(), true, { mode: 'open' }); } } - }) - ); + } + })); } }); + } onStart(app: FrontendApplication): void { @@ -274,7 +220,7 @@ export class ArduinoFrontendContribution this.problemContribution, this.scmContribution, this.siwContribution, - ] as Array) { + this.sketchbookWidgetContribution] as Array) { if (viewContribution.initializeLayout) { viewContribution.initializeLayout(app); } @@ -288,27 +234,18 @@ export class ArduinoFrontendContribution } }; this.boardsServiceClientImpl.onBoardsConfigChanged(start); - this.arduinoPreferences.onPreferenceChanged((event) => { - if ( - event.preferenceName === 'arduino.language.log' && - event.newValue !== event.oldValue - ) { + this.arduinoPreferences.onPreferenceChanged(event => { + if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) { start(this.boardsServiceClientImpl.boardsConfig); } }); this.arduinoPreferences.ready.then(() => { const webContents = remote.getCurrentWebContents(); - const zoomLevel = this.arduinoPreferences.get( - 'arduino.window.zoomLevel' - ); + 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 - ) { + 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); } @@ -322,33 +259,21 @@ export class ArduinoFrontendContribution protected languageServerFqbn?: string; protected languageServerStartMutex = new Mutex(); - protected async startLanguageServer( - fqbn: string, - name: string | undefined - ): Promise { + protected async startLanguageServer(fqbn: string, name: string | undefined): Promise { const release = await this.languageServerStartMutex.acquire(); try { await this.hostedPluginSupport.didStart; const details = await this.boardsService.getBoardDetails({ fqbn }); if (!details) { // Core is not installed for the selected board. - console.info( - `Could not start language server for ${fqbn}. The core is not installed for the board.` - ); + console.info(`Could not start language server for ${fqbn}. The core is not installed for the board.`); if (this.languageServerFqbn) { try { - await this.commandRegistry.executeCommand( - 'arduino.languageserver.stop' - ); - console.info( - `Stopped language server process for ${this.languageServerFqbn}.` - ); + await this.commandRegistry.executeCommand('arduino.languageserver.stop'); + console.info(`Stopped language server process for ${this.languageServerFqbn}.`); this.languageServerFqbn = undefined; } catch (e) { - console.error( - `Failed to start language server process for ${this.languageServerFqbn}`, - e - ); + console.error(`Failed to start language server process for ${this.languageServerFqbn}`, e); throw e; } } @@ -362,46 +287,31 @@ export class ArduinoFrontendContribution const log = this.arduinoPreferences.get('arduino.language.log'); let currentSketchPath: string | undefined = undefined; if (log) { - const currentSketch = - await this.sketchServiceClient.currentSketch(); + const currentSketch = await this.sketchServiceClient.currentSketch(); if (currentSketch) { - currentSketchPath = await this.fileService.fsPath( - new URI(currentSketch.uri) - ); + currentSketchPath = await this.fileService.fsPath(new URI(currentSketch.uri)); } } - const { clangdUri, cliUri, lsUri } = - await this.executableService.list(); - const [clangdPath, cliPath, lsPath, cliConfigPath] = - await Promise.all([ - this.fileService.fsPath(new URI(clangdUri)), - this.fileService.fsPath(new URI(cliUri)), - this.fileService.fsPath(new URI(lsUri)), - this.fileService.fsPath( - new URI(await this.configService.getCliConfigFileUri()) - ), - ]); + const { clangdUri, cliUri, lsUri } = await this.executableService.list(); + const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([ + this.fileService.fsPath(new URI(clangdUri)), + this.fileService.fsPath(new URI(cliUri)), + this.fileService.fsPath(new URI(lsUri)), + this.fileService.fsPath(new URI(await this.configService.getCliConfigFileUri())) + ]); this.languageServerFqbn = await Promise.race([ - new Promise((_, reject) => - setTimeout( - () => reject(new Error(`Timeout after ${20_000} ms.`)), - 20_000 - ) - ), - this.commandRegistry.executeCommand( - 'arduino.languageserver.start', - { - lsPath, - cliPath, - clangdPath, - log: currentSketchPath ? currentSketchPath : log, - cliConfigPath, - board: { - fqbn, - name: name ? `"${name}"` : undefined, - }, + new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${20_000} ms.`)), 20_000)), + this.commandRegistry.executeCommand('arduino.languageserver.start', { + lsPath, + cliPath, + clangdPath, + log: currentSketchPath ? currentSketchPath : log, + cliConfigPath, + board: { + fqbn, + name: name ? `"${name}"` : undefined } - ), + }) ]); } catch (e) { console.log(`Failed to start language server for ${fqbn}`, e); @@ -414,33 +324,29 @@ export class ArduinoFrontendContribution registerToolbarItems(registry: TabBarToolbarRegistry): void { registry.registerItem({ id: BoardsToolBarItem.TOOLBAR_ID, - render: () => ( - - ), - isVisible: (widget) => - ArduinoToolbar.is(widget) && widget.side === 'left', - priority: 7, + render: () => , + isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left', + priority: 7 }); registry.registerItem({ id: 'toggle-serial-monitor', command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR, - tooltip: 'Serial Monitor', + tooltip: 'Serial Monitor' }); } registerCommands(registry: CommandRegistry): void { registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, { execute: () => this.editorMode.toggleCompileForDebug(), - isToggled: () => this.editorMode.compileForDebug, + isToggled: () => this.editorMode.compileForDebug }); registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, { execute: async (uri: URI) => { this.openSketchFiles(uri); - }, + } }); registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { execute: async (query?: string | undefined) => { @@ -448,7 +354,7 @@ export class ArduinoFrontendContribution if (boardsConfig) { this.boardsServiceClientImpl.boardsConfig = boardsConfig; } - }, + } }); } @@ -457,14 +363,10 @@ export class ArduinoFrontendContribution const index = menuPath.length - 1; const menuId = menuPath[index]; return menuId; - }; - registry - .getMenu(MAIN_MENU_BAR) - .removeNode(menuId(MonacoMenus.SELECTION)); + } + registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION)); registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO)); - registry - .getMenu(MAIN_MENU_BAR) - .removeNode(menuId(TerminalMenus.TERMINAL)); + registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL)); registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW)); registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch'); @@ -472,7 +374,7 @@ export class ArduinoFrontendContribution registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id, label: 'Optimize for Debugging', - order: '4', + order: '4' }); } @@ -486,20 +388,11 @@ export class ArduinoFrontendContribution await this.ensureOpened(mainFileUri, true); if (mainFileUri.endsWith('.pde')) { const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`; - this.messageService - .info(message, 'Later', 'Yes') - .then(async (answer) => { - if (answer === 'Yes') { - this.commandRegistry.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - { - execOnlyIfTemp: false, - openAfterMove: true, - wipeOriginal: false, - } - ); - } - }); + this.messageService.info(message, 'Later', 'Yes').then(async answer => { + if (answer === 'Yes') { + this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false }); + } + }); } } catch (e) { console.error(e); @@ -508,14 +401,8 @@ export class ArduinoFrontendContribution } } - protected async ensureOpened( - uri: string, - forceOpen = false, - options?: EditorOpenerOptions | undefined - ): Promise { - const widget = this.editorManager.all.find( - (widget) => widget.editor.uri.toString() === uri - ); + protected async ensureOpened(uri: string, forceOpen = false, options?: EditorOpenerOptions | undefined): Promise { + const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri); if (!widget || forceOpen) { return this.editorManager.open(new URI(uri), options); } @@ -527,78 +414,73 @@ export class ArduinoFrontendContribution id: 'arduino.branding.primary', defaults: { dark: 'statusBar.background', - light: 'statusBar.background', + light: 'statusBar.background' }, - description: - 'The primary branding color, such as dialog titles, library, and board manager list labels.', + description: 'The primary branding color, such as dialog titles, library, and board manager list labels.' }, { id: 'arduino.branding.secondary', defaults: { dark: 'statusBar.background', - light: 'statusBar.background', + light: 'statusBar.background' }, - description: - 'Secondary branding color for list selections, dropdowns, and widget borders.', + description: 'Secondary branding color for list selections, dropdowns, and widget borders.' }, { id: 'arduino.foreground', defaults: { dark: 'editorWidget.background', light: 'editorWidget.background', - hc: 'editorWidget.background', + hc: 'editorWidget.background' }, - description: - 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.', + description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.' }, { id: 'arduino.toolbar.background', defaults: { dark: 'button.background', light: 'button.background', - hc: 'activityBar.inactiveForeground', + hc: 'activityBar.inactiveForeground' }, - description: - 'Background color of the toolbar items. Such as Upload, Verify, etc.', + description: 'Background color of the toolbar items. Such as Upload, Verify, etc.' }, { id: 'arduino.toolbar.hoverBackground', defaults: { dark: 'button.hoverBackground', light: 'button.foreground', - hc: 'textLink.foreground', + hc: 'textLink.foreground' }, - description: - 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.', + description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.' }, { id: 'arduino.toolbar.toggleBackground', defaults: { dark: 'editor.selectionBackground', light: 'editor.selectionBackground', - hc: 'textPreformat.foreground', + hc: 'textPreformat.foreground' }, - description: - 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)', + description: 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)' }, { id: 'arduino.output.foreground', defaults: { dark: 'editor.foreground', light: 'editor.foreground', - hc: 'editor.foreground', + hc: 'editor.foreground' }, - description: 'Color of the text in the Output view.', + description: 'Color of the text in the Output view.' }, { id: 'arduino.output.background', defaults: { dark: 'editor.background', light: 'editor.background', - hc: 'editor.background', + hc: 'editor.background' }, - description: 'Background color of the Output view.', + description: 'Background color of the Output view.' } ); } + } 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 ba580d8b..efb0205a 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -217,6 +217,25 @@ import { NotificationManager } from './theia/messages/notifications-manager'; import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager'; import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer'; import { NotificationsRenderer } from './theia/messages/notifications-renderer'; +import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution'; +import { LocalCacheFsProvider } from './local-cache/local-cache-fs-provider'; +import { CloudSketchbookWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-widget'; +import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget'; +import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container'; +import { CreateApi } from './create/create-api'; +import { ShareSketchDialog } from './dialogs.ts/cloud-share-sketch-dialog'; +import { AuthenticationClientService } from './auth/authentication-client-service'; +import { + AuthenticationService, + AuthenticationServicePath, +} from '../common/protocol/authentication-service'; +import { CreateFsProvider } from './create/create-fs-provider'; +import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service'; +import { CloudSketchbookContribution } from './widgets/cloud-sketchbook/cloud-sketchbook-contributions'; +import { CloudSketchbookCompositeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-composite-widget'; +import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget'; +import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget'; +import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -653,4 +672,51 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaNotificationManager).toService(NotificationManager); bind(NotificationsRenderer).toSelf().inSingletonScope(); rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer); + + // UI for the Sketchbook + bind(SketchbookWidget).toSelf(); + bind(SketchbookTreeWidget).toDynamicValue(({ container }) => + createSketchbookTreeWidget(container) + ); + bindViewContribution(bind, SketchbookWidgetContribution); + bind(FrontendApplicationContribution).toService( + SketchbookWidgetContribution + ); + bind(WidgetFactory).toDynamicValue(({ container }) => ({ + id: 'arduino-sketchbook-widget', + createWidget: () => container.get(SketchbookWidget), + })); + + bind(CloudSketchbookWidget).toSelf(); + rebind(SketchbookWidget).toService(CloudSketchbookWidget); + bind(CloudSketchbookTreeWidget).toDynamicValue(({ container }) => + createCloudSketchbookTreeWidget(container) + ); + bind(CreateApi).toSelf().inSingletonScope(); + bind(ShareSketchDialog).toSelf().inSingletonScope(); + bind(AuthenticationClientService).toSelf().inSingletonScope(); + bind(CommandContribution).toService(AuthenticationClientService); + bind(FrontendApplicationContribution).toService( + AuthenticationClientService + ); + bind(AuthenticationService) + .toDynamicValue((context) => + WebSocketConnectionProvider.createProxy( + context.container, + AuthenticationServicePath + ) + ) + .inSingletonScope(); + bind(CreateFsProvider).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(CreateFsProvider); + bind(FileServiceContribution).toService(CreateFsProvider); + bind(CloudSketchbookContribution).toSelf().inSingletonScope(); + bind(CommandContribution).toService(CloudSketchbookContribution); + bind(LocalCacheFsProvider).toSelf().inSingletonScope(); + bind(FileServiceContribution).toService(LocalCacheFsProvider); + bind(CloudSketchbookCompositeWidget).toSelf(); + bind(WidgetFactory).toDynamicValue((ctx) => ({ + id: 'cloud-sketchbook-composite-widget', + createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget), + })); }); diff --git a/arduino-ide-extension/src/browser/arduino-preferences.ts b/arduino-ide-extension/src/browser/arduino-preferences.ts index b0dd1177..9cef1b13 100644 --- a/arduino-ide-extension/src/browser/arduino-preferences.ts +++ b/arduino-ide-extension/src/browser/arduino-preferences.ts @@ -55,6 +55,62 @@ export const ArduinoConfigSchema: PreferenceSchema = { 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.', default: true, }, + 'arduino.sketchbook.showAllFiles': { + type: 'boolean', + description: + 'True to show all sketch files inside the sketch. It is false by default.', + default: false, + }, + 'arduino.cloud.enabled': { + type: 'boolean', + description: + 'True if the sketch sync functions are enabled. Defaults to true.', + default: true, + }, + 'arduino.cloud.pull.warn': { + type: 'boolean', + description: + 'True if users should be warned before pulling a cloud sketch. Defaults to true.', + default: true, + }, + 'arduino.cloud.push.warn': { + type: 'boolean', + description: + 'True if users should be warned before pushing a cloud sketch. Defaults to true.', + default: true, + }, + 'arduino.cloud.pushpublic.warn': { + type: 'boolean', + description: + 'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.', + default: true, + }, + 'arduino.cloud.sketchSyncEnpoint': { + type: 'string', + description: + 'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.', + default: 'https://api2.arduino.cc/create', + }, + 'arduino.auth.clientID': { + type: 'string', + description: 'The OAuth2 client ID.', + default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo', + }, + 'arduino.auth.domain': { + type: 'string', + description: 'The OAuth2 domain.', + default: 'login.arduino.cc', + }, + 'arduino.auth.audience': { + type: 'string', + description: 'The 0Auth2 audience.', + default: 'https://api.arduino.cc', + }, + 'arduino.auth.registerUri': { + type: 'string', + description: 'The URI used to register a new user.', + default: 'https://auth.arduino.cc/login#/register', + }, }, }; @@ -67,6 +123,16 @@ export interface ArduinoConfiguration { 'arduino.window.autoScale': boolean; 'arduino.window.zoomLevel': number; 'arduino.ide.autoUpdate': boolean; + 'arduino.sketchbook.showAllFiles': boolean; + 'arduino.cloud.enabled': boolean; + 'arduino.cloud.pull.warn': boolean; + 'arduino.cloud.push.warn': boolean; + 'arduino.cloud.pushpublic.warn': boolean; + 'arduino.cloud.sketchSyncEnpoint': string; + 'arduino.auth.clientID': string; + 'arduino.auth.domain': string; + 'arduino.auth.audience': string; + 'arduino.auth.registerUri': string; } export const ArduinoPreferences = Symbol('ArduinoPreferences'); diff --git a/arduino-ide-extension/src/browser/auth/authentication-client-service.ts b/arduino-ide-extension/src/browser/auth/authentication-client-service.ts new file mode 100644 index 00000000..d145680e --- /dev/null +++ b/arduino-ide-extension/src/browser/auth/authentication-client-service.ts @@ -0,0 +1,93 @@ +import { inject, injectable } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { + CommandRegistry, + CommandContribution, +} from '@theia/core/lib/common/command'; +import { + AuthenticationService, + AuthenticationServiceClient, + AuthenticationSession, +} from '../../common/protocol/authentication-service'; +import { CloudUserCommands } from './cloud-user-commands'; +import { serverPort } from '../../node/auth/authentication-server'; +import { AuthOptions } from '../../node/auth/types'; +import { ArduinoPreferences } from '../arduino-preferences'; + +@injectable() +export class AuthenticationClientService + implements + FrontendApplicationContribution, + CommandContribution, + AuthenticationServiceClient +{ + @inject(AuthenticationService) + protected readonly service: JsonRpcProxy; + + @inject(WindowService) + protected readonly windowService: WindowService; + + @inject(ArduinoPreferences) + protected readonly arduinoPreferences: ArduinoPreferences; + + protected authOptions: AuthOptions; + protected _session: AuthenticationSession | undefined; + protected readonly toDispose = new DisposableCollection(); + protected readonly onSessionDidChangeEmitter = new Emitter< + AuthenticationSession | undefined + >(); + + readonly onSessionDidChange = this.onSessionDidChangeEmitter.event; + + onStart(): void { + this.toDispose.push(this.onSessionDidChangeEmitter); + this.service.setClient(this); + this.service + .session() + .then((session) => this.notifySessionDidChange(session)); + this.setOptions(); + this.arduinoPreferences.onPreferenceChanged((event) => { + if (event.preferenceName.startsWith('arduino.auth.')) { + this.setOptions(); + } + }); + } + + setOptions(): void { + this.service.setOptions({ + redirectUri: `http://localhost:${serverPort}/callback`, + responseType: 'code', + clientID: this.arduinoPreferences['arduino.auth.clientID'], + domain: this.arduinoPreferences['arduino.auth.domain'], + audience: this.arduinoPreferences['arduino.auth.audience'], + registerUri: this.arduinoPreferences['arduino.auth.registerUri'], + scopes: ['openid', 'profile', 'email', 'offline_access'], + }); + } + + protected updateSession(session?: AuthenticationSession | undefined) { + this._session = session; + this.onSessionDidChangeEmitter.fire(this._session); + } + + get session(): AuthenticationSession | undefined { + return this._session; + } + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(CloudUserCommands.LOGIN, { + execute: () => this.service.login(), + }); + registry.registerCommand(CloudUserCommands.LOGOUT, { + execute: () => this.service.logout(), + }); + } + + notifySessionDidChange(session: AuthenticationSession | undefined): void { + this.updateSession(session); + } +} diff --git a/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts b/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts new file mode 100644 index 00000000..8947f001 --- /dev/null +++ b/arduino-ide-extension/src/browser/auth/cloud-user-commands.ts @@ -0,0 +1,18 @@ +import { Command } from '@theia/core/lib/common/command'; + +export namespace CloudUserCommands { + export const LOGIN: Command = { + id: 'arduino-cloud--login', + label: 'Sign in', + }; + + export const LOGOUT: Command = { + id: 'arduino-cloud--logout', + label: 'Sign Out', + }; + + export const OPEN_PROFILE_CONTEXT_MENU: Command = { + id: 'arduino-cloud-sketchbook--open-profile-menu', + label: 'Contextual menu', + }; +} diff --git a/arduino-ide-extension/src/browser/contributions/sketch-control.ts b/arduino-ide-extension/src/browser/contributions/sketch-control.ts index 5f8d6034..94f540eb 100644 --- a/arduino-ide-extension/src/browser/contributions/sketch-control.ts +++ b/arduino-ide-extension/src/browser/contributions/sketch-control.ts @@ -17,7 +17,10 @@ import { TabBarToolbarRegistry, open, } from './contribution'; -import { ArduinoMenus } from '../menu/arduino-menus'; +import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; +import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl'; +import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider'; @injectable() export class SketchControl extends SketchContribution { @@ -30,6 +33,15 @@ export class SketchControl extends SketchContribution { @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(EditorManager) + protected readonly editorManager: EditorManager; + + @inject(SketchesServiceClientImpl) + protected readonly sketchesServiceClient: SketchesServiceClientImpl; + + @inject(LocalCacheFsProvider) + protected readonly localCacheFsProvider: LocalCacheFsProvider; + protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection(); @@ -61,8 +73,100 @@ export class SketchControl extends SketchContribution { const { mainFileUri, rootFolderFileUris } = await this.sketchService.loadSketch(sketch.uri); const uris = [mainFileUri, ...rootFolderFileUris]; + + const currentSketch = + await this.sketchesServiceClient.currentSketch(); + const parentsketchUri = this.editorManager.currentEditor + ?.getResourceUri() + ?.toString(); + const parentsketch = + await this.sketchService.getSketchFolder( + parentsketchUri || '' + ); + + // if the current file is in the current opened sketch, show extra menus + if ( + currentSketch && + parentsketch && + parentsketch.uri === currentSketch.uri && + (await this.allowRename(parentsketch.uri)) + ) { + this.menuRegistry.registerMenuAction( + ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, + { + commandId: WorkspaceCommands.FILE_RENAME.id, + label: 'Rename', + order: '1', + } + ); + this.toDisposeBeforeCreateNewContextMenu.push( + Disposable.create(() => + this.menuRegistry.unregisterMenuAction( + WorkspaceCommands.FILE_RENAME + ) + ) + ); + } else { + const renamePlaceholder = new PlaceholderMenuNode( + ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, + 'Rename' + ); + this.menuRegistry.registerMenuNode( + ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, + renamePlaceholder + ); + this.toDisposeBeforeCreateNewContextMenu.push( + Disposable.create(() => + this.menuRegistry.unregisterMenuNode( + renamePlaceholder.id + ) + ) + ); + } + + if ( + currentSketch && + parentsketch && + parentsketch.uri === currentSketch.uri && + (await this.allowDelete(parentsketch.uri)) + ) { + this.menuRegistry.registerMenuAction( + ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, + { + commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window. + label: 'Delete', + order: '2', + } + ); + this.toDisposeBeforeCreateNewContextMenu.push( + Disposable.create(() => + this.menuRegistry.unregisterMenuAction( + WorkspaceCommands.FILE_DELETE + ) + ) + ); + } else { + const deletePlaceholder = new PlaceholderMenuNode( + ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, + 'Delete' + ); + this.menuRegistry.registerMenuNode( + ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, + deletePlaceholder + ); + this.toDisposeBeforeCreateNewContextMenu.push( + Disposable.create(() => + this.menuRegistry.unregisterMenuNode( + deletePlaceholder.id + ) + ) + ); + } + for (let i = 0; i < uris.length; i++) { const uri = new URI(uris[i]); + + // focus on the opened sketch const command = { id: `arduino-focus-file--${uri.toString()}`, }; @@ -110,22 +214,6 @@ export class SketchControl extends SketchContribution { order: '0', } ); - registry.registerMenuAction( - ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, - { - commandId: WorkspaceCommands.FILE_RENAME.id, - label: 'Rename', - order: '1', - } - ); - registry.registerMenuAction( - ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, - { - commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window. - label: 'Delete', - order: '2', - } - ); registry.registerMenuAction( ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, @@ -166,6 +254,23 @@ export class SketchControl extends SketchContribution { command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id, }); } + + protected async isCloudSketch(uri: string) { + const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri)); + + if (cloudCacheLocation) { + return true; + } + return false; + } + + protected async allowRename(uri: string) { + return !this.isCloudSketch(uri); + } + + protected async allowDelete(uri: string) { + return !this.isCloudSketch(uri); + } } export namespace SketchControl { diff --git a/arduino-ide-extension/src/browser/create/create-api.ts b/arduino-ide-extension/src/browser/create/create-api.ts new file mode 100644 index 00000000..3e02707e --- /dev/null +++ b/arduino-ide-extension/src/browser/create/create-api.ts @@ -0,0 +1,544 @@ +import { injectable } from 'inversify'; +import * as createPaths from './create-paths'; +import { posix, splitSketchPath } from './create-paths'; +import { AuthenticationClientService } from '../auth/authentication-client-service'; +import { ArduinoPreferences } from '../arduino-preferences'; + +export interface ResponseResultProvider { + (response: Response): Promise; +} +export namespace ResponseResultProvider { + export const NOOP: ResponseResultProvider = async () => undefined; + export const TEXT: ResponseResultProvider = (response) => response.text(); + export const JSON: ResponseResultProvider = (response) => response.json(); +} + +type ResourceType = 'f' | 'd'; + +export let sketchCache: Create.Sketch[] = []; + +@injectable() +export class CreateApi { + protected authenticationService: AuthenticationClientService; + protected arduinoPreferences: ArduinoPreferences; + + public init( + authenticationService: AuthenticationClientService, + arduinoPreferences: ArduinoPreferences + ): CreateApi { + this.authenticationService = authenticationService; + this.arduinoPreferences = arduinoPreferences; + + return this; + } + + async findSketchByPath( + path: string, + trustCache = true + ): Promise { + const skatches = sketchCache; + const sketch = skatches.find((sketch) => { + const [, spath] = splitSketchPath(sketch.path); + return path === spath; + }); + if (trustCache) { + return Promise.resolve(sketch); + } + return await this.sketch({ id: sketch?.id }); + } + + getSketchSecretStat(sketch: Create.Sketch): Create.Resource { + return { + href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`, + modified_at: sketch.modified_at, + name: `${Create.arduino_secrets_file}`, + path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`, + mimetype: 'text/x-c++src; charset=utf-8', + type: 'file', + sketchId: sketch.id, + }; + } + + async sketch(opt: { + id?: string; + path?: string; + }): Promise { + let url; + if (opt.id) { + url = new URL(`${this.domain()}/sketches/byID/${opt.id}`); + } else if (opt.path) { + url = new URL(`${this.domain()}/sketches/byPath${opt.path}`); + } else { + return; + } + + url.searchParams.set('user_id', 'me'); + const headers = await this.headers(); + const result = await this.run(url, { + method: 'GET', + headers, + }); + return result; + } + + async sketches(): Promise { + const url = new URL(`${this.domain()}/sketches`); + url.searchParams.set('user_id', 'me'); + const headers = await this.headers(); + const result = await this.run<{ sketches: Create.Sketch[] }>(url, { + method: 'GET', + headers, + }); + sketchCache = result.sketches; + return result.sketches; + } + + async createSketch( + posixPath: string, + content: string = CreateApi.defaultInoContent + ): Promise { + const url = new URL(`${this.domain()}/sketches`); + const headers = await this.headers(); + const payload = { + ino: btoa(content), + path: posixPath, + user_id: 'me', + }; + const init = { + method: 'PUT', + body: JSON.stringify(payload), + headers, + }; + const result = await this.run(url, init); + return result; + } + + async readDirectory( + posixPath: string, + options: { recursive?: boolean; match?: string; secrets?: boolean } = {} + ): Promise { + const url = new URL( + `${this.domain()}/files/d/$HOME/sketches_v2${posixPath}` + ); + if (options.recursive) { + url.searchParams.set('deep', 'true'); + } + if (options.match) { + url.searchParams.set('name_like', options.match); + } + const headers = await this.headers(); + + const sketchProm = options.secrets + ? this.sketches() + : Promise.resolve(sketchCache); + + return Promise.all([ + this.run(url, { + method: 'GET', + headers, + }), + sketchProm, + ]) + .then(async ([result, sketches]) => { + if (options.secrets) { + // for every sketch with secrets, create a fake arduino_secrets.h + result.forEach(async (res) => { + if (res.type !== 'sketch') { + return; + } + + const [, spath] = createPaths.splitSketchPath(res.path); + const sketch = await this.findSketchByPath(spath); + if ( + sketch && + sketch.secrets && + sketch.secrets.length > 0 + ) { + result.push(this.getSketchSecretStat(sketch)); + } + }); + + if (posixPath !== posix.sep) { + const sketch = await this.findSketchByPath(posixPath); + if ( + sketch && + sketch.secrets && + sketch.secrets.length > 0 + ) { + result.push(this.getSketchSecretStat(sketch)); + } + } + } + const sketchesMap: Record = + sketches.reduce((prev, curr) => { + return { ...prev, [curr.path]: curr }; + }, {}); + + // add the sketch id and isPublic to the resource + return result.map((resource) => { + return { + ...resource, + sketchId: sketchesMap[resource.path]?.id || '', + isPublic: + sketchesMap[resource.path]?.is_public || false, + }; + }); + }) + .catch((reason) => { + if (reason?.status === 404) return [] as Create.Resource[]; + else throw reason; + }); + } + + async createDirectory(posixPath: string): Promise { + const url = new URL( + `${this.domain()}/files/d/$HOME/sketches_v2${posixPath}` + ); + const headers = await this.headers(); + await this.run(url, { + method: 'POST', + headers, + }); + } + + async stat(posixPath: string): Promise { + // The root is a directory read. + if (posixPath === '/') { + throw new Error('Stating the root is not supported'); + } + // The RESTful API has different endpoints for files and directories. + // The RESTful API does not provide specific error codes, only HTP 500. + // We query the parent directory and look for the file with the last segment. + const parentPosixPath = createPaths.parentPosix(posixPath); + const basename = createPaths.basename(posixPath); + + let resources; + if (basename === Create.arduino_secrets_file) { + const sketch = await this.findSketchByPath(parentPosixPath); + resources = sketch ? [this.getSketchSecretStat(sketch)] : []; + } else { + resources = await this.readDirectory(parentPosixPath, { + match: basename, + }); + } + + resources.sort((left, right) => left.path.length - right.path.length); + const resource = resources.find(({ name }) => name === basename); + if (!resource) { + throw new CreateError(`Not found: ${posixPath}.`, 404); + } + return resource; + } + + async readFile(posixPath: string): Promise { + const basename = createPaths.basename(posixPath); + + if (basename === Create.arduino_secrets_file) { + const parentPosixPath = createPaths.parentPosix(posixPath); + const sketch = await this.findSketchByPath(parentPosixPath, false); + + let file = ''; + if (sketch && sketch.secrets) { + for (const item of sketch?.secrets) { + file += `#define ${item.name} "${item.value}"\r\n`; + } + } + return file; + } + + const url = new URL( + `${this.domain()}/files/f/$HOME/sketches_v2${posixPath}` + ); + const headers = await this.headers(); + const result = await this.run<{ data: string }>(url, { + method: 'GET', + headers, + }); + const { data } = result; + return atob(data); + } + + async writeFile( + posixPath: string, + content: string | Uint8Array + ): Promise { + const basename = createPaths.basename(posixPath); + + if (basename === Create.arduino_secrets_file) { + const parentPosixPath = createPaths.parentPosix(posixPath); + const sketch = await this.findSketchByPath(parentPosixPath); + if (sketch) { + const url = new URL(`${this.domain()}/sketches/${sketch.id}`); + const headers = await this.headers(); + + // parse the secret file + const secrets = ( + typeof content === 'string' + ? content + : new TextDecoder().decode(content) + ) + .split(/\r?\n/) + .reduce((prev, curr) => { + // check if the line contains a secret + const secret = curr.split('SECRET_')[1] || null; + if (!secret) { + return prev; + } + const regexp = /(\S*)\s+([\S\s]*)/g; + const tokens = regexp.exec(secret) || []; + const name = + tokens[1].length > 0 ? `SECRET_${tokens[1]}` : ''; + + let value = ''; + if (tokens[2].length > 0) { + value = JSON.parse( + JSON.stringify( + tokens[2] + .replace(/^['"]?/g, '') + .replace(/['"]?$/g, '') + ) + ); + } + + if (name.length === 0 || value.length === 0) { + return prev; + } + + return [...prev, { name, value }]; + }, []); + + const payload = { + id: sketch.id, + libraries: sketch.libraries, + secrets: { data: secrets }, + }; + + // replace the sketch in the cache, so other calls will not overwrite each other + sketchCache = sketchCache.filter((skt) => skt.id !== sketch.id); + sketchCache.push({ ...sketch, secrets }); + + const init = { + method: 'POST', + body: JSON.stringify(payload), + headers, + }; + await this.run(url, init); + } + return; + } + + const url = new URL( + `${this.domain()}/files/f/$HOME/sketches_v2${posixPath}` + ); + const headers = await this.headers(); + const data = btoa( + typeof content === 'string' + ? content + : new TextDecoder().decode(content) + ); + const payload = { data }; + const init = { + method: 'POST', + body: JSON.stringify(payload), + headers, + }; + await this.run(url, init); + } + + async deleteFile(posixPath: string): Promise { + await this.delete(posixPath, 'f'); + } + + async deleteDirectory(posixPath: string): Promise { + await this.delete(posixPath, 'd'); + } + + private async delete(posixPath: string, type: ResourceType): Promise { + const url = new URL( + `${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}` + ); + const headers = await this.headers(); + await this.run(url, { + method: 'DELETE', + headers, + }); + } + + async rename(fromPosixPath: string, toPosixPath: string): Promise { + const url = new URL(`${this.domain('v3')}/files/mv`); + const headers = await this.headers(); + const payload = { + from: `$HOME/sketches_v2${fromPosixPath}`, + to: `$HOME/sketches_v2${toPosixPath}`, + }; + const init = { + method: 'POST', + body: JSON.stringify(payload), + headers, + }; + await this.run(url, init, ResponseResultProvider.NOOP); + } + + async editSketch({ + id, + params, + }: { + id: string; + params: Record; + }): Promise { + const url = new URL(`${this.domain()}/sketches/${id}`); + + const headers = await this.headers(); + const result = await this.run(url, { + method: 'POST', + body: JSON.stringify({ id, ...params }), + headers, + }); + return result; + } + + async copy(fromPosixPath: string, toPosixPath: string): Promise { + const payload = { + from: `$HOME/sketches_v2${fromPosixPath}`, + to: `$HOME/sketches_v2${toPosixPath}`, + }; + const url = new URL(`${this.domain('v3')}/files/cp`); + const headers = await this.headers(); + const init = { + method: 'POST', + body: JSON.stringify(payload), + headers, + }; + await this.run(url, init, ResponseResultProvider.NOOP); + } + + private async run( + requestInfo: RequestInfo | URL, + init: RequestInit | undefined, + resultProvider: ResponseResultProvider = ResponseResultProvider.JSON + ): Promise { + const response = await fetch( + requestInfo instanceof URL ? requestInfo.toString() : requestInfo, + init + ); + if (!response.ok) { + let details: string | undefined = undefined; + try { + details = await response.json(); + } catch (e) { + console.error('Cloud not get the error details.', e); + } + const { statusText, status } = response; + throw new CreateError(statusText, status, details); + } + const result = await resultProvider(response); + return result; + } + + private async headers(): Promise> { + const token = await this.token(); + return { + 'content-type': 'application/json', + accept: 'application/json', + authorization: `Bearer ${token}`, + }; + } + + private domain(apiVersion = 'v2'): string { + const endpoint = + this.arduinoPreferences['arduino.cloud.sketchSyncEnpoint']; + return `${endpoint}/${apiVersion}`; + } + + private async token(): Promise { + return this.authenticationService.session?.accessToken || ''; + } +} + +export namespace CreateApi { + export const defaultInoContent = `/* + +*/ + +void setup() { + +} + +void loop() { + +} + +`; +} + +export namespace Create { + export interface Sketch { + readonly name: string; + readonly path: string; + readonly modified_at: string; + readonly created_at: string; + + readonly secrets?: { name: string; value: string }[]; + + readonly id: string; + readonly is_public: boolean; + // readonly board_fqbn: '', + // readonly board_name: '', + // readonly board_type: 'serial' | 'network' | 'cloud' | '', + readonly href?: string; + readonly libraries: string[]; + // readonly tutorials: string[] | null; + // readonly types: string[] | null; + // readonly user_id: string; + } + + export type ResourceType = 'sketch' | 'folder' | 'file'; + export const arduino_secrets_file = 'arduino_secrets.h'; + export interface Resource { + readonly name: string; + /** + * Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`. + */ + readonly path: string; + readonly type: ResourceType; + readonly sketchId: string; + readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ` + readonly children?: number; // For 'sketch' and 'folder' types. + readonly size?: number; // For 'sketch' type only. + readonly isPublic?: boolean; // For 'sketch' type only. + + readonly mimetype?: string; // For 'file' type. + readonly href?: string; + } + export namespace Resource { + export function is(arg: any): arg is Resource { + return ( + !!arg && + 'name' in arg && + typeof arg['name'] === 'string' && + 'path' in arg && + typeof arg['path'] === 'string' && + 'type' in arg && + typeof arg['type'] === 'string' && + 'modified_at' in arg && + typeof arg['modified_at'] === 'string' && + (arg['type'] === 'sketch' || + arg['type'] === 'folder' || + arg['type'] === 'file') + ); + } + } + + export type RawResource = Omit; +} + +export class CreateError extends Error { + constructor( + message: string, + readonly status: number, + readonly details?: string + ) { + super(message); + Object.setPrototypeOf(this, CreateError.prototype); + } +} diff --git a/arduino-ide-extension/src/browser/create/create-fs-provider.ts b/arduino-ide-extension/src/browser/create/create-fs-provider.ts new file mode 100644 index 00000000..645d89f5 --- /dev/null +++ b/arduino-ide-extension/src/browser/create/create-fs-provider.ts @@ -0,0 +1,212 @@ +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { Event } from '@theia/core/lib/common/event'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { + Stat, + FileType, + FileChange, + FileWriteOptions, + FileDeleteOptions, + FileOverwriteOptions, + FileSystemProvider, + FileSystemProviderError, + FileSystemProviderErrorCode, + FileSystemProviderCapabilities, + WatchOptions, +} from '@theia/filesystem/lib/common/files'; +import { + FileService, + FileServiceContribution, +} from '@theia/filesystem/lib/browser/file-service'; +import { AuthenticationClientService } from '../auth/authentication-client-service'; +import { Create, CreateApi } from './create-api'; +import { CreateUri } from './create-uri'; +import { SketchesService } from '../../common/protocol'; +import { ArduinoPreferences } from '../arduino-preferences'; + +export const REMOTE_ONLY_FILES = [ + 'sketch.json', + 'thingsProperties.h', + 'thingProperties.h', +]; + +@injectable() +export class CreateFsProvider + implements + FileSystemProvider, + FrontendApplicationContribution, + FileServiceContribution +{ + @inject(AuthenticationClientService) + protected readonly authenticationService: AuthenticationClientService; + + @inject(CreateApi) + protected readonly createApi: CreateApi; + + @inject(SketchesService) + protected readonly sketchesService: SketchesService; + + @inject(ArduinoPreferences) + protected readonly arduinoPreferences: ArduinoPreferences; + + protected readonly toDispose = new DisposableCollection(); + + readonly onFileWatchError: Event = Event.None; + readonly onDidChangeFile: Event = Event.None; + readonly onDidChangeCapabilities: Event = Event.None; + readonly capabilities: FileSystemProviderCapabilities = + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.PathCaseSensitive | + FileSystemProviderCapabilities.Access; + + onStop(): void { + this.toDispose.dispose(); + } + + registerFileSystemProviders(service: FileService): void { + service.onWillActivateFileSystemProvider((event) => { + if (event.scheme === CreateUri.scheme) { + event.waitUntil( + (async () => { + service.registerProvider(CreateUri.scheme, this); + })() + ); + } + }); + } + + watch(uri: URI, opts: WatchOptions): Disposable { + return Disposable.NULL; + } + + async stat(uri: URI): Promise { + if (CreateUri.equals(CreateUri.root, uri)) { + this.getCreateApi; // This will throw when not logged in. + return { + type: FileType.Directory, + ctime: 0, + mtime: 0, + size: 0, + }; + } + const resource = await this.getCreateApi.stat(uri.path.toString()); + const mtime = Date.parse(resource.modified_at); + return { + type: this.toFileType(resource.type), + ctime: mtime, + mtime, + size: 0, + }; + } + + async mkdir(uri: URI): Promise { + await this.getCreateApi.createDirectory(uri.path.toString()); + } + + async readdir(uri: URI): Promise<[string, FileType][]> { + const resources = await this.getCreateApi.readDirectory( + uri.path.toString(), + { + secrets: true, + } + ); + return resources + .filter((res) => !REMOTE_ONLY_FILES.includes(res.name)) + .map(({ name, type }) => [name, this.toFileType(type)]); + } + + async delete(uri: URI, opts: FileDeleteOptions): Promise { + return; + + if (!opts.recursive) { + throw new Error( + 'Arduino Create file-system provider does not support non-recursive deletion.' + ); + } + const stat = await this.stat(uri); + if (!stat) { + throw new FileSystemProviderError( + 'File not found.', + FileSystemProviderErrorCode.FileNotFound + ); + } + switch (stat.type) { + case FileType.Directory: { + await this.getCreateApi.deleteDirectory(uri.path.toString()); + break; + } + case FileType.File: { + await this.getCreateApi.deleteFile(uri.path.toString()); + break; + } + default: { + throw new FileSystemProviderError( + `Unexpected file type '${ + stat.type + }' for resource: ${uri.toString()}`, + FileSystemProviderErrorCode.Unknown + ); + } + } + } + + async rename( + oldUri: URI, + newUri: URI, + options: FileOverwriteOptions + ): Promise { + await this.getCreateApi.rename( + oldUri.path.toString(), + newUri.path.toString() + ); + } + + async readFile(uri: URI): Promise { + const content = await this.getCreateApi.readFile(uri.path.toString()); + return new TextEncoder().encode(content); + } + + async writeFile( + uri: URI, + content: Uint8Array, + options: FileWriteOptions + ): Promise { + await this.getCreateApi.writeFile(uri.path.toString(), content); + } + + async access(uri: URI, mode?: number): Promise { + this.getCreateApi; // Will throw if not logged in. + } + + public toFileType(type: Create.ResourceType): FileType { + switch (type) { + case 'file': + return FileType.File; + case 'sketch': + case 'folder': + return FileType.Directory; + default: + return FileType.Unknown; + } + } + + private get getCreateApi(): CreateApi { + const { session } = this.authenticationService; + if (!session) { + throw new FileSystemProviderError( + 'Not logged in.', + FileSystemProviderErrorCode.NoPermissions + ); + } + + return this.createApi.init( + this.authenticationService, + this.arduinoPreferences + ); + } +} diff --git a/arduino-ide-extension/src/browser/create/create-paths.ts b/arduino-ide-extension/src/browser/create/create-paths.ts new file mode 100644 index 00000000..0fccb0c0 --- /dev/null +++ b/arduino-ide-extension/src/browser/create/create-paths.ts @@ -0,0 +1,59 @@ +export const posix = { sep: '/' }; + +// TODO: poor man's `path.join(path, '..')` in the browser. +export function parentPosix(path: string): string { + const segments = path.split(posix.sep) || []; + segments.pop(); + let modified = segments.join(posix.sep); + if (path.charAt(path.length - 1) === posix.sep) { + modified += posix.sep; + } + return modified; +} + +export function basename(path: string): string { + const segments = path.split(posix.sep) || []; + return segments.pop()!; +} + +export function posixSegments(posixPath: string): string[] { + return posixPath.split(posix.sep).filter((segment) => !!segment); +} + +/** + * Splits the `raw` path into two segments, a root that contains user information and the relevant POSIX path. \ + * For examples: + * ``` + * `29ad0829759028dde9b877343fa3b0e1:testrest/sketches_v2/xxx_folder/xxx_sub_folder/sketch_in_folder/sketch_in_folder.ino` + * ``` + * will be: + * ``` + * ['29ad0829759028dde9b877343fa3b0e1:testrest/sketches_v2', '/xxx_folder/xxx_sub_folder/sketch_in_folder/sketch_in_folder.ino'] + * ``` + */ +export function splitSketchPath( + raw: string, + sep = '/sketches_v2/' +): [string, string] { + if (!sep) { + throw new Error('Invalid separator. Cannot be zero length.'); + } + const index = raw.indexOf(sep); + if (index === -1) { + throw new Error(`Invalid path pattern. Raw path was '${raw}'.`); + } + const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format. + const posixPath = raw.substr(index + sep.length - 1); + if (!posixPath) { + throw new Error(`Could not extract POSIX path from '${raw}'.`); + } + return [createRoot, posixPath]; +} + +export function toPosixPath(raw: string): string { + if (raw === posix.sep) { + return posix.sep; // Handles the root resource case. + } + const [, posixPath] = splitSketchPath(raw); + return posixPath; +} diff --git a/arduino-ide-extension/src/browser/create/create-uri.ts b/arduino-ide-extension/src/browser/create/create-uri.ts new file mode 100644 index 00000000..b9a5d176 --- /dev/null +++ b/arduino-ide-extension/src/browser/create/create-uri.ts @@ -0,0 +1,39 @@ +import { URI as Uri } from 'vscode-uri'; +import URI from '@theia/core/lib/common/uri'; +import { Create } from './create-api'; +import { toPosixPath, parentPosix, posix } from './create-paths'; + +export namespace CreateUri { + export const scheme = 'arduino-create'; + export const root = toUri(posix.sep); + + export function toUri(posixPathOrResource: string | Create.Resource): URI { + const posixPath = + typeof posixPathOrResource === 'string' + ? posixPathOrResource + : toPosixPath(posixPathOrResource.path); + return new URI( + Uri.parse(posixPath).with({ scheme, authority: 'create' }) + ); + } + + export function is(uri: URI): boolean { + return uri.scheme === scheme; + } + + export function equals(left: URI, right: URI): boolean { + return is(left) && is(right) && left.toString() === right.toString(); + } + + export function parent(uri: URI): URI { + if (!is(uri)) { + throw new Error( + `Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.` + ); + } + if (equals(uri, root)) { + return uri; + } + return toUri(parentPosix(uri.path.toString())); + } +} diff --git a/arduino-ide-extension/src/browser/data/arduino.color-theme.json b/arduino-ide-extension/src/browser/data/arduino.color-theme.json index f35d3e0a..97fcf436 100644 --- a/arduino-ide-extension/src/browser/data/arduino.color-theme.json +++ b/arduino-ide-extension/src/browser/data/arduino.color-theme.json @@ -85,7 +85,11 @@ ], "colors": { "list.highlightForeground": "#005c5f", - "list.activeSelectionBackground": "#005c5f", + "list.activeSelectionForeground": "#424242", + "list.activeSelectionBackground": "#DAE3E3", + "list.inactiveSelectionForeground": "#424242", + "list.inactiveSelectionBackground": "#DAE3E3", + "list.hoverBackground": "#ECF1F1", "progressBar.background": "#005c5f", "editor.background": "#ffffff", "editorCursor.foreground": "#434f54", @@ -110,6 +114,7 @@ "activityBar.foreground": "#616161", "statusBar.background": "#005c5f", "secondaryButton.background": "#b5c8c9", + "secondaryButton.foreground": "#ececec", "secondaryButton.hoverBackground": "#dae3e3", "arduino.branding.primary": "#00979d", "arduino.branding.secondary": "#b5c8c9", diff --git a/arduino-ide-extension/src/browser/dialogs.ts/cloud-share-sketch-dialog.tsx b/arduino-ide-extension/src/browser/dialogs.ts/cloud-share-sketch-dialog.tsx new file mode 100644 index 00000000..60f53eeb --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs.ts/cloud-share-sketch-dialog.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; +import { inject, injectable } from 'inversify'; +import { Widget } from '@phosphor/widgets'; +import { Message } from '@phosphor/messaging'; +import { clipboard } from 'electron'; +import { + AbstractDialog, + ReactWidget, + DialogProps, +} from '@theia/core/lib/browser'; +import { CreateApi } from '../create/create-api'; + +const RadioButton = (props: { + id: string; + changed: (evt: React.BaseSyntheticEvent) => void; + value: string; + isSelected: boolean; + isDisabled: boolean; + label: string; +}) => { + return ( +

+ + +

+ ); +}; + +export const ShareSketchComponent = ({ + treeNode, + createApi, + domain = 'https://create.arduino.cc', +}: { + treeNode: any; + createApi: CreateApi; + domain?: string; +}): React.ReactElement => { + // const [publicVisibility, setPublicVisibility] = React.useState( + // treeNode.isPublic + // ); + + const [loading, setloading] = React.useState(false); + + const radioChangeHandler = async (event: React.BaseSyntheticEvent) => { + setloading(true); + const sketch = await createApi.editSketch({ + id: treeNode.sketchId, + params: { + is_public: event.target.value === 'private' ? false : true, + }, + }); + // setPublicVisibility(sketch.is_public); + treeNode.isPublic = sketch.is_public; + setloading(false); + }; + + const sketchLink = `${domain}/editor/_/${treeNode.sketchId}/preview`; + const embedLink = ``; + + return ( +
+

Choose visibility of your Sketch:

+ + + + {treeNode.isPublic && ( +
+

Link:

+
+ + +
+

Embed:

+
+