diff --git a/config.xml b/config.xml index 71d282dcc..a8c1baeca 100644 --- a/config.xml +++ b/config.xml @@ -116,10 +116,10 @@ - + diff --git a/package-lock.json b/package-lock.json index 33acaa63c..b963b9677 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,11 +54,21 @@ "resolved": "https://registry.npmjs.org/@angular/tsc-wrapped/-/tsc-wrapped-4.4.3.tgz", "integrity": "sha1-LT84IQodTbA/yG3PHglYErhc0Rk=" }, + "@ionic-native/clipboard": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-4.3.2.tgz", + "integrity": "sha512-dutKKMnpyhFeEPc09O5MzrtPKtv13f3X4mCmDmUEj5CExMecTOhox/rnIB70t8GP2/3bTC/CSr39DhkLLS8MzA==" + }, "@ionic-native/core": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-4.3.0.tgz", "integrity": "sha512-Pf0qCzqlVFmIpZpvo35Kl0e+1K8GUgPMcKBnN57gWh+5Ecj3dPcb+MbP4murJo/dnFsIYPYdXRZRf74hjo6gtw==" }, + "@ionic-native/globalization": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.3.2.tgz", + "integrity": "sha512-sDyriA3/xspu6RM8arEOlhOkSxvRwLDNdvbBoZ59i+Dn5i6bjoE4hMMEFvUlnzvOZKn5GusurrWPz5cmfA9aPw==" + }, "@ionic-native/keyboard": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/@ionic-native/keyboard/-/keyboard-4.3.2.tgz", @@ -110,6 +120,11 @@ "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=" }, + "@types/cordova-plugin-globalization": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/cordova-plugin-globalization/-/cordova-plugin-globalization-0.0.3.tgz", + "integrity": "sha1-ySA6HENtPS0DBXiffJwrq6i6KK0=" + }, "@types/cordova-plugin-network-information": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/cordova-plugin-network-information/-/cordova-plugin-network-information-0.0.3.tgz", @@ -120,6 +135,11 @@ "resolved": "https://registry.npmjs.org/@types/localforage/-/localforage-0.0.30.tgz", "integrity": "sha1-PWCmv23aOOP4pGlhFZg3nx9klQk=" }, + "@types/node": { + "version": "8.0.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.47.tgz", + "integrity": "sha512-kOwL746WVvt/9Phf6/JgX/bsGQvbrK5iUgzyfwZNcKVFcjAUVSpF9HxevLTld2SG9aywYHOILj38arDdY1r/iQ==" + }, "@types/promise.prototype.finally": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/promise.prototype.finally/-/promise.prototype.finally-2.0.2.tgz", diff --git a/package.json b/package.json index 89e9c0cf8..34f09290a 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "@angular/http": "4.4.3", "@angular/platform-browser": "4.4.3", "@angular/platform-browser-dynamic": "4.4.3", + "@ionic-native/clipboard": "^4.3.2", "@ionic-native/core": "4.3.0", + "@ionic-native/globalization": "^4.3.2", "@ionic-native/keyboard": "^4.3.2", "@ionic-native/network": "^4.3.2", "@ionic-native/splash-screen": "4.3.0", @@ -43,7 +45,9 @@ "@ngx-translate/core": "^8.0.0", "@ngx-translate/http-loader": "^2.0.0", "@types/cordova": "0.0.34", + "@types/cordova-plugin-globalization": "0.0.3", "@types/cordova-plugin-network-information": "0.0.3", + "@types/node": "^8.0.47", "@types/promise.prototype.finally": "^2.0.2", "electron-builder-squirrel-windows": "^19.3.0", "electron-windows-notifications": "^1.1.13", @@ -58,5 +62,8 @@ "devDependencies": { "@ionic/app-scripts": "3.0.0", "typescript": "2.3.4" + }, + "browser": { + "electron": false } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0d832d81c..e1be5a289 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,13 +1,12 @@ import { BrowserModule } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; import { SplashScreen } from '@ionic-native/splash-screen'; import { StatusBar } from '@ionic-native/status-bar'; import { SQLite } from '@ionic-native/sqlite'; import { Keyboard } from '@ionic-native/keyboard'; -import { Network } from '@ionic-native/network'; import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; @@ -16,6 +15,7 @@ import { CoreLoggerProvider } from '../providers/logger'; import { CoreDbProvider } from '../providers/db'; import { CoreAppProvider } from '../providers/app'; import { CoreConfigProvider } from '../providers/config'; +import { CoreEmulatorModule } from '../core/emulator/emulator.module'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient) { @@ -28,6 +28,7 @@ export function createTranslateLoader(http: HttpClient) { ], imports: [ BrowserModule, + HttpClientModule, IonicModule.forRoot(MyApp), TranslateModule.forRoot({ loader: { @@ -35,7 +36,8 @@ export function createTranslateLoader(http: HttpClient) { useFactory: (createTranslateLoader), deps: [HttpClient] } - }) + }), + CoreEmulatorModule ], bootstrap: [IonicApp], entryComponents: [ @@ -46,12 +48,11 @@ export function createTranslateLoader(http: HttpClient) { SplashScreen, SQLite, Keyboard, - Network, {provide: ErrorHandler, useClass: IonicErrorHandler}, CoreLoggerProvider, CoreDbProvider, CoreAppProvider, - CoreConfigProvider, + CoreConfigProvider ] }) export class AppModule {} diff --git a/src/core/emulator/classes/sqlitedb.ts b/src/core/emulator/classes/sqlitedb.ts new file mode 100644 index 000000000..34210a38f --- /dev/null +++ b/src/core/emulator/classes/sqlitedb.ts @@ -0,0 +1,153 @@ +// (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 { SQLiteDB } from '../../../classes/sqlitedb'; + +/** + * Class to mock the interaction with the SQLite database. + */ +export class SQLiteDBMock extends SQLiteDB { + promise: Promise; + + /** + * Create and open the database. + * + * @param {string} name Database name. + */ + constructor(public name: string) { + super(name, null, null); + } + + /** + * Close the database. + * + * @return {Promise} Promise resolved when done. + */ + close() : Promise { + // WebSQL databases aren't closed. + return Promise.resolve(); + } + + /** + * Drop all the data in the database. + * + * @return {Promise} Promise resolved when done. + */ + emptyDatabase() : Promise { + return new Promise((resolve, reject) => { + this.db.transaction(tx => { + // Query all tables from sqlite_master that we have created and can modify. + let args = [], + query = `SELECT * FROM sqlite_master + WHERE name NOT LIKE 'sqlite\\_%' escape '\\' AND name NOT LIKE '\\_%' escape '\\'`; + + tx.executeSql(query, args, (tx, result) => { + if (result.rows.length <= 0) { + // No tables to delete, stop. + resolve(); + return; + } + + // Drop all the tables. + let promises = []; + + for (let i = 0; i < result.rows.length; i++) { + promises.push(new Promise((resolve, reject) => { + // Drop the table. + let name = JSON.stringify(result.rows.item(i).name); + tx.executeSql('DROP TABLE ' + name, [], resolve, reject); + })); + } + + Promise.all(promises).then(resolve, reject); + }, reject); + }); + }); + } + + /** + * Execute a SQL query. + * + * @param {string} sql SQL query to execute. + * @param {any[]} params Query parameters. + * @return {Promise} Promise resolved with the result. + */ + protected execute(sql: string, params?: any[]) : Promise { + return new Promise((resolve, reject) => { + // With WebSQL, all queries must be run in a transaction. + this.db.transaction((tx) => { + tx.executeSql(sql, params, (tx, results) => { + resolve(results); + }, reject); + }); + }); + } + + /** + * Execute a set of SQL queries. This operation is atomic. + * + * @param {any[]} sqlStatements SQL statements to execute. + * @return {Promise} Promise resolved with the result. + */ + protected executeBatch(sqlStatements: any[]) : Promise { + return new Promise((resolve, reject) => { + // Create a transaction to execute the queries. + this.db.transaction((tx) => { + let promises = []; + + // Execute all the queries. Each statement can be a string or an array. + sqlStatements.forEach((statement) => { + promises.push(new Promise((resolve, reject) => { + let query, + params; + + if (Array.isArray(statement)) { + query = statement[0]; + params = statement[1]; + } else { + query = statement; + params = null; + } + + tx.executeSql(query, params, (tx, results) => { + resolve(results); + }, reject); + })); + }); + + Promise.all(promises).then(resolve, reject); + }); + }); + } + + /** + * Initialize the database. + */ + init(): void { + // This DB is for desktop apps, so use a big size to be sure it isn't filled. + this.db = (window).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); + this.promise = Promise.resolve(); + } + + /** + * Open the database. Only needed if it was closed before, a database is automatically opened when created. + * + * @return {Promise} Promise resolved when done. + */ + open() : Promise { + // WebSQL databases can't closed, so the open method isn't needed. + return Promise.resolve(); + } + +} diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts new file mode 100644 index 000000000..4c87604d0 --- /dev/null +++ b/src/core/emulator/emulator.module.ts @@ -0,0 +1,69 @@ +// (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 { NgModule } from '@angular/core'; +import { Platform } from 'ionic-angular'; + +import { CoreAppProvider } from '../../providers/app'; +import { Clipboard } from '@ionic-native/clipboard'; +import { Globalization } from '@ionic-native/globalization'; +import { Network } from '@ionic-native/network'; +import { ClipboardMock } from './providers/clipboard'; +import { GlobalizationMock } from './providers/globalization'; +import { NetworkMock } from './providers/network'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + ClipboardMock, + GlobalizationMock, + { + provide: Clipboard, + deps: [CoreAppProvider], + useFactory: (appProvider: CoreAppProvider) => { + return appProvider.isMobile() ? new Clipboard() : new ClipboardMock(appProvider); + } + }, + { + provide: Globalization, + deps: [CoreAppProvider], + useFactory: (appProvider: CoreAppProvider) => { + return appProvider.isMobile() ? new Globalization() : new GlobalizationMock(appProvider); + } + }, + { + provide: Network, + deps: [Platform], + useFactory: (platform: Platform) => { + // Use platform instead of CoreAppProvider to prevent circular dependencies. + return platform.is('cordova') ? new Network() : new NetworkMock(); + } + } + ] +}) +export class CoreEmulatorModule { + constructor(appProvider: CoreAppProvider) { + let win = window; // Convert the "window" to "any" type to be able to use non-standard properties. + + // Emulate Custom URL Scheme plugin in desktop apps. + if (appProvider.isDesktop()) { + require('electron').ipcRenderer.on('mmAppLaunched', function(event, url) { + win.handleOpenURL && win.handleOpenURL(url); + }); + } + } +} diff --git a/src/core/emulator/providers/clipboard.ts b/src/core/emulator/providers/clipboard.ts new file mode 100644 index 000000000..89a427f8e --- /dev/null +++ b/src/core/emulator/providers/clipboard.ts @@ -0,0 +1,102 @@ +// (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 { Injectable } from '@angular/core'; +import { Clipboard } from '@ionic-native/clipboard'; +import { CoreAppProvider } from '../../../providers/app'; + +/** + * Emulates the Cordova Clipboard plugin in desktop apps and in browser. + */ +@Injectable() +export class ClipboardMock extends Clipboard { + isDesktop: boolean; + clipboard: any; + copyTextarea: HTMLTextAreaElement; + + constructor(appProvider: CoreAppProvider) { + super(); + + this.isDesktop = appProvider.isDesktop(); + if (this.isDesktop) { + this.clipboard = require('electron').clipboard; + } else { + // In browser the text must be selected in order to copy it. Create a hidden textarea to put the text in it. + this.copyTextarea = document.createElement('textarea'); + this.copyTextarea.className = 'mm-browser-copy-area'; + this.copyTextarea.setAttribute('aria-hidden', 'true'); + document.body.appendChild(this.copyTextarea); + } + } + + /** + * Copy some text to the clipboard. + * + * @param {string} text The text to copy. + * @return {Promise} Promise resolved when copied. + */ + copy(text: string) : Promise { + return new Promise((resolve, reject) => { + if (this.isDesktop) { + this.clipboard.writeText(text); + resolve(); + } else { + // Put the text in the hidden textarea and select it. + this.copyTextarea.innerHTML = text; + this.copyTextarea.select(); + + try { + if (document.execCommand('copy')) { + resolve(); + } else { + reject(); + } + } catch (err) { + reject(); + } + + this.copyTextarea.innerHTML = ''; + } + }); + } + + /* + * Get the text stored in the clipboard. + * + * @return {Promise} Promise resolved with the text. + */ + paste() : Promise { + return new Promise((resolve, reject) => { + if (this.isDesktop) { + resolve(this.clipboard.readText()); + } else { + // Paste the text in the hidden textarea and get it. + this.copyTextarea.innerHTML = ''; + this.copyTextarea.select(); + + try { + if (document.execCommand('paste')) { + resolve(this.copyTextarea.innerHTML); + } else { + reject(); + } + } catch (err) { + reject(); + } + + this.copyTextarea.innerHTML = ''; + } + }); + } +} diff --git a/src/core/emulator/providers/globalization.ts b/src/core/emulator/providers/globalization.ts new file mode 100644 index 000000000..5ffa2e142 --- /dev/null +++ b/src/core/emulator/providers/globalization.ts @@ -0,0 +1,74 @@ +// (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 { Injectable } from '@angular/core'; +import { Globalization } from '@ionic-native/globalization'; +import { CoreAppProvider } from '../../../providers/app'; + +/** + * Emulates the Cordova Globalization plugin in desktop apps and in browser. + */ +@Injectable() +export class GlobalizationMock extends Globalization { + + constructor(private appProvider: CoreAppProvider) { + super(); + } + + /** + * Get the current locale. + * + * @return {string} Locale name. + */ + private getCurrentlocale() : string { + // Get browser language. + var navLang = (navigator).userLanguage || navigator.language; + + try { + if (this.appProvider.isDesktop()) { + var locale = require('electron').remote.app.getLocale(); + return locale || navLang; + } else { + return navLang; + } + } catch(ex) { + // Something went wrong, return browser language. + return navLang; + } + } + + /** + * Get the current locale name. + * + * @return {Promise<{value: string}>} Promise resolved with an object with the language string. + */ + getLocaleName() : Promise<{value: string}> { + var locale = this.getCurrentlocale(); + if (locale) { + return Promise.resolve({value: locale}); + } else { + var error = {code: GlobalizationError.UNKNOWN_ERROR, message: 'Cannot get language'}; + return Promise.reject(error); + } + } + + /* + * Get the current preferred language. + * + * @return {Promise<{value: string}>} Promise resolved with an object with the language string. + */ + getPreferredLanguage() : Promise<{value: string}> { + return this.getLocaleName(); + } +} diff --git a/src/core/emulator/providers/network.ts b/src/core/emulator/providers/network.ts new file mode 100644 index 000000000..c6dfa36da --- /dev/null +++ b/src/core/emulator/providers/network.ts @@ -0,0 +1,79 @@ +// (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 { Injectable } from '@angular/core'; +import { Network } from '@ionic-native/network'; +import { Observable, Subject } from 'rxjs'; + +/** + * Emulates the Cordova Globalization plugin in desktop apps and in browser. + */ +@Injectable() +export class NetworkMock extends Network { + type: null; + + constructor() { + super(); + + (window).Connection = { + UNKNOWN: 'unknown', + ETHERNET: 'ethernet', + WIFI: 'wifi', + CELL_2G: '2g', + CELL_3G: '3g', + CELL_4G: '4g', + CELL: 'cellular', + NONE: 'none' + }; + } + + /** + * Returns an observable to watch connection changes. + * + * @return {Observable} Observable. + */ + onchange(): Observable { + return Observable.merge(this.onConnect(), this.onDisconnect()); + } + + /** + * Returns an observable to notify when the app is connected. + * + * @return {Observable} Observable. + */ + onConnect() : Observable { + let observable = new Subject(); + + window.addEventListener('online', (ev) => { + observable.next(ev); + }, false); + + return observable; + } + + /** + * Returns an observable to notify when the app is disconnected. + * + * @return {Observable} Observable. + */ + onDisconnect() : Observable { + let observable = new Subject(); + + window.addEventListener('offline', (ev) => { + observable.next(ev); + }, false); + + return observable; + } +} diff --git a/src/providers/db.ts b/src/providers/db.ts index 4e807a9de..0a463143e 100644 --- a/src/providers/db.ts +++ b/src/providers/db.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { SQLite } from '@ionic-native/sqlite'; import { Platform } from 'ionic-angular'; import { SQLiteDB } from '../classes/sqlitedb'; +import { SQLiteDBMock } from '../core/emulator/classes/sqlitedb'; /** * This service allows interacting with the local database to store and retrieve data. @@ -38,7 +39,11 @@ export class CoreDbProvider { */ getDB(name: string, forceNew?: boolean) : SQLiteDB { if (typeof this.dbInstances[name] === 'undefined' || forceNew) { - this.dbInstances[name] = new SQLiteDB(name, this.sqlite, this.platform); + if (this.platform.is('cordova')) { + this.dbInstances[name] = new SQLiteDB(name, this.sqlite, this.platform); + } else { + this.dbInstances[name] = new SQLiteDBMock(name); + } } return this.dbInstances[name]; } @@ -60,15 +65,18 @@ export class CoreDbProvider { } return promise.then(() => { + let db = this.dbInstances[name]; delete this.dbInstances[name]; - return this.sqlite.deleteDatabase({ - name: name, - location: 'default' - }); + if (this.platform.is('cordova')) { + return this.sqlite.deleteDatabase({ + name: name, + location: 'default' + }); + } else { + // In WebSQL we cannot delete the database, just empty it. + return db.emptyDatabase(); + } }); } } - - -