diff --git a/.gitignore b/.gitignore index c607c38f8..a9aaff62a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ UserInterfaceState.xcuserstate e2e/build /desktop/* !/desktop/assets/ +!/desktop/electron.js src/configconstants.ts src/assets/lang diff --git a/config/webpack.config.js b/config/webpack.config.js index 203780578..3f622bcd3 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -14,6 +14,25 @@ const customConfig = { '@directives': resolve('./src/directives'), '@pipes': resolve('./src/pipes') } + }, + externals: [ + (function () { + var IGNORES = ["fs","child_process","electron","path","assert","cluster","crypto","dns","domain","events","http","https","net","os","process","punycode","querystring","readline","repl","stream","string_decoder","tls","tty","dgram","url","util","v8","vm","zlib"]; + return function (context, request, callback) { + if (IGNORES.indexOf(request) >= 0) { + return callback(null, "require('" + request + "')"); + } + return callback(); + }; + })() + ], + module: { + loaders: [ + { + test: /\.node$/, + use: 'node-loader' + } + ] } }; diff --git a/desktop/electron.js b/desktop/electron.js new file mode 100644 index 000000000..51087a40d --- /dev/null +++ b/desktop/electron.js @@ -0,0 +1,253 @@ + +// dialog isn't used, but not requiring it throws an error. +const {app, BrowserWindow, ipcMain, shell, dialog, Menu} = require('electron'); +const path = require('path'); +const url = require('url'); +const fs = require('fs'); +const os = require('os'); + +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow, + appName = 'Moodle Desktop', // Default value. + isReady = false, + configRead = false; + +function createWindow() { + // Create the browser window. + var width = 800, + height = 600; + + const screen = require('electron').screen; + if (screen) { + const display = screen.getPrimaryDisplay(); + if (display && display.workArea) { + width = display.workArea.width || width; + height = display.workArea.height || height; + } + } + + const options = { + width: width, + height: height, + minWidth: 400, + minHeight: 400, + textAreasAreResizable: false, + plugins: true, + show: false // Don't show it until it's ready to prevent showing a blank screen. + }; + + if (os.platform().indexOf('linux') === 0) { + options.icon = path.join(__dirname, '/../www/assets/icon/icon.png'); + } + + mainWindow = new BrowserWindow(options); + + // And load the index.html of the app. + mainWindow.loadURL(url.format({ + pathname: path.join(__dirname, '/../www/index.html'), + protocol: 'file:', + slashes: true + })); + + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + mainWindow.maximize(); + }); + + // Emitted when the window is closed. + mainWindow.on('closed', () => { + // Dereference the window object. + mainWindow = null + }); + + mainWindow.on('focus', () => { + mainWindow.webContents.send('coreAppFocused'); // Send an event to the main window. + }); +} + +// This method will be called when Electron has finished initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', function() { + isReady = true; + + createWindow(); + + if (configRead) { + setAppMenu(); + } +}); + +// Quit when all windows are closed. +app.on('window-all-closed', () => { + app.exit(); +}); + +app.on('activate', () => { + // On macOS it's common to re-create a window in the app when the dock icon is clicked and there are no other windows open. + if (mainWindow === null) { + createWindow(); + } +}); + +// Read the config.json file. +fs.readFile(path.join(__dirname, 'config.json'), 'utf8', (err, data) => { + configRead = true; + + // Default values. + var ssoScheme = 'moodlemobile', + appId = 'com.moodle.moodlemobile'; + + if (!err) { + try { + data = JSON.parse(data); + ssoScheme = data.customurlscheme; + appName = data.desktopappname; + appId = data.app_id; + } catch(ex) {} + } + + // Set default protocol (custom URL scheme). + app.setAsDefaultProtocolClient(ssoScheme); + + // Fix notifications in Windows. + app.setAppUserModelId(appId); + + if (isReady) { + setAppMenu(); + } +}); + +// Make sure that only a single instance of the app is running. +var shouldQuit = app.makeSingleInstance((argv, workingDirectory) => { + // Another instance was launched. If it was launched with a URL, it should be in the second param. + if (argv && argv[1]) { + appLaunched(argv[1]); + } else { + focusApp(); + } +}); + +// For some reason, shouldQuit is always true in signed Mac apps so we should ingore it. +if (shouldQuit && os.platform().indexOf('darwin') == -1) { + // It's not the main instance of the app, kill it. + app.exit(); + return; +} + +// Listen for open-url events (Mac OS only). +app.on('open-url', (event, url) => { + event.preventDefault(); + appLaunched(url); +}); + +function appLaunched(url) { + // App was launched again with a URL. Focus the main window and send an event to treat the URL. + if (mainWindow) { + focusApp(); + mainWindow.webContents.send('coreAppLaunched', url); // Send an event to the main window. + } +} + +function focusApp() { + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + mainWindow.focus(); + } +} + +// Listen for events sent by the renderer processes (windows). +ipcMain.on('openItem', (event, path) => { + var result; + + // Add file:// protocol if it isn't there. + if (path.indexOf('file://') == -1) { + path = 'file://' + path; + } + + if (os.platform().indexOf('darwin') > -1) { + // Use openExternal in MacOS because openItem doesn't work in sandboxed apps. + // https://github.com/electron/electron/issues/9005 + result = shell.openExternal(path); + } else { + result = shell.openItem(path); + } + + if (!result) { + // Cannot open file, probably no app to handle it. Open the folder. + result = shell.showItemInFolder(path.replace('file://', '')); + } + + event.returnValue = result; +}); + +ipcMain.on('closeSecondaryWindows', () => { + const windows = BrowserWindow.getAllWindows(); + for (let i = 0; i < windows.length; i++) { + const win = windows[i]; + if (!win.isDestroyed() && (!mainWindow || win.id != mainWindow.id)) { + win.close(); + } + } +}); + +ipcMain.on('focusApp', focusApp); + +// Configure the app's menu. +function setAppMenu() { + let menuTemplate = [ + { + label: appName, + role: 'window', + submenu: [ + { + label: 'Quit', + accelerator: 'CmdorCtrl+Q', + role: 'close' + } + ] + }, + { + label: 'Edit', + submenu: [ + { + label: 'Cut', + accelerator: 'CmdOrCtrl+X', + role: 'cut' + }, + { + label: 'Copy', + accelerator: 'CmdOrCtrl+C', + role: 'copy' + }, + { + label: 'Paste', + accelerator: 'CmdOrCtrl+V', + role: 'paste' + }, + { + label: 'Select All', + accelerator: 'CmdOrCtrl+A', + role: 'selectall' + } + ] + }, + { + label: 'Help', + role: 'help', + submenu: [ + { + label: 'Docs', + accelerator: 'CmdOrCtrl+H', + click() { + shell.openExternal('https://docs.moodle.org/en/Moodle_Mobile'); + } + } + ] + } + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate)); +} diff --git a/package-lock.json b/package-lock.json index ecea0ccd9..f88b6a810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.5.0", + "version": "3.5.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -284,6 +284,12 @@ "resolved": "https://registry.npmjs.org/@types/promise.prototype.finally/-/promise.prototype.finally-2.0.2.tgz", "integrity": "sha512-Fs99h+iFQZ4ZY2vO3+uJCrx+5KQnJ4FPerZ3oT/1L5aA7vnmK/d7Z/Ml1yHtNCh9UQcjFTR4Xo/Jss2f39Fgtw==" }, + "7zip-bin-mac": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/7zip-bin-mac/-/7zip-bin-mac-1.0.1.tgz", + "integrity": "sha1-Pmh3i78JJq3GgVlCcHRQXUdVXAI=", + "optional": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -395,7 +401,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "1.9.2" }, @@ -404,7 +409,6 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", - "dev": true, "requires": { "color-name": "1.1.1" } @@ -412,8 +416,7 @@ "color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", - "dev": true + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" } } }, @@ -432,12 +435,47 @@ "normalize-path": "2.1.1" } }, + "app-builder-bin": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-1.10.3.tgz", + "integrity": "sha512-DQva42HxatQkic4T2ybHpELsBlesj3ftMmSJkaSRP9N5q9qiQHjQO7t8o6j6nTKyKSNF7xr1jQtevyVvAPq9Wg==", + "optional": true + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true }, + "archiver": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", + "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=", + "optional": true, + "requires": { + "archiver-utils": "1.3.0", + "async": "2.6.1", + "buffer-crc32": "0.2.13", + "glob": "7.1.2", + "lodash": "4.17.10", + "readable-stream": "2.3.6", + "tar-stream": "1.6.1", + "zip-stream": "1.2.0" + } + }, + "archiver-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", + "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", + "requires": { + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lazystream": "1.0.0", + "lodash": "4.17.10", + "normalize-path": "2.1.1", + "readable-stream": "2.3.6" + } + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -458,7 +496,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "1.0.3" } @@ -582,7 +619,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "dev": true, "requires": { "lodash": "4.17.10" } @@ -592,6 +628,12 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=" }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "optional": true + }, "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", @@ -785,6 +827,16 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "optional": true, + "requires": { + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -794,6 +846,19 @@ "inherits": "2.0.3" } }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bluebird-lst": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", + "integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==", + "requires": { + "bluebird": "3.5.1" + } + }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -996,6 +1061,33 @@ "isarray": "1.0.0" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "optional": true, + "requires": { + "buffer-alloc-unsafe": "1.1.0", + "buffer-fill": "1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "optional": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "optional": true + }, "buffer-from": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", @@ -1007,6 +1099,29 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, + "builder-util-runtime": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.0.tgz", + "integrity": "sha512-tkTF1o7XAX79ZkMo8822ZdQMpEBGSgfJ9kEYgyTAja90BPu7HO8C02pb8iSlFXfmK0Q0UA6D8MmnSNNPi0JLeg==", + "optional": true, + "requires": { + "bluebird-lst": "1.0.5", + "debug": "3.1.0", + "fs-extra-p": "4.6.1", + "sax": "1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "optional": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -1092,7 +1207,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, "requires": { "ansi-styles": "3.2.1", "escape-string-regexp": "1.0.5", @@ -1141,6 +1255,12 @@ "readdirp": "2.1.0" } }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "optional": true + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -1197,6 +1317,21 @@ } } }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==", + "dev": true + }, "cliui": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", @@ -1258,6 +1393,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "colors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", + "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==", + "dev": true + }, "combined-stream": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", @@ -1279,6 +1420,18 @@ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", "dev": true }, + "compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "optional": true, + "requires": { + "buffer-crc32": "0.2.13", + "crc32-stream": "2.0.0", + "normalize-path": "2.1.1", + "readable-stream": "2.3.6" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1341,6 +1494,505 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "cordova-android": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-6.1.2.tgz", + "integrity": "sha1-lgZzmhFPCjf0kFPjZMlthPtGK/g=", + "requires": { + "cordova-common": "1.5.1", + "elementtree": "0.1.6", + "nopt": "3.0.6", + "properties-parser": "0.2.3", + "q": "1.4.1", + "shelljs": "0.5.3" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "bundled": true + }, + "ansi": { + "version": "0.3.1", + "bundled": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true + }, + "base64-js": { + "version": "0.0.8", + "bundled": true + }, + "big-integer": { + "version": "1.6.16", + "bundled": true + }, + "bplist-parser": { + "version": "0.1.1", + "bundled": true, + "requires": { + "big-integer": "1.6.16" + } + }, + "brace-expansion": { + "version": "1.1.6", + "bundled": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "cordova-common": { + "version": "1.5.1", + "bundled": true, + "requires": { + "ansi": "0.3.1", + "bplist-parser": "0.1.1", + "cordova-registry-mapper": "1.1.15", + "elementtree": "0.1.6", + "glob": "5.0.15", + "minimatch": "3.0.3", + "osenv": "0.1.3", + "plist": "1.2.0", + "q": "1.4.1", + "semver": "5.3.0", + "shelljs": "0.5.3", + "underscore": "1.8.3", + "unorm": "1.4.1" + } + }, + "cordova-registry-mapper": { + "version": "1.1.15", + "bundled": true + }, + "elementtree": { + "version": "0.1.6", + "bundled": true, + "requires": { + "sax": "0.3.5" + } + }, + "glob": { + "version": "5.0.15", + "bundled": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.3", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "lodash": { + "version": "3.10.1", + "bundled": true + }, + "minimatch": { + "version": "3.0.3", + "bundled": true, + "requires": { + "brace-expansion": "1.1.6" + } + }, + "nopt": { + "version": "3.0.6", + "bundled": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.3", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "plist": { + "version": "1.2.0", + "bundled": true, + "requires": { + "base64-js": "0.0.8", + "util-deprecate": "1.0.2", + "xmlbuilder": "4.0.0", + "xmldom": "0.1.22" + } + }, + "properties-parser": { + "version": "0.2.3", + "bundled": true + }, + "q": { + "version": "1.4.1", + "bundled": true + }, + "sax": { + "version": "0.3.5", + "bundled": true + }, + "semver": { + "version": "5.3.0", + "bundled": true + }, + "shelljs": { + "version": "0.5.3", + "bundled": true + }, + "underscore": { + "version": "1.8.3", + "bundled": true + }, + "unorm": { + "version": "1.4.1", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "xmlbuilder": { + "version": "4.0.0", + "bundled": true, + "requires": { + "lodash": "3.10.1" + } + }, + "xmldom": { + "version": "0.1.22", + "bundled": true + } + } + }, + "cordova-ios": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/cordova-ios/-/cordova-ios-4.3.1.tgz", + "integrity": "sha1-e+zJUBROx6wfVgzphesHv+4bd/E=", + "requires": { + "cordova-common": "1.5.1", + "ios-sim": "5.0.12", + "nopt": "3.0.6", + "plist": "1.2.0", + "q": "1.4.1", + "shelljs": "0.5.3", + "xcode": "0.8.9" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "bundled": true + }, + "ansi": { + "version": "0.3.1", + "bundled": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true + }, + "base64-js": { + "version": "0.0.8", + "bundled": true + }, + "big-integer": { + "version": "1.6.17", + "bundled": true + }, + "bplist-creator": { + "version": "0.0.4", + "bundled": true, + "requires": { + "stream-buffers": "0.2.6" + } + }, + "bplist-parser": { + "version": "0.1.1", + "bundled": true, + "requires": { + "big-integer": "1.6.17" + } + }, + "brace-expansion": { + "version": "1.1.6", + "bundled": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "cordova-common": { + "version": "1.5.1", + "bundled": true, + "requires": { + "ansi": "0.3.1", + "bplist-parser": "0.1.1", + "cordova-registry-mapper": "1.1.15", + "elementtree": "0.1.6", + "glob": "5.0.15", + "minimatch": "3.0.3", + "osenv": "0.1.3", + "plist": "1.2.0", + "q": "1.4.1", + "semver": "5.3.0", + "shelljs": "0.5.3", + "underscore": "1.8.3", + "unorm": "1.4.1" + } + }, + "cordova-registry-mapper": { + "version": "1.1.15", + "bundled": true + }, + "elementtree": { + "version": "0.1.6", + "bundled": true, + "requires": { + "sax": "0.3.5" + } + }, + "glob": { + "version": "5.0.15", + "bundled": true, + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.3", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ios-sim": { + "version": "5.0.12", + "bundled": true, + "requires": { + "bplist-parser": "0.0.6", + "nopt": "1.0.9", + "plist": "1.2.0", + "simctl": "0.1.0" + }, + "dependencies": { + "bplist-parser": { + "version": "0.0.6", + "bundled": true + }, + "nopt": { + "version": "1.0.9", + "bundled": true, + "requires": { + "abbrev": "1.0.9" + } + } + } + }, + "lodash": { + "version": "3.10.1", + "bundled": true + }, + "minimatch": { + "version": "3.0.3", + "bundled": true, + "requires": { + "brace-expansion": "1.1.6" + } + }, + "node-uuid": { + "version": "1.4.7", + "bundled": true + }, + "nopt": { + "version": "3.0.6", + "bundled": true, + "requires": { + "abbrev": "1.0.9" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.3", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "pegjs": { + "version": "0.9.0", + "bundled": true + }, + "plist": { + "version": "1.2.0", + "bundled": true, + "requires": { + "base64-js": "0.0.8", + "util-deprecate": "1.0.2", + "xmlbuilder": "4.0.0", + "xmldom": "0.1.27" + } + }, + "q": { + "version": "1.4.1", + "bundled": true + }, + "sax": { + "version": "0.3.5", + "bundled": true + }, + "semver": { + "version": "5.3.0", + "bundled": true + }, + "shelljs": { + "version": "0.5.3", + "bundled": true + }, + "simctl": { + "version": "0.1.0", + "bundled": true, + "requires": { + "shelljs": "0.2.6", + "tail": "0.4.0" + }, + "dependencies": { + "shelljs": { + "version": "0.2.6", + "bundled": true + } + } + }, + "simple-plist": { + "version": "0.1.4", + "bundled": true, + "requires": { + "bplist-creator": "0.0.4", + "bplist-parser": "0.0.6", + "plist": "1.2.0" + }, + "dependencies": { + "bplist-parser": { + "version": "0.0.6", + "bundled": true + } + } + }, + "stream-buffers": { + "version": "0.2.6", + "bundled": true + }, + "tail": { + "version": "0.4.0", + "bundled": true + }, + "underscore": { + "version": "1.8.3", + "bundled": true + }, + "unorm": { + "version": "1.4.1", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "xcode": { + "version": "0.8.9", + "bundled": true, + "requires": { + "node-uuid": "1.4.7", + "pegjs": "0.9.0", + "simple-plist": "0.1.4" + } + }, + "xmlbuilder": { + "version": "4.0.0", + "bundled": true, + "requires": { + "lodash": "3.10.1" + } + }, + "xmldom": { + "version": "0.1.27", + "bundled": true + } + } + }, "cordova-plugin-file": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.1.tgz", @@ -1356,6 +2008,22 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crc": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz", + "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=", + "optional": true + }, + "crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "optional": true, + "requires": { + "crc": "3.5.0", + "readable-stream": "2.3.6" + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -1483,7 +2151,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -1617,6 +2284,12 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -1715,12 +2388,129 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "electron-builder-squirrel-windows": { + "version": "20.19.0", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-20.19.0.tgz", + "integrity": "sha512-iFAtCoC0FyX8dJUvoY+NPUskdKPk7k+IyWv8GeNx/lXGxWmIo6kixXQwqCl51RTCwWvffc+Z9rssvsBx+NZvtQ==", + "optional": true, + "requires": { + "7zip-bin": "3.1.0", + "archiver": "2.1.1", + "bluebird-lst": "1.0.5", + "builder-util": "5.13.2", + "fs-extra-p": "4.6.1", + "sanitize-filename": "1.6.1" + }, + "dependencies": { + "7zip-bin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-3.1.0.tgz", + "integrity": "sha512-juYJNi8JEpTUWXwz8ssa8Oop4n/kwJ/pIQP22vJAVAe6RTRD+0m+e9LRNnfK2EDaX8uwmUzLNGviFQRD6SxeOw==", + "optional": true, + "requires": { + "7zip-bin-mac": "1.0.1" + } + }, + "builder-util": { + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-5.13.2.tgz", + "integrity": "sha512-pdt5hkCS69Hhy6b8VuZbLi9fYtjkhah2QPB7Rwg7/475kFOhMEkbYhGckfDPcdT64Vk+1RHtZd2oiNiZNyaZ3w==", + "optional": true, + "requires": { + "7zip-bin": "4.0.2", + "app-builder-bin": "1.10.3", + "bluebird-lst": "1.0.5", + "builder-util-runtime": "4.4.0", + "chalk": "2.4.1", + "debug": "3.1.0", + "fs-extra-p": "4.6.1", + "is-ci": "1.1.0", + "js-yaml": "3.12.0", + "lazy-val": "1.0.3", + "semver": "5.5.0", + "source-map-support": "0.5.6", + "stat-mode": "0.2.2", + "temp-file": "3.1.3" + }, + "dependencies": { + "7zip-bin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-4.0.2.tgz", + "integrity": "sha512-XtGk+IF57pr852UK1AhQJXqmm1WmSgS5uISL+LPs0z/iAxXouMvdlLJrHPeukP6gd7yR2rDTMSMkHNODgwIq7A==", + "optional": true + } + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "optional": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "electron-rebuild": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-1.8.1.tgz", + "integrity": "sha512-aCNKjlHyY4yqmavth/BVbmYhuL/3hrLIhg5P0fIQzGrlFA4V9TmA8FTlhjq4fT+FgDOynPBhHS5ZWLMU6ZUh3A==", + "dev": true, + "requires": { + "colors": "1.3.0", + "debug": "2.6.9", + "detect-libc": "1.0.3", + "fs-extra": "3.0.1", + "node-abi": "2.4.3", + "node-gyp": "3.7.0", + "ora": "1.4.0", + "rimraf": "2.6.2", + "spawn-rx": "2.0.12", + "yargs": "7.1.0" + }, + "dependencies": { + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.1" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + } + } + }, "electron-to-chromium": { "version": "1.3.48", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.48.tgz", "integrity": "sha1-07DYWTgUBE4JLs4hCPw6ya6kuQA=", "dev": true }, + "electron-windows-notifications": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/electron-windows-notifications/-/electron-windows-notifications-2.1.1.tgz", + "integrity": "sha512-RDPUlSOKrcuo7+eqqv9TPkJgNIO6E1SJzqx6LBTKTAvvHn9c2lRPIsrPJbKSRVC2V82SSv2358TbEkFC85tTVA==", + "optional": true, + "requires": { + "debug": "2.6.9", + "is-electron-renderer": "2.0.1", + "sanitize-xml-string": "1.1.0", + "uuid": "3.2.1", + "xml-escape": "1.1.0" + } + }, "elliptic": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", @@ -1915,8 +2705,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escope": { "version": "3.6.0", @@ -1933,8 +2722,7 @@ "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" }, "esrecurse": { "version": "4.2.1", @@ -2597,6 +3385,12 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", @@ -2608,11 +3402,31 @@ "universalify": "0.1.1" } }, + "fs-extra-p": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.1.tgz", + "integrity": "sha512-IsTMbUS0svZKZTvqF4vDS9c/L7Mw9n8nZQWWeSzAGacOSe+8CzowhUN0tdZEZFIJNP5HC7L9j3MMikz/G4hDeQ==", + "requires": { + "bluebird-lst": "1.0.5", + "fs-extra": "6.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.4", @@ -3178,7 +3992,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -3668,8 +4481,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-gulplog": { "version": "0.1.0", @@ -3887,7 +4699,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -3984,6 +4795,15 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=" }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "optional": true, + "requires": { + "ci-info": "1.1.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -4022,6 +4842,12 @@ "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" }, + "is-electron-renderer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz", + "integrity": "sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=", + "optional": true + }, "is-equal-shallow": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", @@ -4245,7 +5071,6 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "dev": true, "requires": { "argparse": "1.0.10", "esprima": "4.0.0" @@ -4292,7 +5117,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, "requires": { "graceful-fs": "4.1.11" } @@ -4374,6 +5198,19 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "dev": true }, + "lazy-val": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", + "integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg==" + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "requires": { + "readable-stream": "2.3.6" + } + }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -4464,8 +5301,7 @@ "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "lodash._basecopy": { "version": "3.0.1", @@ -4604,6 +5440,15 @@ "lodash.escape": "3.2.0" } }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "2.4.1" + } + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -4879,8 +5724,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multipipe": { "version": "0.1.2", @@ -4960,6 +5804,15 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "node-abi": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.4.3.tgz", + "integrity": "sha512-b656V5C0628gOOA2kwcpNA/bxdlqYF9FvxJ+qqVX0ctdXNVZpS8J6xEUYir3WAKc7U0BH/NRlSpNbGsy+azjeg==", + "dev": true, + "requires": { + "semver": "5.5.0" + } + }, "node-gyp": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz", @@ -5019,6 +5872,12 @@ "vm-browserify": "0.0.4" } }, + "node-loader": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-0.6.0.tgz", + "integrity": "sha1-x5fvUQle1YWZArFX9jhPY2HgWug=", + "dev": true + }, "node-sass": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", @@ -5296,11 +6155,31 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1.0.2" } }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "ora": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-1.4.0.tgz", + "integrity": "sha512-iMK1DOQxzzh2MBlVsU42G80mnrvUhqsMh74phHtDlrcTZPK0pH6o7l7DRshK+0YsxDyEuaOkziVdvM3T0QTzpw==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-spinners": "1.3.1", + "log-symbols": "2.2.0" + } + }, "orchestrator": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", @@ -6023,6 +6902,16 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -6132,6 +7021,21 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sanitize-filename": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", + "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", + "optional": true, + "requires": { + "truncate-utf8-bytes": "1.0.2" + } + }, + "sanitize-xml-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/sanitize-xml-string/-/sanitize-xml-string-1.1.0.tgz", + "integrity": "sha512-RzX25K64YtZm9FvdZr/Ac7Eeq0va1YX0xmpOkjWoREhgKXXldrJRVJhBel83nS8omIcaKcNTdLY8XzOIK920HA==", + "optional": true + }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", @@ -6147,8 +7051,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "scss-tokenizer": { "version": "0.2.3", @@ -6174,8 +7077,7 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "send": { "version": "0.16.2", @@ -6478,6 +7380,17 @@ "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", "dev": true }, + "spawn-rx": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-2.0.12.tgz", + "integrity": "sha512-gOPXiQQFQ9lTOLuys0iMn3jfxxv9c7zzwhbYLOEbQGvEShHVJ5sSR1oD3Daj88os7jKArDYT7rbOKdvNhe7iEg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "lodash.assign": "4.2.0", + "rxjs": "5.5.11" + } + }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -6522,8 +7435,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.14.2", @@ -6550,6 +7462,12 @@ } } }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "optional": true + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -6683,7 +7601,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, "requires": { "has-flag": "3.0.0" } @@ -6719,6 +7636,44 @@ "inherits": "2.0.3" } }, + "tar-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", + "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "optional": true, + "requires": { + "bl": "1.2.2", + "buffer-alloc": "1.2.0", + "end-of-stream": "1.4.1", + "fs-constants": "1.0.0", + "readable-stream": "2.3.6", + "to-buffer": "1.1.1", + "xtend": "4.0.1" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "optional": true, + "requires": { + "once": "1.4.0" + } + } + } + }, + "temp-file": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.1.3.tgz", + "integrity": "sha512-oz2J77loDE9sGrlRTqBzwbsUvoBD2BpyXeaRPKyGwBIwaamSs2jdqAfhutw7Tch9llr1u8E2ruoug09rNPa3PA==", + "optional": true, + "requires": { + "async-exit-hook": "2.0.1", + "bluebird-lst": "1.0.5", + "fs-extra-p": "4.6.1", + "lazy-val": "1.0.3" + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -6790,6 +7745,12 @@ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", "dev": true }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "optional": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -6871,6 +7832,15 @@ } } }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "optional": true, + "requires": { + "utf8-byte-length": "1.0.4" + } + }, "ts-md5": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.4.tgz", @@ -7115,8 +8085,7 @@ "universalify": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", - "dev": true + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" }, "unpipe": { "version": "1.0.0", @@ -7223,6 +8192,12 @@ "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", "dev": true }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", + "optional": true + }, "util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -7246,8 +8221,7 @@ "uuid": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "v8flags": { "version": "2.1.1", @@ -8046,8 +9020,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { "version": "3.3.2", @@ -8060,6 +9033,12 @@ "ultron": "1.1.1" } }, + "xml-escape": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz", + "integrity": "sha1-OQTBQ/qOs6ADDsZG0pAqLxtwbEQ=", + "optional": true + }, "xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", @@ -8079,8 +9058,7 @@ "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" }, "y18n": { "version": "3.2.1", @@ -8140,6 +9118,18 @@ } } }, + "zip-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", + "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", + "optional": true, + "requires": { + "archiver-utils": "1.3.0", + "compress-commons": "1.2.2", + "lodash": "4.17.10", + "readable-stream": "2.3.6" + } + }, "zone.js": { "version": "0.8.26", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.26.tgz", diff --git a/package.json b/package.json index 53e69fe03..f3ced9ef8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,10 @@ "ionic:build:before": "gulp", "ionic:watch:before": "gulp", "ionic:build:after": "gulp copy-component-templates", - "postionic:build": "gulp copy-component-templates" + "postionic:build": "gulp copy-component-templates", + "desktop.pack": "electron-builder --dir", + "desktop.dist": "electron-builder", + "windows.store": "electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store --flatten true -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop" }, "dependencies": { "@angular/animations": "5.2.10", @@ -88,15 +91,21 @@ }, "devDependencies": { "@ionic/app-scripts": "3.1.9", + "electron-rebuild": "^1.8.1", "gulp": "^3.9.1", "gulp-clip-empty-files": "^0.1.2", "gulp-flatten": "^0.4.0", "gulp-rename": "^1.2.2", "gulp-slash": "^1.1.3", + "node-loader": "^0.6.0", "through": "^2.3.8", "typescript": "~2.6.2", "webpack-merge": "^4.1.2" }, + "optionalDependencies": { + "electron-windows-notifications": "^2.1.1", + "electron-builder-squirrel-windows": "^20.19.0" + }, "browser": { "electron": false }, @@ -105,5 +114,57 @@ "android", "ios" ] + }, + "main": "desktop/electron.js", + "build": { + "appId": "com.moodle.moodledesktop", + "productName": "Moodle Desktop", + "files": [ + "desktop/electron.js", + "www/**/*", + "!config", + "!desktop/assets", + "!desktop/dist", + "!node_modules", + "!**/e2e", + "!hooks", + "!platforms", + "!plugins", + "!resources", + "!src", + "!**/*.scss" + ], + "directories": { + "output": "desktop/dist" + }, + "protocols": [ + { + "name": "Moodle Mobile URL", + "schemes": [ + "moodlemobile" + ], + "role": "Viewer" + } + ], + "compression": "maximum", + "electronVersion": "2.0.4", + "mac": { + "category": "public.app-category.education", + "icon": "resources/desktop/icon.icns", + "target": "mas", + "extendInfo": { + "ElectronTeamID": "2NU57U5PAW" + } + }, + "win": { + "target": "appx", + "icon": "resources/desktop/icon.ico" + }, + "linux": { + "category": "Education" + }, + "snap": { + "confinement": "classic" + } } -} \ No newline at end of file +} diff --git a/resources/desktop/Square150x150Logo.png b/resources/desktop/Square150x150Logo.png index dd059984b..9d9206792 100644 Binary files a/resources/desktop/Square150x150Logo.png and b/resources/desktop/Square150x150Logo.png differ diff --git a/resources/desktop/Square44x44Logo.png b/resources/desktop/Square44x44Logo.png index b1ad9e9bd..6db08c208 100644 Binary files a/resources/desktop/Square44x44Logo.png and b/resources/desktop/Square44x44Logo.png differ diff --git a/resources/desktop/StoreLogo.png b/resources/desktop/StoreLogo.png index fa5cebb17..de5b0f4c6 100644 Binary files a/resources/desktop/StoreLogo.png and b/resources/desktop/StoreLogo.png differ diff --git a/resources/desktop/Wide310x150Logo.png b/resources/desktop/Wide310x150Logo.png index 70e54eb73..0b7e6c110 100644 Binary files a/resources/desktop/Wide310x150Logo.png and b/resources/desktop/Wide310x150Logo.png differ diff --git a/resources/desktop/icon.icns b/resources/desktop/icon.icns index 602b37712..6e02dbf03 100644 Binary files a/resources/desktop/icon.icns and b/resources/desktop/icon.icns differ diff --git a/resources/desktop/icon.ico b/resources/desktop/icon.ico index c65354c81..4c516e7b5 100644 Binary files a/resources/desktop/icon.ico and b/resources/desktop/icon.ico differ diff --git a/src/addon/mod/lti/providers/lti.ts b/src/addon/mod/lti/providers/lti.ts index f2a453ce6..64f6f6e6a 100644 --- a/src/addon/mod/lti/providers/lti.ts +++ b/src/addon/mod/lti/providers/lti.ts @@ -14,6 +14,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; import { CoreSitesProvider } from '@providers/sites'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -40,7 +41,8 @@ export class AddonModLtiProvider { private textUtils: CoreTextUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider, - private translate: TranslateService) {} + private translate: TranslateService, + private appProvider: CoreAppProvider) {} /** * Delete launcher. @@ -84,7 +86,11 @@ export class AddonModLtiProvider { ' \n'; return this.fileProvider.writeFile(this.LAUNCHER_FILE_NAME, text).then((entry) => { - return entry.toURL(); + if (this.appProvider.isDesktop()) { + return entry.toInternalURL(); + } else { + return entry.toURL(); + } }); } diff --git a/src/addon/mod/lti/providers/module-handler.ts b/src/addon/mod/lti/providers/module-handler.ts index dfeff3028..c88705911 100644 --- a/src/addon/mod/lti/providers/module-handler.ts +++ b/src/addon/mod/lti/providers/module-handler.ts @@ -96,7 +96,10 @@ export class AddonModLtiModuleHandler implements CoreCourseModuleHandler { const icon = ltiData.secureicon || ltiData.icon; if (icon) { const siteId = this.sitesProvider.getCurrentSiteId(); - this.filepoolProvider.downloadUrl(siteId, icon, false, AddonModLtiProvider.COMPONENT, module.id).then((url) => { + this.filepoolProvider.downloadUrl(siteId, icon, false, AddonModLtiProvider.COMPONENT, module.id).then(() => { + // Get the internal URL. + return this.filepoolProvider.getSrcByUrl(siteId, icon, AddonModLtiProvider.COMPONENT, module.id); + }).then((url) => { data.icon = url; }).catch(() => { // Error downloading. If we're online we'll set the online url. diff --git a/src/addon/notifications/providers/notifications.ts b/src/addon/notifications/providers/notifications.ts index 3fefc469c..34c3a1819 100644 --- a/src/addon/notifications/providers/notifications.ts +++ b/src/addon/notifications/providers/notifications.ts @@ -58,10 +58,14 @@ export class AddonNotificationsProvider { if (cid && cid[1]) { notification.courseid = cid[1]; } - // Try to get the profile picture of the user. - this.userProvider.getProfile(notification.useridfrom, notification.courseid, true).then((user) => { - notification.profileimageurlfrom = user.profileimageurl; - }); + if (notification.useridfrom > 0) { + // Try to get the profile picture of the user. + this.userProvider.getProfile(notification.useridfrom, notification.courseid, true).then((user) => { + notification.profileimageurlfrom = user.profileimageurl; + }).catch(() => { + // Error getting user. This can happen if device is offline or the user is deleted. + }); + } }); } diff --git a/src/assets/icon/favicon.ico b/src/assets/icon/favicon.ico index c65354c81..4c516e7b5 100644 Binary files a/src/assets/icon/favicon.ico and b/src/assets/icon/favicon.ico differ diff --git a/src/assets/icon/icon.png b/src/assets/icon/icon.png new file mode 100644 index 000000000..de5b0f4c6 Binary files /dev/null and b/src/assets/icon/icon.png differ diff --git a/src/core/emulator/classes/filesystem.ts b/src/core/emulator/classes/filesystem.ts new file mode 100644 index 000000000..591334e59 --- /dev/null +++ b/src/core/emulator/classes/filesystem.ts @@ -0,0 +1,606 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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. + +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; + +/** + * This file includes all the classes needed to emulate file system in NodeJS (desktop apps). + */ + +/** + * Emulate Entry object of the Cordova file plugin using NodeJS functions. + * It includes fileSystem and nativeURL as the Cordova plugin does, but they aren't used. + */ +export class EntryMock { + + protected fs = require('fs'); + + constructor(protected textUtils: CoreTextUtilsProvider, protected mimeUtils: CoreMimetypeUtilsProvider, public isFile: boolean, + public isDirectory: boolean, public name: string = '', public fullPath: string = '', public fileSystem?: FileSystem, + public nativeURL?: string) { } + + /** + * Copy the file or directory. + * + * @param {Entry} parent The folder where to move the file to. + * @param {string} newName The new name for the file. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + copyTo(parent: Entry, newName: string, successCallback: Function, errorCallback: Function): void { + newName = newName || this.name; + + // There is no function to copy a file, read the source and write the dest. + const srcPath = this.fullPath, + destPath = this.textUtils.concatenatePaths(parent.fullPath, newName), + reader = this.fs.createReadStream(srcPath), + writer = this.fs.createWriteStream(destPath); + + // Use a promise to make sure only one callback is called. + new Promise((resolve, reject): void => { + reader.on('error', reject); + + writer.on('error', reject); + + writer.on('close', resolve); + + reader.pipe(writer); + }).then(() => { + const constructor = this.isDirectory ? DirectoryEntryMock : FileEntryMock; + successCallback && successCallback( + new constructor(this.textUtils, this.mimeUtils, newName, destPath)); + }).catch((error) => { + errorCallback && errorCallback(error); + }); + } + + /** + * Get the entry's metadata. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + getMetadata(successCallback: Function, errorCallback: Function): void { + this.fs.stat(this.fullPath, (err, stats) => { + if (err) { + errorCallback && errorCallback(err); + } else { + successCallback && successCallback({ + size: stats.size, + modificationTime: stats.mtime + }); + } + }); + } + + /** + * Get the parent directory. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + getParent(successCallback: Function, errorCallback: Function): void { + // Remove last slash if present and get the path of the parent. + const fullPath = this.fullPath.slice(-1) == '/' ? this.fullPath.slice(0, -1) : this.fullPath, + parentPath = fullPath.substr(0, fullPath.lastIndexOf('/')); + + // Check that parent exists. + this.fs.stat(parentPath, (err, stats) => { + if (err || !stats.isDirectory()) { + errorCallback && errorCallback(err); + } else { + const fileName = parentPath.substr(parentPath.lastIndexOf('/') + 1); + successCallback && successCallback( + new DirectoryEntryMock(this.textUtils, this.mimeUtils, fileName, parentPath)); + } + }); + } + + /** + * Move the file or directory. + * + * @param {Entry} parent The folder where to move the file to. + * @param {string} newName The new name for the file. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + moveTo(parent: Entry, newName: string, successCallback: Function, errorCallback: Function): void { + newName = newName || this.name; + + const srcPath = this.fullPath, + destPath = this.textUtils.concatenatePaths(parent.fullPath, newName); + + this.fs.rename(srcPath, destPath, (err) => { + if (err) { + errorCallback && errorCallback(err); + } else { + const constructor = this.isDirectory ? DirectoryEntryMock : FileEntryMock; + successCallback && successCallback( + new constructor(this.textUtils, this.mimeUtils, newName, destPath)); + } + }); + } + + /** + * Remove the entry. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + remove(successCallback: Function, errorCallback: Function): void { + const removeFn = this.isDirectory ? this.fs.rmdir : this.fs.unlink; + removeFn(this.fullPath, (err) => { + if (err < 0) { + errorCallback && errorCallback(err); + } else { + successCallback && successCallback(); + } + }); + } + + /** + * Set the entry's metadata. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + * @param {any} metadataObject The metadata to set. + */ + setMetadata(successCallback: Function, errorCallback: Function, metadataObject: any): void { + // Not supported. + errorCallback && errorCallback('Not supported'); + } + + /** + * Get the internal URL of the Entry. + * + * @return {string} Internal URL. + */ + toInternalURL(): string { + return 'file://' + this.fullPath; + } + + /** + * Get the URL of the Entry. + * + * @return {string} URL. + */ + toURL(): string { + return this.fullPath; + } +} + +/** + * Emulate DirectoryEntry object of the Cordova file plugin using NodeJS functions. + */ +export class DirectoryEntryMock extends EntryMock { + + constructor(textUtils: CoreTextUtilsProvider, mimeUtils: CoreMimetypeUtilsProvider, name: string = '', fullPath: string = '', + fileSystem?: FileSystem, nativeURL?: string) { + + super(textUtils, mimeUtils, false, true, name, fullPath, fileSystem, nativeURL); + + // Add trailing slash if it is missing. + if ((this.fullPath) && !/\/$/.test(this.fullPath)) { + this.fullPath += '/'; + } + if (this.nativeURL && !/\/$/.test(this.nativeURL)) { + this.nativeURL += '/'; + } + } + + /** + * Create reader. + * + * @return {DirectoryReader} Reader. + */ + createReader(): DirectoryReader { + return new DirectoryReaderMock(this.textUtils, this.mimeUtils, this.fullPath); + } + + /** + * Delete an empty folder. + * + * @param {string} path Path of the folder. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + protected deleteEmptyFolder(path: string, successCallback: Function, errorCallback: Function): void { + this.fs.rmdir(path, (err) => { + if (err) { + // Error removing directory. + errorCallback && errorCallback(err); + } else { + successCallback && successCallback(); + } + }); + } + + /** + * Get a directory inside this directory entry. + * + * @param {string} path Path of the dir. + * @param {any} options Options. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + getDirectory(path: string, options: any, successCallback: Function, errorCallback: Function): void { + this.getDirOrFile(true, path, options, successCallback, errorCallback); + } + + /** + * Helper function for getDirectory and getFile. + * + * @param {boolean} isDir True if getting a directory, false if getting a file. + * @param {string} path Path of the file or dir. + * @param {any} options Options. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + protected getDirOrFile(isDir: boolean, path: string, options: any, successCallback: Function, errorCallback: Function): void { + + // Success, return the DirectoryEntry or FileEntry. + const success = (): void => { + const constructor = isDir ? DirectoryEntryMock : FileEntryMock; + successCallback && successCallback( + new constructor(this.textUtils, this.mimeUtils, fileName, fileDirPath)); + }; + + // Create the file/dir. + const create = (done): void => { + if (isDir) { + this.fs.mkdir(fileDirPath, done); + } else { + this.fs.writeFile(fileDirPath, '', done); + } + }; + + const fileName = path.substr(path.lastIndexOf('/') + 1), + fileDirPath = this.textUtils.concatenatePaths(this.fullPath, path); + + // Check if file/dir exists. + this.fs.stat(fileDirPath, (err) => { + if (err) { + if (options.create) { + // File/Dir probably doesn't exist, create it. + create((error2) => { + if (!error2) { + // File created successfully, return it. + success(); + } else if (error2.code === 'EEXIST') { + // File exists, success. + success(); + } else if (error2.code === 'ENOENT') { + // Seems like the parent doesn't exist, create it too. + const parent = fileDirPath.substring(0, fileDirPath.lastIndexOf('/')); + + if (parent) { + this.getDirectory(parent, options, () => { + // Parent created, try to create the child again. + create((error3) => { + if (!error3) { + success(); + } else { + errorCallback && errorCallback(error3); + } + }); + }, errorCallback); + } else { + errorCallback && errorCallback(error2); + } + } else { + errorCallback && errorCallback(error2); + } + }); + } else { + errorCallback && errorCallback(err); + } + } else { + success(); + } + }); + } + + /** + * Get a file inside this directory entry. + * + * @param {string} path Path of the dir. + * @param {any} options Options. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + getFile(path: string, options: any, successCallback: Function, errorCallback: Function): void { + this.getDirOrFile(false, path, options, successCallback, errorCallback); + } + + /** + * Remove the directory and all its contents. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + removeRecursively(successCallback: Function, errorCallback: Function): void { + // Use a promise to make sure only one callback is called. + new Promise((resolve, reject): void => { + this.removeRecursiveFn(this.fullPath, resolve, reject); + }).then(() => { + successCallback && successCallback(); + }).catch((error) => { + errorCallback && errorCallback(error); + }); + } + + /** + * Delete a file or folder recursively. + * + * @param {string} path Path of the folder. + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + protected removeRecursiveFn(path: string, successCallback: Function, errorCallback: Function): void { + // Check if it exists. + this.fs.stat(path, (err, stats) => { + if (err) { + // File not found, reject. + errorCallback && errorCallback(err); + } else if (stats.isFile()) { + // It's a file, remove it. + this.fs.unlink(path, (err) => { + if (err) { + // Error removing file, reject. + errorCallback && errorCallback(err); + } else { + successCallback && successCallback(); + } + }); + } else { + // It's a directory, read the contents. + this.fs.readdir(path, (err, files) => { + if (err) { + // Error reading directory contents, reject. + errorCallback && errorCallback(err); + } else if (!files.length) { + // No files to delete, delete the folder. + this.deleteEmptyFolder(path, successCallback, errorCallback); + } else { + // Remove all the files and directories. + let removed = 0; + files.forEach((filename) => { + this.removeRecursiveFn(this.textUtils.concatenatePaths(path, filename), () => { + // Success deleting the file/dir. + removed++; + if (removed == files.length) { + // All files deleted, delete the folder. + this.deleteEmptyFolder(path, successCallback, errorCallback); + } + }, errorCallback); + }); + } + }); + } + }); + } +} + +/** + * Emulate FileEntry object of the Cordova file plugin using NodeJS functions. + */ +export class FileEntryMock extends EntryMock { + + constructor(textUtils: CoreTextUtilsProvider, mimeUtils: CoreMimetypeUtilsProvider, name: string = '', fullPath: string = '', + fileSystem?: FileSystem, nativeURL?: string) { + + super(textUtils, mimeUtils, true, false, name, fullPath, fileSystem, nativeURL); + + // Remove trailing slash if it is present. + if (this.fullPath && /\/$/.test(this.fullPath)) { + this.fullPath = this.fullPath.substring(0, this.fullPath.length - 1); + } + if (this.nativeURL && /\/$/.test(this.nativeURL)) { + this.nativeURL = this.nativeURL.substring(0, this.nativeURL.length - 1); + } + } + + /** + * Create writer. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + createWriter(successCallback: Function, errorCallback: Function): void { + this.file((file) => { + successCallback && successCallback(new FileWriterMock(this.textUtils, this.mimeUtils, file)); + }, errorCallback); + } + + /** + * Get the file data. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + file(successCallback: Function, errorCallback: Function): void { + // Get the metadata to know the time modified. + this.getMetadata((metadata) => { + // Read the file. + this.fs.readFile(this.fullPath, (err, data) => { + if (err) { + errorCallback && errorCallback(err); + } else { + // Create a File instance and return it. + data = Uint8Array.from(data).buffer; // Convert the NodeJS Buffer to ArrayBuffer. + + const file: any = new File([data], this.name || '', { + lastModified: metadata.modificationTime || null, + type: this.mimeUtils.getMimeType(this.mimeUtils.getFileExtension(this.name)) || null + }); + file.localURL = this.fullPath; + file.start = 0; + file.end = file.size; + + successCallback && successCallback(file); + } + }); + }, errorCallback); + } +} + +/** + * Emulate DirectoryReader object of the Cordova file plugin using NodeJS functions. + */ +export class DirectoryReaderMock implements DirectoryReader { + + protected fs = require('fs'); + + constructor(protected textUtils: CoreTextUtilsProvider, protected mimeUtils: CoreMimetypeUtilsProvider, + protected localURL: string = null) { } + + /** + * Read entries inside a folder. + * + * @param {Function} successCallback Success callback. + * @param {Function} errorCallback Error callback. + */ + readEntries(successCallback: Function, errorCallback: Function): void { + this.fs.readdir(this.localURL, (err, files) => { + if (err) { + errorCallback && errorCallback(err); + } else { + // Use try/catch because it includes sync calls. + try { + const entries = []; + + for (let i = 0; i < files.length; i++) { + const fileName = files[i], + filePath = this.textUtils.concatenatePaths(this.localURL, fileName), + stats = this.fs.statSync(filePath); // Use sync function to make code simpler. + + if (stats.isDirectory()) { + entries.push(new DirectoryEntryMock(this.textUtils, this.mimeUtils, fileName, + filePath)); + } else if (stats.isFile()) { + entries.push(new FileEntryMock(this.textUtils, this.mimeUtils, fileName, filePath)); + } + } + + successCallback && successCallback(entries); + } catch (ex) { + errorCallback && errorCallback(ex); + } + } + }); + } +} + +/** + * Emulate FileWriter object of the Cordova file plugin using NodeJS functions. + */ +export class FileWriterMock { + + protected fs = require('fs'); + + fileName = ''; + length = 0; + localURL: string; + size = 0; + position = 0; // Default is to write at the beginning of the file. + readyState = 0; // EMPTY. + result: any = null; + error: any = null; + + // Event handlers. + onwritestart: (event?: ProgressEvent) => void; // When writing starts. + onprogress: (event?: ProgressEvent) => void; // While writing the file, and reporting partial file data. + onwrite: (event?: ProgressEvent) => void; // When the write has successfully completed. + onwriteend: (event?: ProgressEvent) => void; // When the request has completed (either in success or failure). + onabort: (event?: ProgressEvent) => void; // When the write has been aborted. + onerror: (event?: ProgressEvent) => void; // When the write has failed (see errors). + + constructor(protected textUtils: CoreTextUtilsProvider, protected mimeUtils: CoreMimetypeUtilsProvider, protected file: any) { + + if (file) { + this.localURL = file.localURL || file; + this.length = file.size || 0; + } + } + + /** + * Terminate file operation. + */ + abort(): void { + // Not supported. + } + + /** + * The file position at which the next write will occur. + * + * @param {number} offset If nonnegative, an absolute byte offset into the file. + * If negative, an offset back from the end of the file. + */ + seek(offset: number): void { + this.position = offset; + } + + /** + * Changes the length of the file to that specified. If shortening the file, data beyond the new length + * will be discarded. If extending the file, the existing data will be zero-padded up to the new length. + * + * @param {number} size The size to which the length of the file is to be adjusted, measured in bytes. + */ + truncate(size: number): void { + this.size = size; + } + + /** + * Write some data into the file. + * + * @param {any} data The data to write. + */ + write(data: any): void { + if (data && data.toString() == '[object Blob]') { + // Can't write Blobs, convert it to a Buffer. + const reader = new FileReader(); + reader.onload = (): void => { + if (reader.readyState == 2) { + this.writeFile(new Buffer(reader.result)); + } + }; + reader.readAsArrayBuffer(data); + } else if (data && data.toString() == '[object ArrayBuffer]') { + // Convert it to a Buffer. + data = new Uint8Array(data); + this.writeFile(Buffer.from(data)); + } else { + this.writeFile(data); + } + } + + /** + * Write some data into the file. + * + * @param {Buffer} data The data to write. + */ + protected writeFile(data: Buffer): void { + this.fs.writeFile(this.localURL, data, (err) => { + if (err) { + this.onerror && this.onerror(err); + } else { + this.onwrite && this.onwrite(); + } + this.onwriteend && this.onwriteend(); + }); + + this.onwritestart && this.onwritestart(); + } +} diff --git a/src/core/emulator/classes/inappbrowserobject.ts b/src/core/emulator/classes/inappbrowserobject.ts index b557003f7..95b23898f 100644 --- a/src/core/emulator/classes/inappbrowserobject.ts +++ b/src/core/emulator/classes/inappbrowserobject.ts @@ -16,21 +16,20 @@ import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { Observable, Observer } from 'rxjs'; -import { InAppBrowserObject, InAppBrowserEvent } from '@ionic-native/in-app-browser'; +import { InAppBrowserEvent } from '@ionic-native/in-app-browser'; /** * Emulates the Cordova InAppBrowserObject in desktop apps. + * We aren't extending InAppBrowserObject because its constructor also opens a window, so we'd end up with 2 windows. */ -export class InAppBrowserObjectMock extends InAppBrowserObject { +export class InAppBrowserObjectMock { protected window; protected browserWindow; protected screen; protected isSSO: boolean; - protected isLinux: boolean; constructor(appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private urlUtils: CoreUrlUtilsProvider, - private url: string, target?: string, options: string = '') { - super(url, target, options); + private url: string, target?: string, options: string = 'location=yes') { if (!appProvider.isDesktop()) { // This plugin is only supported in desktop. @@ -40,12 +39,21 @@ export class InAppBrowserObjectMock extends InAppBrowserObject { this.browserWindow = require('electron').remote.BrowserWindow; this.screen = require('electron').screen; this.isSSO = !!(url && url.match(/\/launch\.php\?service=.+&passport=/)); - this.isLinux = appProvider.isLinux(); let width = 800, height = 600, display; - const bwOptions: any = {}; + + const bwOptions: any = { + webPreferences: {} + }; + + try { + // Create a separate session for inappbrowser so we can clear its data without affecting the app. + bwOptions.webPreferences.session = require('electron').remote.session.fromPartition('inappbrowsersession'); + } catch (ex) { + // Ignore errors. + } if (screen) { display = this.screen.getPrimaryDisplay(); @@ -67,11 +75,14 @@ export class InAppBrowserObjectMock extends InAppBrowserObject { if (options.indexOf('fullscreen=yes') != -1) { bwOptions.fullscreen = true; } + if (options.indexOf('clearsessioncache=yes') != -1 && bwOptions.webPreferences.session) { + bwOptions.webPreferences.session.clearStorageData({storages: 'cookies'}); + } this.window = new this.browserWindow(bwOptions); this.window.loadURL(url); - if (this.isLinux && this.isSSO) { + if (this.isSSO) { // SSO in Linux. Simulate it's an iOS device so we can retrieve the launch URL. // This is needed because custom URL scheme is not supported in Linux. const userAgent = 'Mozilla/5.0 (iPad) AppleWebKit/603.3.8 (KHTML, like Gecko) Mobile/14G60'; @@ -83,7 +94,16 @@ export class InAppBrowserObjectMock extends InAppBrowserObject { * Close the window. */ close(): void { - this.window.close(); + if (this.window.isDestroyed()) { + // Window already closed, nothing to do. + return; + } + + try { + this.window.close(); + } catch (ex) { + // Ignore errors. + } } /** @@ -113,11 +133,16 @@ export class InAppBrowserObjectMock extends InAppBrowserObject { * @return {Promise} Promise resolved with the launch URL. */ protected getLaunchUrl(retry: number = 0): Promise { + + if (this.window.isDestroyed()) { + // Window is destroyed, stop. + return Promise.reject(null); + } + return new Promise((resolve, reject): void => { // Execute Javascript to retrieve the launch link. const jsCode = 'var el = document.querySelector("#launchapp"); el && el.href;'; let found = false; - this.window.webContents.executeJavaScript(jsCode).then((launchUrl) => { found = true; resolve(launchUrl); @@ -179,7 +204,7 @@ export class InAppBrowserObjectMock extends InAppBrowserObject { // Helper functions to handle events. const received = (event, url): void => { try { - event.url = url || this.window.getURL(); + event.url = url || (this.window.isDestroyed() ? '' : this.window.getURL()); event.type = name; observer.next(event); } catch (ex) { @@ -203,7 +228,7 @@ export class InAppBrowserObjectMock extends InAppBrowserObject { case 'loadstart': this.window.webContents.on('did-start-loading', received); - if (this.isLinux && this.isSSO) { + if (this.isSSO) { // Linux doesn't support custom URL Schemes. Check if launch page is loaded. this.window.webContents.on('did-finish-load', finishLoad); } diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index edc9a165a..a74809625 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -55,6 +55,7 @@ import { CoreEmulatorHelperProvider } from './providers/helper'; import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper'; import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; @@ -125,10 +126,11 @@ export const IONIC_NATIVE_PROVIDERS = [ Device, { provide: File, - deps: [CoreAppProvider, CoreTextUtilsProvider], - useFactory: (appProvider: CoreAppProvider, textUtils: CoreTextUtilsProvider): File => { + deps: [CoreAppProvider, CoreTextUtilsProvider, CoreMimetypeUtilsProvider], + useFactory: (appProvider: CoreAppProvider, textUtils: CoreTextUtilsProvider, mimeUtils: CoreMimetypeUtilsProvider) + : File => { // Use platform instead of CoreAppProvider to prevent circular dependencies. - return appProvider.isMobile() ? new File() : new FileMock(appProvider, textUtils); + return appProvider.isMobile() ? new File() : new FileMock(appProvider, textUtils, mimeUtils); } }, { @@ -214,11 +216,16 @@ export class CoreEmulatorModule { // Emulate Custom URL Scheme plugin in desktop apps. if (appProvider.isDesktop()) { - require('electron').ipcRenderer.on('mmAppLaunched', (event, url) => { + require('electron').ipcRenderer.on('coreAppLaunched', (event, url) => { if (typeof win.handleOpenURL != 'undefined') { win.handleOpenURL(url); } }); + + // Listen for 'resume' events. + require('electron').ipcRenderer.on('coreAppFocused', () => { + document.dispatchEvent(new Event('resume')); + }); } if (!appProvider.isMobile()) { diff --git a/src/core/emulator/providers/file.ts b/src/core/emulator/providers/file.ts index c0d20b98b..891d983be 100644 --- a/src/core/emulator/providers/file.ts +++ b/src/core/emulator/providers/file.ts @@ -15,8 +15,10 @@ import { Injectable } from '@angular/core'; import { File, Entry, DirectoryEntry, FileEntry, FileError, IWriteOptions } from '@ionic-native/file'; import { CoreAppProvider } from '@providers/app'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreConfigConstants } from '../../../configconstants'; +import { FileEntryMock, DirectoryEntryMock } from '../classes/filesystem'; /** * Emulates the Cordova File plugin in desktop apps and in browser. @@ -25,7 +27,8 @@ import { CoreConfigConstants } from '../../../configconstants'; @Injectable() export class FileMock extends File { - constructor(private appProvider: CoreAppProvider, private textUtils: CoreTextUtilsProvider) { + constructor(private appProvider: CoreAppProvider, private textUtils: CoreTextUtilsProvider, + private mimeUtils: CoreMimetypeUtilsProvider) { super(); } @@ -190,6 +193,34 @@ export class FileMock extends File { }); } + /** + * Emulate Cordova file plugin using NodeJS functions. This is only for NodeJS environments, + * browser works with the default resolveLocalFileSystemURL. + * + * @param {any} fs Node module 'fs'. + */ + protected emulateCordovaFileForDesktop(fs: any): void { + if (!this.appProvider.isDesktop()) { + return; + } + + // Implement resolveLocalFileSystemURL. + window.resolveLocalFileSystemURL = (path: string, successCallback: Function, errorCallback: Function): void => { + // Check that the file/dir exists. + fs.stat(path, (err, stats) => { + if (err) { + errorCallback && errorCallback(err); + } else { + // The file/dir exists, return an instance. + const constructor = stats.isDirectory() ? DirectoryEntryMock : FileEntryMock, + fileName = path.substr(path.lastIndexOf('/') + 1); + + successCallback && successCallback(new constructor(this.textUtils, this.mimeUtils, fileName, path)); + } + }); + }; + } + /** * Fill the message for an error. * @@ -336,7 +367,7 @@ export class FileMock extends File { const fs = require('fs'), app = require('electron').remote.app; - // @todo emulateCordovaFileForDesktop(fs); + this.emulateCordovaFileForDesktop(fs); // Initialize File System. Get the path to use. basePath = app.getPath('documents') || app.getPath('home'); @@ -352,7 +383,6 @@ export class FileMock extends File { fs.mkdir(basePath, (e) => { if (!e || (e && e.code === 'EEXIST')) { // Create successful or it already exists. Resolve. - // @todo this.fileProvider.setHTMLBasePath(basePath); resolve(basePath); } else { reject('Error creating base path.'); diff --git a/src/core/emulator/providers/globalization.ts b/src/core/emulator/providers/globalization.ts index 6bcd6008c..9cd70aa17 100644 --- a/src/core/emulator/providers/globalization.ts +++ b/src/core/emulator/providers/globalization.ts @@ -13,9 +13,21 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { Globalization } from '@ionic-native/globalization'; +import { Globalization, GlobalizationOptions } from '@ionic-native/globalization'; import { CoreAppProvider } from '@providers/app'; +/** + * Mock the Globalization Error. + */ +export class GlobalizationErrorMock implements GlobalizationError { + static UNKNOWN_ERROR = 0; + static FORMATTING_ERROR = 1; + static PARSING_ERROR = 2; + static PATTERN_ERROR = 3; + + constructor(public code: number, public message: string) { } +} + /** * Emulates the Cordova Globalization plugin in desktop apps and in browser. */ @@ -26,6 +38,28 @@ export class GlobalizationMock extends Globalization { super(); } + /** + * Converts date to string. + * + * @param {Date} date Date you wish to convert + * @param options Options for the converted date. Length, selector. + * @returns {Promise<{value: string}>} Returns a promise when the date has been converted. + */ + dateToString(date: Date, options: GlobalizationOptions): Promise<{ value: string; }> { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + + /** + * Returns a pattern string to format and parse currency values according to the client's user preferences and ISO 4217 + * currency code. + * + * @param {string} currencyCode Currency Code. + * @returns {Promise} + */ + getCurrencyPattern(currencyCode: string): Promise { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + /** * Get the current locale. * @@ -47,6 +81,35 @@ export class GlobalizationMock extends Globalization { } } + /** + * Returns an array of the names of the months or days of the week, depending on the client's user preferences and calendar. + * + * @param options Object with type (narrow or wide) and item (month or days). + * @returns {Promise<{value: Array}>} Returns a promise. + */ + getDateNames(options: { type: string; item: string; }): Promise<{ value: Array; }> { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + + /** + * Returns a pattern string to format and parse dates according to the client's user preferences. + * + * @param options Object with the format length and selector + * @returns {Promise} Returns a promise. + */ + getDatePattern(options: GlobalizationOptions): Promise { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + + /** + * Returns the first day of the week according to the client's user preferences and calendar. + * + * @returns {Promise<{value: string}>} returns a promise with the value + */ + getFirstDayOfWeek(): Promise<{ value: string; }> { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + /** * Get the current locale name. * @@ -57,12 +120,21 @@ export class GlobalizationMock extends Globalization { if (locale) { return Promise.resolve({ value: locale }); } else { - const error = { code: GlobalizationError.UNKNOWN_ERROR, message: 'Cannot get language' }; + const error = new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Cannot get language'); return Promise.reject(error); } } + /** + * Returns a pattern string to format and parse numbers according to the client's user preferences. + * @param options Can be decimal, percent, or currency. + * @returns {Promise} + */ + getNumberPattern(options: { type: string; }): Promise { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + /* * Get the current preferred language. * @@ -71,4 +143,46 @@ export class GlobalizationMock extends Globalization { getPreferredLanguage(): Promise<{ value: string }> { return this.getLocaleName(); } + + /** + * Indicates whether daylight savings time is in effect for a given date using the client's time zone and calendar. + * + * @param {data} date Date to process. + * @returns {Promise<{dst: string}>} reutrns a promise with the value + */ + isDayLightSavingsTime(date: Date): Promise<{ dst: string; }> { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + + /** + * Returns a number formatted as a string according to the client's user preferences. + * @param numberToConvert {Number} The number to convert + * @param options {Object} Object with property `type` that can be set to: decimal, percent, or currency. + */ + numberToString(numberToConvert: number, options: { type: string; }): Promise<{ value: string; }> { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + + /** + * Parses a date formatted as a string, according to the client's user preferences and calendar using the time zone of the + * client, and returns the corresponding date object. + * + * @param {string} dateString Date as a string to be converted + * @param options Options for the converted date. Length, selector. + * @returns {Promise} Returns a promise when the date has been converted. + */ + stringToDate(dateString: string, options: GlobalizationOptions): Promise { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } + + /** + * + * @param {string} stringToConvert String you want to conver to a number. + * + * @param options The type of number you want to return. Can be decimal, percent, or currency. + * @returns {Promise<{ value: number | string }>} Returns a promise with the value. + */ + stringToNumber(stringToConvert: string, options: { type: string; }): Promise<{ value: number | string; }> { + return Promise.reject(new GlobalizationErrorMock(GlobalizationErrorMock.UNKNOWN_ERROR, 'Not supported.')); + } } diff --git a/src/core/emulator/providers/inappbrowser.ts b/src/core/emulator/providers/inappbrowser.ts index 21c18d3f2..e991e6a90 100644 --- a/src/core/emulator/providers/inappbrowser.ts +++ b/src/core/emulator/providers/inappbrowser.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser'; +import { InAppBrowser } from '@ionic-native/in-app-browser'; import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -36,9 +36,16 @@ export class InAppBrowserMock extends InAppBrowser { * @param {string} url The URL to load. * @param {string} [target] The target in which to load the URL, an optional parameter that defaults to _self. * @param {string} [options] Options for the InAppBrowser. - * @return {InAppBrowserObject} The new instance. + * @return {any} The new instance. */ - create(url: string, target?: string, options: string = ''): InAppBrowserObject { + create(url: string, target?: string, options: string = 'location=yes'): any { + if (options && typeof options !== 'string') { + // Convert to string. + options = Object.keys(options).map((key) => { + return key + '=' + options[key]; + }).join(','); + } + if (!this.appProvider.isDesktop()) { return super.create(url, target, options); } diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index 47ba21038..d1685ec80 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -148,6 +148,10 @@ export class LocalNotificationsMock extends LocalNotifications { * @return {Void} */ protected cancelNotification(id: number, omitEvent: boolean, eventName: string): void { + if (!this.scheduled[id]) { + return; + } + const notification = this.scheduled[id].notification; clearTimeout(this.scheduled[id].timeout); @@ -698,7 +702,7 @@ export class LocalNotificationsMock extends LocalNotifications { id : notification.id, title: notification.title, text: notification.text, - at: notification.at ? notification.at.getTime() : 0, + at: notification.at ? (typeof notification.at == 'object' ? notification.at.getTime() : notification.at) : 0, data: notification.data ? JSON.stringify(notification.data) : '{}', triggered: triggered ? 1 : 0 }; diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index a2393ed8a..89567ab8d 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -91,8 +91,11 @@ export class CoreMainMenuPage implements OnDestroy { for (let i = 0; i < this.tabs.length; i++) { const tab = this.tabs[i]; if (tab.page == this.redirectPage) { + // Tab found. Set the params and unset the redirect page. this.initialTab = i + 1; tab.pageParams = Object.assign(tab.pageParams || {}, this.redirectParams); + this.redirectPage = null; + this.redirectParams = null; break; } } diff --git a/src/core/settings/pages/space-usage/space-usage.html b/src/core/settings/pages/space-usage/space-usage.html index 9ff095758..d2eef1677 100644 --- a/src/core/settings/pages/space-usage/space-usage.html +++ b/src/core/settings/pages/space-usage/space-usage.html @@ -20,7 +20,7 @@

{{ 'core.settings.total' | translate }}

{{ totalUsage | coreBytesToSize }}

- +

{{ 'core.settings.estimatedfreespace' | translate }}

{{ freeSpace | coreBytesToSize }}

diff --git a/src/core/settings/pages/space-usage/space-usage.ts b/src/core/settings/pages/space-usage/space-usage.ts index c24ba88b6..0f10c8545 100644 --- a/src/core/settings/pages/space-usage/space-usage.ts +++ b/src/core/settings/pages/space-usage/space-usage.ts @@ -15,6 +15,7 @@ import { Component, } from '@angular/core'; import { IonicPage } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; @@ -36,11 +37,13 @@ export class CoreSettingsSpaceUsagePage { currentSiteId = ''; totalUsage = 0; freeSpace = 0; + showFreeSpace = true; constructor(private fileProvider: CoreFileProvider, private filePoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, - private translate: TranslateService, private domUtils: CoreDomUtilsProvider) { + private translate: TranslateService, private domUtils: CoreDomUtilsProvider, appProvider: CoreAppProvider) { this.currentSiteId = this.sitesProvider.getCurrentSiteId(); + this.showFreeSpace = !appProvider.isDesktop(); } /** @@ -112,10 +115,15 @@ export class CoreSettingsSpaceUsagePage { * @return {Promise} Resolved when done. */ protected fetchData(): Promise { - return Promise.all([ + const promises = [ this.calculateSizeUsage().then(() => this.calculateTotalUsage()), - this.calculateFreeSpace(), - ]); + ]; + + if (this.showFreeSpace) { + promises.push(this.calculateFreeSpace()); + } + + return Promise.all(promises); } /** diff --git a/src/core/user/providers/user-handler.ts b/src/core/user/providers/user-handler.ts index 8437b3e8b..15956dd8e 100644 --- a/src/core/user/providers/user-handler.ts +++ b/src/core/user/providers/user-handler.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from './user-delegate'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Profile links email handler. @@ -25,7 +26,7 @@ export class CoreUserProfileMailHandler implements CoreUserProfileHandler { priority = 700; type = CoreUserDelegate.TYPE_COMMUNICATION; - constructor(protected sitesProvider: CoreSitesProvider) { } + constructor(protected sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider) { } /** * Check if handler is enabled. @@ -63,7 +64,8 @@ export class CoreUserProfileMailHandler implements CoreUserProfileHandler { action: (event, navCtrl, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - window.open('mailto:' + user.email, '_blank'); + + this.utils.openInBrowser('mailto:' + user.email); } }; } diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 31c53a230..d0a9b1e83 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -1757,7 +1757,12 @@ export class CoreFilepoolProvider { if (this.fileProvider.isAvailable()) { return Promise.resolve(this.getFilePath(siteId, fileId)).then((path) => { return this.fileProvider.getFile(path).then((fileEntry) => { - return fileEntry.toURL(); + // This URL is usually used to launch files or put them in HTML. In desktop we need the internal URL. + if (this.appProvider.isDesktop()) { + return fileEntry.toInternalURL(); + } else { + return fileEntry.toURL(); + } }); }); } diff --git a/src/providers/update-manager.ts b/src/providers/update-manager.ts index fb451a9f9..b4017eb1f 100644 --- a/src/providers/update-manager.ts +++ b/src/providers/update-manager.ts @@ -105,10 +105,6 @@ export class CoreUpdateManagerProvider implements CoreInitHandler { { name: 'desktop_local_notifications', fields: [ - { - name: 'at', - type: 'date' - }, { name: 'data', type: 'object' diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index 20122e7a8..74e6819f2 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -853,7 +853,7 @@ export class CoreUtilsProvider { } // In the rest of platforms we need to open them in InAppBrowser. - window.open(url, '_blank'); + this.openInApp(url); return Promise.resolve(); }