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();
+ }
});
}
}
-
-
-