mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-20 05:17:18 +00:00

* feat(GUI): dynamic finish page We implement an externally loaded dynamic finish page in React with `react2angular`. If the Internet connection is unreliable or unavailable, or a non-200 HTTP response is returned we display a fallback default finish banner. Change-Type: minor Changelog-Entry: Implement a dynamic finish page. Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
309 lines
8.8 KiB
JavaScript
309 lines
8.8 KiB
JavaScript
/*
|
|
* Copyright 2016 resin.io
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @module Etcher
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/* eslint-disable no-var */
|
|
|
|
var angular = require('angular');
|
|
|
|
/* eslint-enable no-var */
|
|
|
|
const electron = require('electron');
|
|
const Bluebird = require('bluebird');
|
|
const semver = require('semver');
|
|
const _ = require('lodash');
|
|
const EXIT_CODES = require('../shared/exit-codes');
|
|
const messages = require('../shared/messages');
|
|
const s3Packages = require('../shared/s3-packages');
|
|
const release = require('../shared/release');
|
|
const packageJSON = require('../../package.json');
|
|
const flashState = require('./models/flash-state');
|
|
const settings = require('./models/settings');
|
|
const windowProgress = require('./os/window-progress');
|
|
const analytics = require('./modules/analytics');
|
|
|
|
const Store = require('./models/store');
|
|
|
|
const app = angular.module('Etcher', [
|
|
require('angular-ui-router'),
|
|
require('angular-ui-bootstrap'),
|
|
require('angular-if-state'),
|
|
|
|
// Etcher modules
|
|
require('./modules/error'),
|
|
require('./modules/drive-scanner'),
|
|
|
|
// Models
|
|
require('./models/selection-state'),
|
|
require('./models/drives'),
|
|
|
|
// Components
|
|
require('./components/svg-icon/svg-icon'),
|
|
require('./components/update-notifier/update-notifier'),
|
|
require('./components/warning-modal/warning-modal'),
|
|
require('./components/safe-webview/safe-webview'),
|
|
|
|
// Pages
|
|
require('./pages/main/main'),
|
|
require('./pages/finish/finish'),
|
|
require('./pages/settings/settings'),
|
|
|
|
// OS
|
|
require('./os/open-external/open-external'),
|
|
require('./os/dropzone/dropzone'),
|
|
require('./os/dialog/dialog'),
|
|
|
|
// Utils
|
|
require('./utils/manifest-bind/manifest-bind')
|
|
]);
|
|
|
|
app.run(() => {
|
|
console.log([
|
|
' _____ _ _',
|
|
'| ___| | | |',
|
|
'| |__ | |_ ___| |__ ___ _ __',
|
|
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
|
|
'| |___| || (__| | | | __/ |',
|
|
'\\____/ \\__\\___|_| |_|\\___|_|',
|
|
'',
|
|
'Interested in joining the Etcher team?',
|
|
'Drop us a line at join+etcher@resin.io'
|
|
].join('\n'));
|
|
});
|
|
|
|
app.run((ErrorService, UpdateNotifierService, SelectionStateModel) => {
|
|
analytics.logEvent('Application start');
|
|
|
|
const currentVersion = packageJSON.version;
|
|
const currentReleaseType = release.getReleaseType(currentVersion);
|
|
const shouldCheckForUpdates = UpdateNotifierService.shouldCheckForUpdates({
|
|
ignoreSleepUpdateCheck: currentReleaseType !== release.RELEASE_TYPE.PRODUCTION
|
|
});
|
|
|
|
if (_.some([
|
|
!shouldCheckForUpdates,
|
|
process.env.ETCHER_DISABLE_UPDATES,
|
|
currentReleaseType === release.RELEASE_TYPE.UNKNOWN
|
|
])) {
|
|
analytics.logEvent('Not checking for updates', {
|
|
shouldCheckForUpdates,
|
|
disableUpdatesEnvironmentVariable: process.env.ETCHER_DISABLE_UPDATES,
|
|
releaseType: currentReleaseType
|
|
});
|
|
return;
|
|
}
|
|
|
|
const updateSemverRange = packageJSON.updates.semverRange;
|
|
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel');
|
|
|
|
analytics.logEvent('Checking for updates', {
|
|
currentVersion,
|
|
releaseType: currentReleaseType,
|
|
updateSemverRange,
|
|
includeUnstableChannel
|
|
});
|
|
|
|
s3Packages.getLatestVersion(currentReleaseType, {
|
|
range: updateSemverRange,
|
|
includeUnstableChannel
|
|
}).then((latestVersion) => {
|
|
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
|
|
analytics.logEvent('Update notification skipped', {
|
|
reason: 'Latest version'
|
|
});
|
|
return Bluebird.resolve();
|
|
}
|
|
|
|
// In case the internet connection is not good and checking the
|
|
// latest published version takes too long, only show notify
|
|
// the user about the new version if he didn't start the flash
|
|
// process (e.g: selected an image), otherwise such interruption
|
|
// might be annoying.
|
|
if (SelectionStateModel.hasImage()) {
|
|
analytics.logEvent('Update notification skipped', {
|
|
reason: 'Image selected'
|
|
});
|
|
return Bluebird.resolve();
|
|
}
|
|
|
|
analytics.logEvent('Notifying update', {
|
|
latestVersion
|
|
});
|
|
|
|
return UpdateNotifierService.notify(latestVersion, {
|
|
allowSleepUpdateCheck: currentReleaseType === release.RELEASE_TYPE.PRODUCTION
|
|
});
|
|
}).catch(ErrorService.reportException);
|
|
});
|
|
|
|
app.run(() => {
|
|
Store.subscribe(() => {
|
|
const currentFlashState = flashState.getFlashState();
|
|
|
|
// There is usually a short time period between the `isFlashing()`
|
|
// property being set, and the flashing actually starting, which
|
|
// might cause some non-sense flashing state logs including
|
|
// `undefined` values.
|
|
//
|
|
// We use the presence of `.eta` to determine that the actual
|
|
// writing started.
|
|
if (!flashState.isFlashing() || !currentFlashState.eta) {
|
|
return;
|
|
}
|
|
|
|
analytics.logDebug([
|
|
`Progress (${currentFlashState.type}):`,
|
|
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s`,
|
|
`(eta ${currentFlashState.eta}s)`
|
|
].join(' '));
|
|
|
|
windowProgress.set(currentFlashState.percentage);
|
|
});
|
|
});
|
|
|
|
app.run(($timeout, DriveScannerService, DrivesModel, ErrorService) => {
|
|
DriveScannerService.on('drives', (drives) => {
|
|
|
|
// Safely trigger a digest cycle.
|
|
// In some cases, AngularJS doesn't acknowledge that the
|
|
// available drives list has changed, and incorrectly
|
|
// keeps asking the user to "Connect a drive".
|
|
$timeout(() => {
|
|
DrivesModel.setDrives(drives);
|
|
});
|
|
});
|
|
|
|
DriveScannerService.on('error', (error) => {
|
|
|
|
// Stop the drive scanning loop in case of errors,
|
|
// otherwise we risk presenting the same error over
|
|
// and over again to the user, while also heavily
|
|
// spamming our error reporting service.
|
|
DriveScannerService.stop();
|
|
|
|
return ErrorService.reportException(error);
|
|
});
|
|
|
|
DriveScannerService.start();
|
|
});
|
|
|
|
app.run(($window, WarningModalService, ErrorService, OSDialogService) => {
|
|
let popupExists = false;
|
|
|
|
$window.addEventListener('beforeunload', (event) => {
|
|
if (!flashState.isFlashing() || popupExists) {
|
|
analytics.logEvent('Close application', {
|
|
isFlashing: flashState.isFlashing()
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Don't close window while flashing
|
|
event.returnValue = false;
|
|
|
|
// Don't open any more popups
|
|
popupExists = true;
|
|
|
|
analytics.logEvent('Close attempt while flashing');
|
|
|
|
OSDialogService.showWarning({
|
|
confirmationLabel: 'Yes, quit',
|
|
rejectionLabel: 'Cancel',
|
|
title: 'Are you sure you want to close Etcher?',
|
|
description: messages.warning.exitWhileFlashing()
|
|
}).then((confirmed) => {
|
|
if (confirmed) {
|
|
analytics.logEvent('Close confirmed while flashing', {
|
|
uuid: flashState.getFlashUuid()
|
|
});
|
|
|
|
// This circumvents the 'beforeunload' event unlike
|
|
// electron.remote.app.quit() which does not.
|
|
electron.remote.process.exit(EXIT_CODES.SUCCESS);
|
|
|
|
}
|
|
|
|
analytics.logEvent('Close rejected while flashing');
|
|
popupExists = false;
|
|
}).catch(ErrorService.reportException);
|
|
});
|
|
});
|
|
|
|
app.run(($rootScope) => {
|
|
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
|
|
|
// Ignore first navigation
|
|
if (!fromState.name) {
|
|
return;
|
|
}
|
|
|
|
analytics.logEvent('Navigate', {
|
|
to: toState.name,
|
|
from: fromState.name
|
|
});
|
|
});
|
|
});
|
|
|
|
app.config(($urlRouterProvider) => {
|
|
$urlRouterProvider.otherwise('/main');
|
|
});
|
|
|
|
app.config(($provide) => {
|
|
$provide.decorator('$exceptionHandler', ($delegate, $injector) => {
|
|
return (exception, cause) => {
|
|
const ErrorService = $injector.get('ErrorService');
|
|
ErrorService.reportException(exception);
|
|
$delegate(exception, cause);
|
|
};
|
|
});
|
|
});
|
|
|
|
app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalService) {
|
|
|
|
/**
|
|
* @summary Open help page
|
|
* @function
|
|
* @public
|
|
*
|
|
* @description
|
|
* This application will open either the image's support url, declared
|
|
* in the archive `manifest.json`, or the default Etcher help page.
|
|
*
|
|
* @example
|
|
* HeaderController.openHelpPage();
|
|
*/
|
|
this.openHelpPage = () => {
|
|
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md';
|
|
const supportUrl = SelectionStateModel.getImageSupportUrl() || DEFAULT_SUPPORT_URL;
|
|
OSOpenExternalService.open(supportUrl);
|
|
};
|
|
|
|
});
|
|
|
|
app.controller('StateController', function($state) {
|
|
|
|
/**
|
|
* @param {string} state - state page
|
|
*/
|
|
this.is = $state.is;
|
|
|
|
});
|