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 2b170dd7b..d6779c302 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -387,6 +387,10 @@ import { import { TreeViewDecoratorService } from '@theia/plugin-ext/lib/main/browser/view/tree-view-decorator-service'; import { PLUGIN_VIEW_DATA_FACTORY_ID } from '@theia/plugin-ext/lib/main/browser/view/plugin-view-registry'; import { TreeViewWidget } from './theia/plugin-ext/tree-view-widget'; +import { + VersionWelcomeDialog, + VersionWelcomeDialogProps, +} from './dialogs/version-welcome-dialog'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -1014,6 +1018,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { title: 'IDEUpdater', }); + bind(VersionWelcomeDialog).toSelf().inSingletonScope(); + bind(VersionWelcomeDialogProps).toConstantValue({ + title: 'VersionWelcomeDialog', + }); + bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialogProps).toConstantValue({ title: 'UserFields', diff --git a/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts b/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts index ab5f62ac2..7ac57ac01 100644 --- a/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts +++ b/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts @@ -3,10 +3,14 @@ import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; import { inject, injectable } from '@theia/core/shared/inversify'; import { IDEUpdater, + LAST_USED_IDE_VERSION, SKIP_IDE_VERSION, } from '../../common/protocol/ide-updater'; import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog'; import { Contribution } from './contribution'; +import { VersionWelcomeDialog } from '../dialogs/version-welcome-dialog'; +import { AppService } from '../app-service'; +import { SemVer } from 'semver'; @injectable() export class CheckForIDEUpdates extends Contribution { @@ -16,9 +20,15 @@ export class CheckForIDEUpdates extends Contribution { @inject(IDEUpdaterDialog) private readonly updaterDialog: IDEUpdaterDialog; + @inject(VersionWelcomeDialog) + private readonly versionWelcomeDialog: VersionWelcomeDialog; + @inject(LocalStorageService) private readonly localStorage: LocalStorageService; + @inject(AppService) + private readonly appService: AppService; + override onStart(): void { this.preferences.onPreferenceChanged( ({ preferenceName, newValue, oldValue }) => { @@ -36,7 +46,7 @@ export class CheckForIDEUpdates extends Contribution { ); } - override onReady(): void { + override async onReady(): Promise { this.updater .init( this.preferences.get('arduino.ide.updateChannel'), @@ -49,7 +59,13 @@ export class CheckForIDEUpdates extends Contribution { return this.updater.checkForUpdates(true); }) .then(async (updateInfo) => { - if (!updateInfo) return; + if (!updateInfo) { + const isNewVersion = await this.isNewStableVersion(); + if (isNewVersion) { + this.versionWelcomeDialog.open(); + } + return; + } const versionToSkip = await this.localStorage.getData( SKIP_IDE_VERSION ); @@ -64,6 +80,44 @@ export class CheckForIDEUpdates extends Contribution { e.message ) ); + }) + .finally(() => { + this.setCurrentIDEVersion(); }); } + + private async setCurrentIDEVersion(): Promise { + try { + const { appVersion } = await this.appService.info(); + const currSemVer = new SemVer(appVersion ?? ''); + this.localStorage.setData(LAST_USED_IDE_VERSION, currSemVer.format()); + } catch { + // ignore invalid versions + } + } + + /** + * Check if user is running a new IDE version for the first time. + * @returns true if the current IDE version is greater than the last used version + * and both are non-prerelease versions. + */ + private async isNewStableVersion(): Promise { + try { + const { appVersion } = await this.appService.info(); + const prevVersion = await this.localStorage.getData( + LAST_USED_IDE_VERSION + ); + + const prevSemVer = new SemVer(prevVersion ?? ''); + const currSemVer = new SemVer(appVersion ?? ''); + + if (prevSemVer.prerelease.length || currSemVer.prerelease.length) { + return false; + } + + return currSemVer.compare(prevSemVer) === 1; + } catch (e) { + return false; + } + } } diff --git a/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx new file mode 100644 index 000000000..6d5a71db0 --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx @@ -0,0 +1,104 @@ +import React from '@theia/core/shared/react'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Message } from '@theia/core/shared/@phosphor/messaging'; +import { ReactDialog } from '../theia/dialogs/dialogs'; +import { nls } from '@theia/core'; +import { DialogProps } from '@theia/core/lib/browser'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { AppService } from '../app-service'; + +@injectable() +export class VersionWelcomeDialogProps extends DialogProps {} + +@injectable() +export class VersionWelcomeDialog extends ReactDialog { + @inject(AppService) + private readonly appService: AppService; + + @inject(WindowService) + private readonly windowService: WindowService; + + constructor( + @inject(VersionWelcomeDialogProps) + protected override readonly props: VersionWelcomeDialogProps + ) { + super({ + title: nls.localize( + 'arduino/versionWelcome/title', + 'Welcome to a new version of the Arduino IDE!' + ), + }); + this.node.id = 'version-welcome-dialog-container'; + this.contentNode.classList.add('version-welcome-dialog'); + } + + protected render(): React.ReactNode { + return ( +
+

+ {nls.localize( + 'arduino/versionWelcome/donateMessage', + 'Arduino is committed to keeping software free and open-source for everyone. Your donation helps us develop new features, improve libraries, and support millions of users worldwide.' + )} +

+

+ {nls.localize( + 'arduino/versionWelcome/donateMessage2', + 'Please consider supporting our work on the free open source Arduino IDE.' + )} +

+
+ ); + } + + override get value(): void { + return; + } + + private appendButtons(): void { + const cancelButton = this.createButton( + nls.localize('arduino/versionWelcome/cancelButton', 'Maybe later') + ); + cancelButton.classList.add('secondary'); + cancelButton.classList.add('cancel-button'); + this.addAction(cancelButton, this.close.bind(this), 'click'); + this.controlPanel.appendChild(cancelButton); + + const donateButton = this.createButton( + nls.localize('arduino/versionWelcome/donateButton', 'Donate now') + ); + this.addAction(donateButton, this.onDonateButtonClick.bind(this), 'click'); + this.controlPanel.appendChild(donateButton); + donateButton.focus(); + } + + private onDonateButtonClick(): void { + this.openDonationPage(); + this.close(); + } + + private readonly openDonationPage = () => { + const url = 'https://www.arduino.cc/en/donate'; + this.windowService.openNewWindow(url, { external: true }); + }; + + private async updateTitleVersion(): Promise { + const appInfo = await this.appService.info(); + const { appVersion } = appInfo; + + if (appVersion) { + this.titleNode.innerHTML = nls.localize( + 'arduino/versionWelcome/titleWithVersion', + 'Welcome to the new Arduino IDE {0}!', + appVersion + ); + } + } + + protected override onAfterAttach(msg: Message): void { + this.update(); + this.appendButtons(); + this.updateTitleVersion(); + super.onAfterAttach(msg); + } +} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index fd7887ae1..593cf1eaf 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -10,6 +10,7 @@ @import "./settings-dialog.css"; @import "./firmware-uploader-dialog.css"; @import "./ide-updater-dialog.css"; +@import "./version-welcome-dialog.css"; @import "./certificate-uploader-dialog.css"; @import "./user-fields-dialog.css"; @import "./debug.css"; diff --git a/arduino-ide-extension/src/browser/style/version-welcome-dialog.css b/arduino-ide-extension/src/browser/style/version-welcome-dialog.css new file mode 100644 index 000000000..80d6f71e4 --- /dev/null +++ b/arduino-ide-extension/src/browser/style/version-welcome-dialog.css @@ -0,0 +1,7 @@ +#version-welcome-dialog-container > .dialogBlock { + width: 546px; + + .bold { + font-weight: bold; + } +} diff --git a/arduino-ide-extension/src/common/protocol/ide-updater.ts b/arduino-ide-extension/src/common/protocol/ide-updater.ts index 5608e63be..ebc8bfef2 100644 --- a/arduino-ide-extension/src/common/protocol/ide-updater.ts +++ b/arduino-ide-extension/src/common/protocol/ide-updater.ts @@ -71,3 +71,4 @@ export interface IDEUpdaterClient { } export const SKIP_IDE_VERSION = 'skipIDEVersion'; +export const LAST_USED_IDE_VERSION = 'lastUsedIDEVersion'; diff --git a/i18n/en.json b/i18n/en.json index d7bd68b4f..b769cd868 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -528,6 +528,14 @@ "renameSketchFolderMessage": "The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?", "renameSketchFolderTitle": "Invalid sketch name" }, + "versionWelcome": { + "cancelButton": "Maybe later", + "donateButton": "Donate now", + "donateMessage": "Arduino is committed to keeping software free and open-source for everyone. Your donation helps us develop new features, improve libraries, and support millions of users worldwide.", + "donateMessage2": "Please consider supporting our work on the free open source Arduino IDE.", + "title": "Welcome to a new version of the Arduino IDE!", + "titleWithVersion": "Welcome to the new Arduino IDE {0}!" + }, "workspace": { "alreadyExists": "'{0}' already exists." }