diff --git a/bower.json b/bower.json index 53d46adde1e266ec7387d796e2ca784a167fff1d..b0734e769b79b3532a303f274e5890995be5cd90 100644 --- a/bower.json +++ b/bower.json @@ -12,7 +12,8 @@ "angular-animate": "1.4.3", "angular-sanitize": "1.5.3", "angular": "1.5.3", - "angular-bind-notifier": "^1.1.7" + "angular-bind-notifier": "^1.1.7", + "angular-image-crop": "^2.0.0" }, "resolutions": { "angular-sanitize": "1.5.3", diff --git a/config.xml b/config.xml index c8aa375ee163db25a01bbe1ec7749d62bedc693e..b41329b83a3e26dde0142df0f5b02b1e480fc4dc 100644 --- a/config.xml +++ b/config.xml @@ -23,12 +23,12 @@ <preference name="SplashScreenDelay" value="4000" /> <preference name="xwalkVersion" value="19" /> <preference name="xwalkMultipleApk" value="false"/> + <preference name="android-minSdkVersion" value="16" /> <allow-navigation href="*" /> <feature name="StatusBar"> <param name="ios-package" onload="true" value="CDVStatusBar" /> </feature> <platform name="android"> - <preference name="android-minSdkVersion" value="19" /> <icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" /> <icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" /> <icon density="hdpi" src="resources/android/icon/drawable-hdpi-icon.png" /> diff --git a/hooks/after_prepare/020_remove_code.js b/hooks/after_prepare/020_remove_code.js index e9ced16196a7c7e9340870a3dc10b65579cd07a2..8b518ab85611d1ca8fb077fd24a1873e6d8ee0ce 100755 --- a/hooks/after_prepare/020_remove_code.js +++ b/hooks/after_prepare/020_remove_code.js @@ -50,11 +50,16 @@ if (rootdir) { .pipe(htmlmin()) .pipe(gulp.dest(wwwPath)), - // Remove unused JS code - gulp.src([wwwPath + + '/js/**/*.js', pluginPath + + '/js/**/*.js']) + // Remove unused JS code + add ng annotations + gulp.src([wwwPath + '/js/**/*.js']) .pipe(removeCode({device: true})) .pipe(ngAnnotate({single_quotes: true})) - .pipe(gulp.dest(".")) + .pipe(gulp.dest(wwwPath + '/dist/dist_js/app')), + + gulp.src([pluginPath + '/js/**/*.js']) + .pipe(removeCode({device: true})) + .pipe(ngAnnotate({single_quotes: true})) + .pipe(gulp.dest(wwwPath + '/dist/dist_js/plugins')) ); } diff --git a/www/i18n/locale-en-GB.json b/www/i18n/locale-en-GB.json index e43cbef60caf9c1fb48b900bbdc2aab429904749..3b9e4ed3d5019e5d55501ead43cbaa6f7e8a5436 100644 --- a/www/i18n/locale-en-GB.json +++ b/www/i18n/locale-en-GB.json @@ -106,7 +106,6 @@ "BTN_CURRENCY": "Explore currency", "BTN_ABOUT": "about", "BTN_HELP": "Help", - "BTN_ACCOUNT": "My Account", "REPORT_ISSUE": "Report an issue" }, "SETTINGS": { diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json index dc3eb1a2815ba35f22eba8fd7114698738bf9fd7..85be275d1aa2a568597bcebbd59970e41ebe7fb7 100644 --- a/www/i18n/locale-en.json +++ b/www/i18n/locale-en.json @@ -106,7 +106,6 @@ "BTN_CURRENCY": "Explore currency", "BTN_ABOUT": "about", "BTN_HELP": "Help", - "BTN_ACCOUNT": "My Account", "REPORT_ISSUE": "Report an issue" }, "SETTINGS": { diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json index 1cca03259838576fcecb75c3872a0b4abb183bac..f388b5bfd56849ef7a853977cec529bda887c4c2 100644 --- a/www/i18n/locale-fr-FR.json +++ b/www/i18n/locale-fr-FR.json @@ -106,7 +106,6 @@ "BTN_CURRENCY": "Explorer la monnaie", "BTN_ABOUT": "à propos", "BTN_HELP": "Aide en ligne", - "BTN_ACCOUNT": "Mon compte", "REPORT_ISSUE": "anomalie" }, "SETTINGS": { diff --git a/www/i18n/locale-nl-NL.json b/www/i18n/locale-nl-NL.json index 9eee9dc1198ff464b29cdf952ffde84a508fb26e..710b957e35b6275cd954aafb0c43c1cb8076ae32 100644 --- a/www/i18n/locale-nl-NL.json +++ b/www/i18n/locale-nl-NL.json @@ -101,7 +101,6 @@ "BTN_CURRENCY": "Verken valuta", "BTN_ABOUT": "over", "BTN_HELP": "Help", - "BTN_ACCOUNT": "Mijn rekening", "REPORT_ISSUE": "Meld een probleem" }, "SETTINGS": { diff --git a/www/index.html b/www/index.html index 1e7c8e2e17ce419d8f136cb7b01638683d4ff235..a9a243ed654635058b2b242537b76b8d34069e31 100644 --- a/www/index.html +++ b/www/index.html @@ -12,6 +12,7 @@ <link href="css/style.css" rel="stylesheet"> <!--removeIf(device)--> + <link href="lib/angular-image-crop/image-crop-styles.css" rel="stylesheet"> <link href="css/style-no-device.css" rel="stylesheet"> <!--endRemoveIf(device)--> @@ -41,11 +42,10 @@ <script src="lib/ionic/js/angular/angular-cache.js"></script> <script src="lib/ionic/js/angular/angular-screenmatch.min.js"></script> <script src="lib/ionic/js/angular/angular-bind-notifier.min.js"></script> - - <!-- crypto libs --> + <script src="lib/angular-image-crop/image-crop.js"></script> <script src="js/vendor/base58.js" async></script> - <script src="js/vendor/nacl_factory.js" async></script> <!--removeIf(device)--> + <script src="js/vendor/nacl_factory.js" async></script> <script src="js/vendor/scrypt-em.js" async></script> <script src="js/vendor/base64.js" async></script> <!--endRemoveIf(device)--> diff --git a/www/js/app.js b/www/js/app.js index d6f2bdad8755d443a93f74137e6dcfa484ac271b..20bdfe6b0e556343cd5d823972eed834c59e29af 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -7,6 +7,7 @@ angular.module('cesium', ['ionic', 'ionic-material', 'ngMessages', 'pascalprecht.translate', 'ngApi', 'angular-cache', 'angular.screenmatch', 'angular.bind.notifier', // removeIf(device) + 'ImageCropper', // endRemoveIf(device) // removeIf(no-device) 'ngCordova', diff --git a/www/js/services/crypto-services.js b/www/js/services/crypto-services.js index fc4a840002166478c0a1942c13b0ebe9473ed20e..062a22383ab68c20c9c9d56dc0bae30ae9b89c8c 100644 --- a/www/js/services/crypto-services.js +++ b/www/js/services/crypto-services.js @@ -5,250 +5,288 @@ angular.module('cesium.crypto.services', ['ngResource', 'cesium.device.services' .factory('CryptoUtils', function($q, $timeout, Device) { 'ngInject'; - // Const - var - crypto_sign_BYTES = 64, - crypto_secretbox_NONCEBYTES = 24, - SEED_LENGTH = 32, // Length of the key - SCRYPT_PARAMS = { + /** + * CryptoAbstract, abstract class with useful methods + * @type {number} + */ + function CryptoAbstractService() { + this.loaded = false; + var that = this; + + this.copy = function(source) { + _.forEach(_.keys(source), function(key) { + that[key] = source[key]; + }); + }; + + this.isLoaded = function() { + return this.loaded; + }; + + this.util = this.util || {}; + + /** + * Converts an array buffer to a string + * + * @private + * @param {ArrayBuffer} buf The buffer to convert + * @param {Function} callback The function to call when conversion is complete + */ + this.util.array_to_string = function(buf, callback) { + var bb = new Blob([new Uint8Array(buf)]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsText(bb); + }; + } + + CryptoAbstractService.prototype.constants = { + crypto_sign_BYTES: 64, + crypto_secretbox_NONCEBYTES: 24, + SEED_LENGTH: 32, // Length of the key + SCRYPT_PARAMS:{ N: 4096, r: 16, p: 1 - }, - // Web crypto API - see https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API - crypto = window.crypto || window.msCrypto || window.Crypto, + } + }; - async_load_base58 = function(on_ready) { + CryptoAbstractService.prototype.async_load_base58 = function(on_ready) { + var that = this; if (Base58 !== null){return on_ready(Base58);} - else {$timeout(function(){async_load_base58(on_ready);}, 100);} - }, + else {$timeout(function(){that.async_load_base58(on_ready);}, 100);} + }; - async_load_scrypt = function(on_ready, options) { + CryptoAbstractService.prototype.async_load_scrypt = function(on_ready, options) { + var that = this; if (scrypt_module_factory !== null){on_ready(scrypt_module_factory(options.requested_total_memory));} - else {$timeout(function(){async_load_scrypt(on_ready, options);}, 100);} - }, + else {$timeout(function(){that.async_load_scrypt(on_ready, options);}, 100);} + }; - async_load_nacl_js = function(on_ready, options) { + CryptoAbstractService.prototype.async_load_nacl_js = function(on_ready, options) { + var that = this; if (nacl_factory !== null) {nacl_factory.instantiate(on_ready, options);} - else {$timeout(function(){async_load_nacl_js(on_ready, options);}, 100);} - }, + else {$timeout(function(){that.async_load_nacl_js(on_ready, options);}, 100);} + }; - async_load_base64 = function(on_ready) { + CryptoAbstractService.prototype.async_load_base64 = function(on_ready) { + var that = this; if (Base64 !== null) {on_ready(Base64);} - else {$timetout(function(){async_load_base64(on_ready);}, 100);} - }, + else {$timetout(function(){that.async_load_base64(on_ready);}, 100);} + }; - async_load_sha256 = function(on_ready) { + CryptoAbstractService.prototype.async_load_sha256 = function(on_ready) { + var that = this; if (sha256 !== null){return on_ready(sha256);} - else {$timeout(function(){async_load_sha256(on_ready);}, 100);} + else {$timeout(function(){that.async_load_sha256(on_ready);}, 100);} }; - if (crypto) { - console.debug('Web Crypto API (window.crypto) exists: getRandomValues=[{0}]'.format(!!crypto.getRandomValues)); + // Web crypto API - see https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API + var crypto = window.crypto || window.msCrypto || window.Crypto; + if (crypto && crypto.getRandomValues) { + CryptoAbstractService.prototype.crypto = crypto; + CryptoAbstractService.prototype.util = {}; + CryptoAbstractService.prototype.util.random_nonce = function() { + var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES); + this.crypto.getRandomValues(nonce); + return $q.when(nonce); + }; + } + else { + // TODO: add a default function ? + //CryptoAbstractService.prototype.random_nonce = function() { + // var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES); + // var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + // for(var i = 0; i < length; i++) { + // text += possible.charAt(Math.floor(Math.random() * possible.length)); + // } + //} } function FullJSServiceFactory() { + this.id = 'FullJS'; + + // libraries handlers + this.scrypt = null; + this.nacl = null; + this.base58 = null; + this.base64 = null; + var that = this; + + this.util = this.util || {}; + this.util.decode_utf8 = function (s) { + var i, d = unescape(encodeURIComponent(s)), b = new Uint8Array(d.length); + for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); + return b; + }; + this.util.encode_utf8 = function (s) { + return that.nacl.encode_utf8(s); + }; + this.util.encode_base58 = function (a) { + return that.base58.encode(a); + }; + this.util.decode_base58 = function (a) { + var i; + a = that.base58.decode(a); + var b = new Uint8Array(a.length); + for (i = 0; i < a.length; i++) b[i] = a[i]; + return b; + }; + this.util.decode_base64 = function (a) { + return that.base64.decode(a); + }; + this.util.encode_base64 = function () { + return that.base64.encode(arguments); + }; - var - // libraries handlers - scrypt, - nacl, - base58, - base64, - loadedLib = 0, - - // functions - decode_utf8 = function(s) { - var i, d = unescape(encodeURIComponent(s)), b = new Uint8Array(d.length); - for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); - return b; - }, - encode_utf8 = function(s) { - return nacl.encode_utf8(s); - }, - encode_base58 = function(a) { - return base58.encode(a); - }, - decode_base58 = function(a) { - var i; - a = base58.decode(a); - var b = new Uint8Array(a.length); - for (i = 0; i < a.length; i++) b[i] = a[i]; - return b; - }, - hash_sha256 = function(message) { - return $q(function(resolve) { - var msg = decode_utf8(message); - var hash = nacl.to_hex(nacl.crypto_hash_sha256(msg)); - resolve(hash.toUpperCase()); - }); - }, - random_nonce = function() { - if (crypto && crypto.getRandomValues) { - var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES); - crypto.getRandomValues(nonce); - return $q.when(nonce); - } - else { - // TODO - // var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES); - // var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - // for(var i = 0; i < length; i++) { - // text += possible.charAt(Math.floor(Math.random() * possible.length)); - // } - return $q.when(nacl.crypto_box_random_nonce()); - } - }, - - /** - * Converts an array buffer to a string - * - * @private - * @param {ArrayBuffer} buf The buffer to convert - * @param {Function} callback The function to call when conversion is complete - */ - array_to_string = function(buf, callback) { - var bb = new Blob([new Uint8Array(buf)]); - var f = new FileReader(); - f.onload = function(e) { - callback(e.target.result); - }; - f.readAsText(bb); - }, - - /** - * Create key pairs (sign and box), from salt+password - */ - connect = function(salt, password) { - return $q(function(resolve, reject) { - var seed = scrypt.crypto_scrypt( - nacl.encode_utf8(password), - nacl.encode_utf8(salt), - SCRYPT_PARAMS.N, - SCRYPT_PARAMS.r, - SCRYPT_PARAMS.p, - SEED_LENGTH); - var signKeypair = nacl.crypto_sign_seed_keypair(seed); - var boxKeypair = nacl.crypto_box_seed_keypair(seed); - resolve({ - signPk: signKeypair.signPk, - signSk: signKeypair.signSk, - boxPk: boxKeypair.boxPk, - boxSk: boxKeypair.boxSk - }); - }); - }, - - /** - * Verify a signature of a message, for a pubkey - */ - verify = function (message, signature, pubkey) { - return $q(function(resolve, reject) { - var msg = decode_utf8(message); - var sig = base64.decode(signature); - var pub = base58.decode(pubkey); - var m = new Uint8Array(crypto_sign_BYTES + msg.length); - var sm = new Uint8Array(crypto_sign_BYTES + msg.length); - var i; - for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; - for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; - - // Call to verification lib... - var verified = nacl.crypto_sign_open(sm, pub) !== null; - resolve(verified); - }); - }, - - /** - * Sign a message, from a key pair - */ - sign = function(message, keypair) { - return $q(function(resolve, reject) { - var m = decode_utf8(message); - var sk = keypair.signSk; - var signedMsg = nacl.crypto_sign(m, sk); - var sig = new Uint8Array(crypto_sign_BYTES); - for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; - var signature = base64.encode(sig); - resolve(signature); - }); - }, + this.util.hash_sha256 = function (message) { + return $q(function (resolve) { + var msg = that.util.decode_utf8(message); + var hash = that.nacl.to_hex(that.nacl.crypto_hash_sha256(msg)); + resolve(hash.toUpperCase()); + }); + }; + this.util.random_nonce = function () { + if (that.crypto && that.crypto.getRandomValues) { + var nonce = new Uint8Array(that.constants.crypto_secretbox_NONCEBYTES); + that.crypto.getRandomValues(nonce); + return $q.when(nonce); + } + else { + return $q.when(that.nacl.crypto_box_random_nonce()); + } + }; - /** - * Compute the box key pair, from a sign key pair - */ - box_keypair_from_sign = function(signKeyPair) { - if (signKeyPair.bokSk && signKeyPair.boxPk) return $q.when(signKeyPair); - return $q.when(nacl.crypto_box_keypair_from_sign_sk(signKeyPair.signSk)); - }, + /** + * Compute the box key pair, from a sign key pair + */ + this.box_keypair_from_sign = function (signKeyPair) { + if (signKeyPair.bokSk && signKeyPair.boxPk) return $q.when(signKeyPair); + return $q.when(that.nacl.crypto_box_keypair_from_sign_sk(signKeyPair.signSk)); + }; - /** - * Compute the box public key, from a sign public key - */ - box_pk_from_sign = function(signPk) { - return $q.when(nacl.crypto_box_pk_from_sign_pk(signPk)); - }, + /** + * Compute the box public key, from a sign public key + */ + this.box_pk_from_sign = function (signPk) { + return $q.when(that.nacl.crypto_box_pk_from_sign_pk(signPk)); + }; - /** - * Encrypt a message, from a key pair - */ - box = function(message, nonce, recipientPk, senderSk) { - return $q(function(resolve, reject) { - if (!message) { - resolve(message); - return; - } - var messageBin = decode_utf8(message); - if (typeof recipientPk === "string") { - recipientPk = decode_base58(recipientPk); - } + /** + * Encrypt a message, from a key pair + */ + this.box = function (message, nonce, recipientPk, senderSk) { + return $q(function (resolve, reject) { + if (!message) { + resolve(message); + return; + } + var messageBin = that.util.decode_utf8(message); + if (typeof recipientPk === "string") { + recipientPk = that.util.decode_base58(recipientPk); + } - //console.debug('Original message: ' + message); - try { - var ciphertextBin = nacl.crypto_box(messageBin, nonce, recipientPk, senderSk); - var ciphertext = base64.encode(ciphertextBin); - //console.debug('Encrypted message: ' + ciphertext); - resolve(ciphertext); - } - catch(err) { - reject(err); - } - }); - }, + //console.debug('Original message: ' + message); + try { + var ciphertextBin = that.nacl.crypto_box(messageBin, nonce, recipientPk, senderSk); + var ciphertext = that.util.encode_base64(ciphertextBin); + //console.debug('Encrypted message: ' + ciphertext); + resolve(ciphertext); + } + catch (err) { + reject(err); + } + }); + }; /** * Decrypt a message, from a key pair */ - box_open = function(cypherText, nonce, senderPk, recipientSk) { - return $q(function(resolve, reject) { + this.box_open = function (cypherText, nonce, senderPk, recipientSk) { + return $q(function (resolve, reject) { if (!cypherText) { resolve(cypherText); return; } - var ciphertextBin = base64.decode(cypherText); + var ciphertextBin = that.util.decode_base64(cypherText); if (typeof senderPk === "string") { - senderPk = CryptoUtils.util.decode_base58(senderPk); + senderPk = that.util.decode_base58(senderPk); } try { - var message = nacl.crypto_box_open(ciphertextBin, nonce, senderPk, recipientSk); - array_to_string(message, function(result) { + var message = that.nacl.crypto_box_open(ciphertextBin, nonce, senderPk, recipientSk); + that.util.array_to_string(message, function (result) { //console.debug('Decrypted text: ' + result); resolve(result); }); } - catch(err) { + catch (err) { reject(err); } }); - }, + }; - isLoaded = function() { - return loadedLib === 4; - }, + /** + * Create key pairs (sign and box), from salt+password + */ + this.connect = function(salt, password) { + return $q(function(resolve, reject) { + var seed = that.scrypt.crypto_scrypt( + that.util.encode_utf8(password), + that.util.encode_utf8(salt), + that.constants.SCRYPT_PARAMS.N, + that.constants.SCRYPT_PARAMS.r, + that.constants.SCRYPT_PARAMS.p, + that.constants.SEED_LENGTH); + var signKeypair = that.nacl.crypto_sign_seed_keypair(seed); + var boxKeypair = that.nacl.crypto_box_seed_keypair(seed); + resolve({ + signPk: signKeypair.signPk, + signSk: signKeypair.signSk, + boxPk: boxKeypair.boxPk, + boxSk: boxKeypair.boxSk + }); + }); + }; - load = function() { + /** + * Verify a signature of a message, for a pubkey + */ + this.verify = function (message, signature, pubkey) { + return $q(function(resolve, reject) { + var msg = that.util.decode_utf8(message); + var sig = that.util.decode_base64(signature); + var pub = that.util.decode_base58(pubkey); + var sm = new Uint8Array(that.constants.crypto_sign_BYTES + msg.length); + var i; + for (i = 0; i < that.constants.crypto_sign_BYTES; i++) sm[i] = sig[i]; + for (i = 0; i < msg.length; i++) sm[i+that.constants.crypto_sign_BYTES] = msg[i]; + + // Call to verification lib... + var verified = that.nacl.crypto_sign_open(sm, pub) !== null; + resolve(verified); + }); + }; + + /** + * Sign a message, from a key pair + */ + this.sign = function(message, keypair) { + return $q(function(resolve, reject) { + var m = that.util.decode_utf8(message); + var sk = keypair.signSk; + var signedMsg = that.nacl.crypto_sign(m, sk); + var sig = new Uint8Array(that.constants.crypto_sign_BYTES); + for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; + var signature = that.base64.encode(sig); + resolve(signature); + }); + }; + + this.load = function() { var deferred = $q.defer(); var naclOptions = {}; var scryptOptions = {}; @@ -258,274 +296,252 @@ angular.module('cesium.crypto.services', ['ngResource', 'cesium.device.services' console.log('Reduce Scrypt memory because plateform grade is not [a] but [' + ionic.Platform.grade + ']'); scryptOptions.requested_total_memory = 16 * 1048576; // 16 Mo } + var loadedLib = 0; var checkAllLibLoaded = function() { loadedLib++; - if (isLoaded()) { + if (loadedLib === 4) { + that.loaded = true; deferred.resolve(); } }; - async_load_nacl_js(function(lib) { - nacl = lib; + this.async_load_nacl_js(function(lib) { + that.nacl = lib; checkAllLibLoaded(); }, naclOptions); - async_load_scrypt(function(lib) { - scrypt = lib; + this.async_load_scrypt(function(lib) { + that.scrypt = lib; checkAllLibLoaded(); }, scryptOptions); - async_load_base58(function(lib) { - base58 = lib; + this.async_load_base58(function(lib) { + that.base58 = lib; checkAllLibLoaded(); }); - async_load_base64(function(lib) { - base64 = lib; + this.async_load_base64(function(lib) { + that.base64 = lib; checkAllLibLoaded(); }); return deferred.promise; }; - // Service's exposed methods - return { - isLoaded: isLoaded, - load: load, - util: { - encode_utf8: encode_utf8, - decode_utf8: decode_utf8, - encode_base58: encode_base58, - decode_base58: decode_base58, - hash: hash_sha256, - encode_base64: function() {return base64.encode(arguments);}, - random_nonce: random_nonce + // Shortcuts + this.util.hash = that.util.hash_sha256; + this.box = { + keypair: { + fromSignKeypair: that.box_keypair_from_sign, + pkFromSignPk: that.box_pk_from_sign }, - connect: connect, - sign: sign, - verify: verify, - box: { - keypair: { - fromSignKeypair: box_keypair_from_sign, - pkFromSignPk: box_pk_from_sign - }, - pack: box, - open: box_open - } + pack: that.box, + open: that.box_open }; } + FullJSServiceFactory.prototype = new CryptoAbstractService(); + + + /* ----------------------------------------------------------------------------------------------------------------- + * Service that use Cordova MiniSodium plugin + * ----------------------------------------------------------------------------------------------------------------*/ /*** * Factory for crypto, using Cordova plugin */ function CordovaServiceFactory() { - var - // libraries handlers - nacl, // the cordova plugin - nacl_js, // the full JS lib (need for random values) - base58, - sha256, - loadedLib = 0, - - // functions - decode_utf8 = function(s) { - return nacl.to_string(s); - }, - encode_utf8 = function(s) { - return nacl.from_string(s); - }, - encode_base58 = function(a) { - return base58.encode(a); - }, - decode_base58 = function(a) { - var i; - a = base58.decode(a); - var b = new Uint8Array(a.length); - for (i = 0; i < a.length; i++) b[i] = a[i]; - return b; - }, - hash_sha256 = function(message) { - return $q.when(sha256(message).toUpperCase()); - }, - random_nonce = function() { - if (crypto && crypto.getRandomValues) { - var nonce = new Uint8Array(crypto_secretbox_NONCEBYTES); - crypto.getRandomValues(nonce); - return $q.when(nonce); - } - else { - return $q.when(nacl_js.crypto_box_random_nonce()); - } - }, - /** - * Converts an array buffer to a string - * - * @private - * @param {ArrayBuffer} buf The buffer to convert - * @param {Function} callback The function to call when conversion is complete - */ - array_to_string = function(buf, callback) { - var bb = new Blob([new Uint8Array(buf)]); - var f = new FileReader(); - f.onload = function(e) { - callback(e.target.result); - }; - f.readAsText(bb); - }, - /** - * Create key pairs (sign and box), from salt+password, using cordova - */ - connect = function(salt, password) { - var deferred = $q.defer(); - - nacl.crypto_pwhash_scryptsalsa208sha256_ll( - nacl.from_string(password), - nacl.from_string(salt), - SCRYPT_PARAMS.N, - SCRYPT_PARAMS.r, - SCRYPT_PARAMS.p, - SEED_LENGTH, - function (err, seed) { - if (err) { deferred.reject(err); return;} - nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) { - if (err) { deferred.reject(err); return;} - var result = { - signPk: signKeypair.pk, - signSk: signKeypair.sk - }; - box_keypair_from_sign(result) - .then(function(boxKeypair) { - result.boxPk = boxKeypair.pk; - result.boxSk = boxKeypair.sk; - deferred.resolve(result); - }) - .catch(function(err) { - deferred.reject(err); - }); - }); + this.id = 'MiniSodium'; - } - ); + // libraries handlers + this.nacl = null; // the cordova plugin + this.nacl_js= null; // the full JS lib (need for random values) + this.base58= null; + this.sha256= null; + var that = this; - return deferred.promise; - }, + // functions + this.util = this.util || {}; + this.util.decode_utf8 = function(s) { + return that.nacl.to_string(s); + }; + this.util.encode_utf8 = function(s) { + return that.nacl.from_string(s); + }; + this.util.encode_base58 = function(a) { + return that.base58.encode(a); + }; + this.util.decode_base58 = function(a) { + var i; + a = that.base58.decode(a); + var b = new Uint8Array(a.length); + for (i = 0; i < a.length; i++) b[i] = a[i]; + return b; + }; + this.util.hash_sha256 = function(message) { + return $q.when(that.sha256(message).toUpperCase()); + }; + this.util.random_nonce = function() { + if (that.crypto && that.crypto.getRandomValues) { + var nonce = new Uint8Array(that.constants.crypto_secretbox_NONCEBYTES); + that.crypto.getRandomValues(nonce); + return $q.when(nonce); + } + else { + return $q.when(that.nacl_js.crypto_box_random_nonce()); + } + }; - /** - * Verify a signature of a message, for a pubkey - */ - verify = function (message, signature, pubkey) { - var deferred = $q.defer(); - nacl.crypto_sign_verify_detached( - nacl.from_base64(signature), - nacl.from_string(message), - nacl.from_base64(pubkey), - function(err, verified) { - if (err) { deferred.reject(err); return;} - deferred.resolve(verified); - }); - return deferred.promise; - }, + /** + * Create key pairs (sign and box), from salt+password, using cordova + */ + this.connect = function(salt, password) { + var deferred = $q.defer(); - /** - * Sign a message, from a key pair - */ - sign = function(message, keypair) { - var deferred = $q.defer(); - var nacl = window.plugins.MiniSodium; - - nacl.crypto_sign( - nacl.from_string(message), // message - keypair.signSk, // sk - function(err, signedMsg) { + that.nacl.crypto_pwhash_scryptsalsa208sha256_ll( + that.nacl.from_string(password), + that.nacl.from_string(salt), + that.constants.SCRYPT_PARAMS.N, + that.constants.SCRYPT_PARAMS.r, + that.constants.SCRYPT_PARAMS.p, + that.constants.SEED_LENGTH, + function (err, seed) { + if (err) { deferred.reject(err); return;} + that.nacl.crypto_sign_seed_keypair(seed, function (err, signKeypair) { if (err) { deferred.reject(err); return;} - var sig; - if (signedMsg.length > crypto_sign_BYTES) { - sig = new Uint8Array(crypto_sign_BYTES); - for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; - console.debug("//******** HAS REDUCE signedMsg ********* /// "); - } - else { - sig = signedMsg; - } - var signature = nacl.to_base64(sig); - deferred.resolve(signature); + var result = { + signPk: signKeypair.pk, + signSk: signKeypair.sk + }; + that.box_keypair_from_sign(result) + .then(function(boxKeypair) { + result.boxPk = boxKeypair.pk; + result.boxSk = boxKeypair.sk; + deferred.resolve(result); + }) + .catch(function(err) { + deferred.reject(err); + }); }); - return deferred.promise; - }, + } + ); - /** - * Compute the box key pair, from a sign key pair - */ - box_keypair_from_sign = function(signKeyPair) { - console.log("box_keypair_from_sign"); - if (signKeyPair.bokSk && signKeyPair.boxPk) return $q.when(signKeyPair); - var deferred = $q.defer(); - var result = {}; - nacl.crypto_sign_ed25519_pk_to_curve25519(signKeyPair.signPk, function(err, boxPk) { - if (err) { deferred.reject(err); return;} - result.boxPk = boxPk; - if (result.boxSk) deferred.resolve(result); - }); - nacl.crypto_sign_ed25519_sk_to_curve25519(signKeyPair.signSk, function(err, boxSk) { + return deferred.promise; + }; + + /** + * Verify a signature of a message, for a pubkey + */ + this.verify = function (message, signature, pubkey) { + var deferred = $q.defer(); + that.nacl.crypto_sign_verify_detached( + that.nacl.from_base64(signature), + that.nacl.from_string(message), + that.nacl.from_base64(pubkey), + function(err, verified) { if (err) { deferred.reject(err); return;} - result.boxSk = boxSk; - if (result.boxPk) deferred.resolve(result); + deferred.resolve(verified); }); + return deferred.promise; + }; - return deferred.promise; - }, + /** + * Sign a message, from a key pair + */ + this.sign = function(message, keypair) { + var deferred = $q.defer(); - /** - * Compute the box public key, from a sign public key - */ - box_pk_from_sign = function(signPk) { - var deferred = $q.defer(); - nacl.crypto_sign_ed25519_pk_to_curve25519(signPk, function(err, boxPk) { + that.nacl.crypto_sign( + that.nacl.from_string(message), // message + keypair.signSk, // sk + function(err, signedMsg) { if (err) { deferred.reject(err); return;} - deferred.resolve(boxPk); + var sig; + if (signedMsg.length > that.constants.crypto_sign_BYTES) { + sig = new Uint8Array(that.constants.crypto_sign_BYTES); + for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; + console.debug("//******** HAS REDUCE signedMsg ********* /// "); + } + else { + sig = signedMsg; + } + var signature = that.nacl.to_base64(sig); + deferred.resolve(signature); }); - return deferred.promise; - }, - /** - * Encrypt a message, from a key pair - */ - box = function(message, nonce, recipientPk, senderSk) { - if (!message) { - return $q.reject('No message'); - } - var deferred = $q.defer(); - var messageBin = encode_utf8(message); - if (typeof recipientPk === "string") { - recipientPk = decode_base58(recipientPk); - } + return deferred.promise; + }; - //console.debug('Original message: ' + message); + /** + * Compute the box key pair, from a sign key pair + */ + this.box_keypair_from_sign = function(signKeyPair) { + if (signKeyPair.bokSk && signKeyPair.boxPk) return $q.when(signKeyPair); + var deferred = $q.defer(); + var result = {}; + that.nacl.crypto_sign_ed25519_pk_to_curve25519(signKeyPair.signPk, function(err, boxPk) { + if (err) { deferred.reject(err); return;} + result.boxPk = boxPk; + if (result.boxSk) deferred.resolve(result); + }); + that.nacl.crypto_sign_ed25519_sk_to_curve25519(signKeyPair.signSk, function(err, boxSk) { + if (err) { deferred.reject(err); return;} + result.boxSk = boxSk; + if (result.boxPk) deferred.resolve(result); + }); - nacl.crypto_box_easy(messageBin, nonce, recipientPk, senderSk, function(err, ciphertextBin) { - if (err) { deferred.reject(err); return;} - var ciphertext = nacl.to_base64(ciphertextBin); - //console.debug('Encrypted message: ' + ciphertext); - deferred.resolve(ciphertext); - }); - return deferred.promise; - }; + return deferred.promise; + }; + + /** + * Compute the box public key, from a sign public key + */ + this.box_pk_from_sign = function(signPk) { + var deferred = $q.defer(); + that.nacl.crypto_sign_ed25519_pk_to_curve25519(signPk, function(err, boxPk) { + if (err) { deferred.reject(err); return;} + deferred.resolve(boxPk); + }); + return deferred.promise; + }; + + /** + * Encrypt a message, from a key pair + */ + this.box = function(message, nonce, recipientPk, senderSk) { + if (!message) { + return $q.reject('No message'); + } + var deferred = $q.defer(); + var messageBin = that.nacl.from_string(message); + if (typeof recipientPk === "string") { + recipientPk = that.util.decode_base58(recipientPk); + } + + //console.debug('Original message: ' + message); + + that.nacl.crypto_box_easy(messageBin, nonce, recipientPk, senderSk, function(err, ciphertextBin) { + if (err) { deferred.reject(err); return;} + var ciphertext = that.nacl.to_base64(ciphertextBin); + //console.debug('Encrypted message: ' + ciphertext); + deferred.resolve(ciphertext); + }); + return deferred.promise; + }; /** * Decrypt a message, from a key pair */ - box_open = function(cypherText, nonce, senderPk, recipientSk) { - console.log("box_open"); + this.box_open = function(cypherText, nonce, senderPk, recipientSk) { if (!cypherText) { return $q.reject('No cypherText'); } var deferred = $q.defer(); - var ciphertextBin = nacl.from_base64(cypherText); + var ciphertextBin = that.nacl.from_base64(cypherText); if (typeof senderPk === "string") { - senderPk = decode_base58(senderPk); + senderPk = that.util.decode_base58(senderPk); } - nacl.crypto_box_open_easy(ciphertextBin, nonce, senderPk, recipientSk, function(err, message) { + that.nacl.crypto_box_open_easy(ciphertextBin, nonce, senderPk, recipientSk, function(err, message) { if (err) { deferred.reject(err); return;} - array_to_string(message, function(result) { + that.util.array_to_string(message, function(result) { //console.debug('Decrypted text: ' + result); deferred.resolve(result); }); @@ -534,99 +550,84 @@ angular.module('cesium.crypto.services', ['ngResource', 'cesium.device.services' return deferred.promise; }; - isLoaded = function(){ - return loadedLib === 3; - }; - - load = function() { + this.load = function() { var deferred = $q.defer(); if (!window.plugins || !window.plugins.MiniSodium) { - deferred.reject("Cordova plugin 'MiniSodium' not found"); + deferred.reject("Cordova plugin 'MiniSodium' not found. Please load Full JS implementation instead."); } else { - nacl = window.plugins.MiniSodium; + that.nacl = window.plugins.MiniSodium; + var loadedLib = 0; + var expectedLoadedLib = 2; var checkAllLibLoaded = function() { loadedLib++; - if (isLoaded()) { + if (loadedLib == expectedLoadedLib) { + that.loaded = true; deferred.resolve(); } }; - async_load_base58(function(lib) { - base58 = lib; + that.async_load_base58(function(lib) { + that.base58 = lib; checkAllLibLoaded(); }); - async_load_sha256(function(lib) { - sha256 = lib; + that.async_load_sha256(function(lib) { + that.sha256 = lib; checkAllLibLoaded(); }); - async_load_nacl_js(function(lib) { - nacl_js = lib; - checkAllLibLoaded(); - }); - + if (!that.crypto || !that.crypto.getRandomValues) { + console.debug('[crypto] Web Crypto API (window.crypto) NOT exists with getRandomValues. Will load nacl_factory'); + expectedLoadedLib++; + that.async_load_nacl_js(function(lib) { + that.nacl_js = lib; + checkAllLibLoaded(); + }); + } } return deferred.promise; }; - // Service's exposed methods - return { - isLoaded: isLoaded, - load: load, - util: { - encode_utf8: encode_utf8, - decode_utf8: decode_utf8, - encode_base58: encode_base58, - decode_base58: decode_base58, - hash: hash_sha256, - encode_base64: function() {return nacl.to_base64(arguments);}, - random_nonce: random_nonce + // Shortcuts + this.util.hash = that.util.hash_sha256; + this.box = { + keypair: { + fromSignKeypair: that.box_keypair_from_sign, + pkFromSignPk: that.box_pk_from_sign }, - connect: connect, - sign: sign, - verify: verify, - box: { - keypair: { - fromSignKeypair: box_keypair_from_sign, - pkFromSignPk: box_pk_from_sign - }, - pack: box, - open: box_open - } + pack: that.box, + open: that.box_open }; } + CordovaServiceFactory.prototype = new CryptoAbstractService(); - var service = { - isLoaded: function(){return false;} - }; + /* ----------------------------------------------------------------------------------------------------------------- + * Create service instance + * ----------------------------------------------------------------------------------------------------------------*/ - // We use 'Device.ready()' instead of '$ionicPlatform.ready()', because it could be call many times - Device.ready() - .then(function() { - var now = new Date().getTime(); - var serviceImpl; - var serviceImplName; - - // Use cordova implementaion, when possible - if (window.plugins && window.plugins.MiniSodium) { - serviceImplName = 'MiniSodium'; - serviceImpl = CordovaServiceFactory(); - } - else { - serviceImplName = 'full JS'; - serviceImpl = FullJSServiceFactory(); - } - return serviceImpl.load() - .catch(function(err) { - console.error(err); - throw err; - }) - .then(function() { - angular.copy(serviceImpl, service); - console.debug('[crypto] Loaded \'{0}\' implementation in {1}ms'.format(serviceImplName, new Date().getTime() - now)); - }); - }); + // Use cordova implementation, when possible + var service; + if (Device.enable) { + service = new CordovaServiceFactory(); + } + else { + service = new FullJSServiceFactory(); + } + + Device.ready().then(function() { + var now = new Date().getTime(); + // Load (async lib) + service.load() + .catch(function(err) { + //console.error(err); + throw err; + }) + .then(function() { + console.debug('[crypto] Loaded \'{0}\' implementation in {1}ms'.format(service.id, new Date().getTime() - now)); + }); + + }); + return service; }) diff --git a/www/js/services/device-services.js b/www/js/services/device-services.js index 2571d2527f780e6dc7f5e698fc202816ebc81efc..5c3ed73208f08334062c2ee0157735252c783e53 100644 --- a/www/js/services/device-services.js +++ b/www/js/services/device-services.js @@ -1,162 +1,156 @@ angular.module('cesium.device.services', ['ngResource', 'cesium.utils.services']) -.factory('Device', - function(UIUtils, $translate, $ionicPopup, $q, - // removeIf(no-device) - $cordovaClipboard, $cordovaBarcodeScanner, $cordovaCamera, - // endRemoveIf(no-device) - $ionicPlatform - ) { - 'ngInject'; - - var CONST = { - MAX_HEIGHT: 400, - MAX_WIDTH: 400 - }, - readyPromise, - - // workaround to quickly no is device or not (even before the ready() event) - enable = true; - // removeIf(device) - enable = false; - // endRemoveIf(device) - - // Replace the '$ionicPlatform.ready()', to enable multiple calls - ready = function () { - if (!readyPromise) { - readyPromise = $ionicPlatform.ready().then(function(){ - console.debug('[ionic] Platform is ready'); - }); - } - return readyPromise; - }; - - /*isEnable = function() { - return enable; - };*/ - - getPicture = function(sourceType) { - return $q(function (resolve, reject) { - if (!enable) { - reject("Camera scanner not enable. Please call 'Device.ready()' once before use (e.g in app.js)."); - return; - } - if (!sourceType) { - $translate(['SYSTEM.PICTURE_CHOOSE_TYPE', 'SYSTEM.BTN_PICTURE_GALLERY', 'SYSTEM.BTN_PICTURE_CAMERA']) - .then(function(translations){ - $ionicPopup.show({ - title: translations['SYSTEM.PICTURE_CHOOSE_TYPE'], - buttons: [ - { - text: translations['SYSTEM.BTN_PICTURE_GALLERY'], - type: 'button', - onTap: function(e) { - return navigator.camera.PictureSourceType.PHOTOLIBRARY; - } - }, - { - text: translations['SYSTEM.BTN_PICTURE_CAMERA'], - type: 'button button-positive', - onTap: function(e) { - return navigator.camera.PictureSourceType.CAMERA; - } - } - ] - }) - .then(function(sourceType){ - getPicture(sourceType); - }); - }); - } - else { - var options = { - quality: 50, - destinationType: navigator.camera.DestinationType.DATA_URL, - sourceType: sourceType, - encodingType: navigator.camera.EncodingType.PNG, - targetWidth : CONST.MAX_WIDTH, - targetHeight : CONST.MAX_HEIGHT + .factory('Device', + function(UIUtils, $translate, $ionicPopup, $q, + // removeIf(no-device) + $cordovaClipboard, $cordovaBarcodeScanner, $cordovaCamera, + // endRemoveIf(no-device) + $ionicPlatform) { + 'ngInject'; + + var + CONST = { + MAX_HEIGHT: 400, + MAX_WIDTH: 400 + }, + readyPromise, + exports = { + // workaround to quickly no is device or not (even before the ready() event) + enable: true }; - $cordovaCamera.getPicture( - function (imageData) {resolve(imageData);}, - function(err){reject(err);}, - options - ); + + // removeIf(device) + // workaround to quickly no is device or not (even before the ready() event) + exports.enable = false; + console.log("TOTOTOTOT - should have been removed on DEVICE !!!"); + // endRemoveIf(device) + + // Replace the '$ionicPlatform.ready()', to enable multiple calls + function ready() { + if (!readyPromise) { + readyPromise = $ionicPlatform.ready().then(function(){ + console.debug('[ionic] Platform is ready'); + }); + } + return readyPromise; } - }); - }; - - scan = function (n) { - return $q(function(resolve,reject){ - if (!enable) { - reject("Barcode scanner not enable. Please call 'Device.ready()' once before use (e.g in app.js)."); - return; + + function getPicture(options) { + if (!exports.enable) { + return $q.reject("Camera not enable. Please call 'Device.ready()' once before use (e.g in app.js)."); + } + + // Options is the sourceType by default + if (options && (typeof options === "string")) { + options = { + sourceType: options + }; + } + options = options || {}; + + // Make sure a source type has been given (if not, ask user) + if (angular.isUndefined(options.sourceType)) { + return $translate(['SYSTEM.PICTURE_CHOOSE_TYPE', 'SYSTEM.BTN_PICTURE_GALLERY', 'SYSTEM.BTN_PICTURE_CAMERA']) + .then(function(translations){ + return $ionicPopup.show({ + title: translations['SYSTEM.PICTURE_CHOOSE_TYPE'], + buttons: [ + { + text: translations['SYSTEM.BTN_PICTURE_GALLERY'], + type: 'button', + onTap: function(e) { + return navigator.camera.PictureSourceType.PHOTOLIBRARY; + } + }, + { + text: translations['SYSTEM.BTN_PICTURE_CAMERA'], + type: 'button button-positive', + onTap: function(e) { + return navigator.camera.PictureSourceType.CAMERA; + } + } + ] + }) + .then(function(sourceType){ + console.info('[camera] User select sourceType:' + sourceType); + options.sourceType = sourceType; + return exports.camera.getPicture(options); + }); + }); + } + + options.quality = options.quality || 50; + options.destinationType = options.destinationType || navigator.camera.DestinationType.DATA_URL; + options.encodingType = options.encodingType || navigator.camera.EncodingType.PNG; + options.targetWidth = options.targetWidth || CONST.MAX_WIDTH; + options.targetHeight = options.targetHeight || CONST.MAX_HEIGHT; + return $cordovaCamera.getPicture(options); } - cordova.plugins.barcodeScanner.scan( - function(result) { - //console.log('bar code result'); - //console.log(result); - if (!result.cancelled) { - resolve(result.text); // make sure to convert into String + + function scan(n) { + if (!exports.enable) { + return $q.reject("Barcode scanner not enable. Please call 'Device.ready()' once before use (e.g in app.js)."); } - else { - resolve(); + var deferred = $q.defer(); + cordova.plugins.barcodeScanner.scan( + function(result) { + //console.log('bar code result'); + //console.log(result); + if (!result.cancelled) { + deferred.resolve(result.text); // make sure to convert into String + } + else { + deferred.resolve(); + } + }, + function(err) {deferred.reject(err);}, + n); + return deferred.promise; + } + + function copy(text, callback) { + if (!exports.enable) { + return; // do nothing if not available } - }, - function(err) {reject(err);}, - n); - }); - }; - - copy = function (text, callback) { - if (!enable) { - return; // do nothing if not available - } - $cordovaClipboard - .copy(text) - .then(function () { - // success - if (callback) { - callback(); + $cordovaClipboard + .copy(text) + .then(function () { + // success + if (callback) { + callback(); + } + else { + UIUtils.toast.show('INFO.COPY_TO_CLIPBOARD_DONE'); + } + }, function () { + // error + UIUtils.alert.error('ERROR.COPY_CLIPBOARD'); + }); + } + + // On platform ready: check if device could be used + ready().then(function() { + var enableCamera = !!navigator.camera; + + exports.enable = enableCamera; + + if (exports.enable){ + var enableBarcodeScanner = cordova && cordova.plugins && !!cordova.plugins.barcodeScanner; + console.debug('[device] Ready with [barcodescanner={0}] [camera={1}]'.format(enableBarcodeScanner, enableCamera)); } else { - UIUtils.toast.show('INFO.COPY_TO_CLIPBOARD_DONE'); + console.debug('[device] No device detected'); } - }, function () { - // error - UIUtils.alert.error('ERROR.COPY_CLIPBOARD'); }); - }; - - // On platform ready: check if device could be used - ready().then(function() { - enable = !!navigator.camera; - - if (enable){ - var enableBarcodeScanner = cordova && cordova.plugins && cordova.plugins.barcodeScanner; - console.debug(' barcodescanner: {0}'.format(enableBarcodeScanner)); - console.debug(' camera: {0}'.format(enable)); - } - if (!enable) { - console.debug('[device] No device detected'); - } - else { - console.debug('[device] Ready'); - } - }); - - return { - ready: ready, - enable: enable, - clipboard: { - copy: copy - }, - camera: { - getPicture : getPicture, - scan: scan - } - }; -}) - -; + + exports.ready = ready; + exports.clipboard = {copy: copy}; + exports.camera = { + getPicture : getPicture, + scan: scan + }; + return exports; + }) + + ; diff --git a/www/js/services/storage-services.js b/www/js/services/storage-services.js index 5eb5fc16fbba492394fb8e255fa555ec19bc64d9..f62040fc05b491cfd135db424fb03a7fe565a90c 100644 --- a/www/js/services/storage-services.js +++ b/www/js/services/storage-services.js @@ -86,8 +86,7 @@ angular.module('cesium.storage.services', ['ngResource', 'cesium.device.services }); Device.ready().then(function() { - console.log($window.plugins && $window.plugins.SecureStorage); - if ($window.plugins) { + if (Device.enable) { exports.secure.storage = new cordova.plugins.SecureStorage( function () { console.log('[storage] Secure storage initialized.'); diff --git a/www/lib/ionic/css/image-crop-styles.css b/www/lib/ionic/css/image-crop-styles.css new file mode 100644 index 0000000000000000000000000000000000000000..2da5e010667905e2e2e5b8f62a25e73a2c8bd9b6 --- /dev/null +++ b/www/lib/ionic/css/image-crop-styles.css @@ -0,0 +1,69 @@ +/* Some of these styles you can override, things like colors, + * however some styles are required for the structure, and are critical to this module behaving properly! + */ + +/* Core component styles */ +.ng-image-crop { + text-align: center; + margin: 0 auto; + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} +/* Each of the 3 steps in the process are contained within sections */ +.ng-image-crop > section { + background: #ccc; +} +/* The cropping button */ +.ng-image-crop button { + margin-top: 10px; +} +/* The dashed cropping guideline */ +.ng-image-crop .cropping-guide { + display: block; + background: rgba(255, 255, 255, .3); + border: 2px dashed white; + position: absolute; + pointer-events: none; +} +/* The circular themed cropping guideline */ +.ng-image-crop--circle .cropping-guide { + border-radius: 50%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + -ms-border-radius: 50%; + -o-border-radius: 50%; +} +/* The canvas where the user positions the image via dragging and zooming */ +.ng-image-crop .cropping-canvas { + background: rgba(255, 255, 255, .3); + margin: 0 auto; + cursor: move; +} +/* The overlayed draggable zoom handle in the corner of the module */ +.ng-image-crop .zoom-handle { + display: block; + position: absolute; + bottom: 1px; + left: 1px; + background: rgba(255,255,255,0.7); + width: 80px; + height: 80px; + cursor: move; + border-radius: 200px 50px; +} +/* The text within the zoom handle */ +.ng-image-crop .zoom-handle > span { + color: rgba(0, 0, 0, 0.5); + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + transform: rotate(-45deg); + display: block; + position: relative; + top: 32px; +} \ No newline at end of file diff --git a/www/lib/ionic/js/angular/angular-image-crop.js b/www/lib/ionic/js/angular/angular-image-crop.js new file mode 100644 index 0000000000000000000000000000000000000000..960d0889db25c869a65bb9ba033e7544dc5557a8 --- /dev/null +++ b/www/lib/ionic/js/angular/angular-image-crop.js @@ -0,0 +1,1344 @@ +/** + * AngularJS Directive - Image Crop v1.1.0 + * Copyright (c) 2014 Andy Shora, andyshora@gmail.com, andyshora.com + * Licensed under the MPL License [http://www.nihilogic.dk/licenses/mpl-license.txt] + */ +(function() { + + /* + * DEPENDENCY + * Javascript BinaryFile + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * Licensed under the MPL License [http://www.nihilogic.dk/licenses/mpl-license.txt] + */ + + var BinaryFile = function(strData, iDataOffset, iDataLength) { + var data = strData; + var dataOffset = iDataOffset || 0; + var dataLength = 0; + + this.getRawData = function() { + return data; + } + + if (typeof strData == "string") { + dataLength = iDataLength || data.length; + + this.getByteAt = function(iOffset) { + return data.charCodeAt(iOffset + dataOffset) & 0xFF; + } + + this.getBytesAt = function(iOffset, iLength) { + var aBytes = []; + + for (var i = 0; i < iLength; i++) { + aBytes[i] = data.charCodeAt((iOffset + i) + dataOffset) & 0xFF + } + ; + + return aBytes; + } + } else if (typeof strData == "unknown") { + dataLength = iDataLength || IEBinary_getLength(data); + + this.getByteAt = function(iOffset) { + return IEBinary_getByteAt(data, iOffset + dataOffset); + } + + this.getBytesAt = function(iOffset, iLength) { + return new VBArray(IEBinary_getBytesAt(data, iOffset + dataOffset, iLength)).toArray(); + } + } + + this.getLength = function() { + return dataLength; + } + + this.getSByteAt = function(iOffset) { + var iByte = this.getByteAt(iOffset); + if (iByte > 127) + return iByte - 256; + else + return iByte; + } + + this.getShortAt = function(iOffset, bBigEndian) { + var iShort = bBigEndian ? + (this.getByteAt(iOffset) << 8) + this.getByteAt(iOffset + 1) + : (this.getByteAt(iOffset + 1) << 8) + this.getByteAt(iOffset) + if (iShort < 0) + iShort += 65536; + return iShort; + } + this.getSShortAt = function(iOffset, bBigEndian) { + var iUShort = this.getShortAt(iOffset, bBigEndian); + if (iUShort > 32767) + return iUShort - 65536; + else + return iUShort; + } + this.getLongAt = function(iOffset, bBigEndian) { + var iByte1 = this.getByteAt(iOffset), + iByte2 = this.getByteAt(iOffset + 1), + iByte3 = this.getByteAt(iOffset + 2), + iByte4 = this.getByteAt(iOffset + 3); + + var iLong = bBigEndian ? + (((((iByte1 << 8) + iByte2) << 8) + iByte3) << 8) + iByte4 + : (((((iByte4 << 8) + iByte3) << 8) + iByte2) << 8) + iByte1; + if (iLong < 0) + iLong += 4294967296; + return iLong; + } + this.getSLongAt = function(iOffset, bBigEndian) { + var iULong = this.getLongAt(iOffset, bBigEndian); + if (iULong > 2147483647) + return iULong - 4294967296; + else + return iULong; + } + + this.getStringAt = function(iOffset, iLength) { + var aStr = []; + + var aBytes = this.getBytesAt(iOffset, iLength); + for (var j = 0; j < iLength; j++) { + aStr[j] = String.fromCharCode(aBytes[j]); + } + return aStr.join(""); + } + + this.getCharAt = function(iOffset) { + return String.fromCharCode(this.getByteAt(iOffset)); + } + this.toBase64 = function() { + return window.btoa(data); + } + this.fromBase64 = function(strBase64) { + data = window.atob(strBase64); + } + }; + /* + * DEPENDENCY + * Javascript EXIF Reader 0.1.6 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * Licensed under the MPL License [http://www.nihilogic.dk/licenses/mpl-license.txt] + */ + var EXIF = (function() { + + var debug = false; + + var ExifTags = { + + // version tags + 0x9000: "ExifVersion", // EXIF version + 0xA000: "FlashpixVersion", // Flashpix format version + + // colorspace tags + 0xA001: "ColorSpace", // Color space information tag + + // image configuration + 0xA002: "PixelXDimension", // Valid width of meaningful image + 0xA003: "PixelYDimension", // Valid height of meaningful image + 0x9101: "ComponentsConfiguration", // Information about channels + 0x9102: "CompressedBitsPerPixel", // Compressed bits per pixel + + // user information + 0x927C: "MakerNote", // Any desired information written by the manufacturer + 0x9286: "UserComment", // Comments by user + + // related file + 0xA004: "RelatedSoundFile", // Name of related sound file + + // date and time + 0x9003: "DateTimeOriginal", // Date and time when the original image was generated + 0x9004: "DateTimeDigitized", // Date and time when the image was stored digitally + 0x9290: "SubsecTime", // Fractions of seconds for DateTime + 0x9291: "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal + 0x9292: "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829A: "ExposureTime", // Exposure time (in seconds) + 0x829D: "FNumber", // F number + 0x8822: "ExposureProgram", // Exposure program + 0x8824: "SpectralSensitivity", // Spectral sensitivity + 0x8827: "ISOSpeedRatings", // ISO speed rating + 0x8828: "OECF", // Optoelectric conversion factor + 0x9201: "ShutterSpeedValue", // Shutter speed + 0x9202: "ApertureValue", // Lens aperture + 0x9203: "BrightnessValue", // Value of brightness + 0x9204: "ExposureBias", // Exposure bias + 0x9205: "MaxApertureValue", // Smallest F number of lens + 0x9206: "SubjectDistance", // Distance to subject in meters + 0x9207: "MeteringMode", // Metering mode + 0x9208: "LightSource", // Kind of light source + 0x9209: "Flash", // Flash status + 0x9214: "SubjectArea", // Location and area of main subject + 0x920A: "FocalLength", // Focal length of the lens in mm + 0xA20B: "FlashEnergy", // Strobe energy in BCPS + 0xA20C: "SpatialFrequencyResponse", // + 0xA20E: "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xA20F: "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xA210: "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xA214: "SubjectLocation", // Location of subject in image + 0xA215: "ExposureIndex", // Exposure index selected on camera + 0xA217: "SensingMethod", // Image sensor type + 0xA300: "FileSource", // Image source (3 == DSC) + 0xA301: "SceneType", // Scene type (1 == directly photographed) + 0xA302: "CFAPattern", // Color filter array geometric pattern + 0xA401: "CustomRendered", // Special processing + 0xA402: "ExposureMode", // Exposure mode + 0xA403: "WhiteBalance", // 1 = auto white balance, 2 = manual + 0xA404: "DigitalZoomRation", // Digital zoom ratio + 0xA405: "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) + 0xA406: "SceneCaptureType", // Type of scene + 0xA407: "GainControl", // Degree of overall image gain adjustment + 0xA408: "Contrast", // Direction of contrast processing applied by camera + 0xA409: "Saturation", // Direction of saturation processing applied by camera + 0xA40A: "Sharpness", // Direction of sharpness processing applied by camera + 0xA40B: "DeviceSettingDescription", // + 0xA40C: "SubjectDistanceRange", // Distance to subject + + // other tags + 0xA005: "InteroperabilityIFDPointer", + 0xA420: "ImageUniqueID" // Identifier assigned uniquely to each image + }; + + var TiffTags = { + 0x0100: "ImageWidth", + 0x0101: "ImageHeight", + 0x8769: "ExifIFDPointer", + 0x8825: "GPSInfoIFDPointer", + 0xA005: "InteroperabilityIFDPointer", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x011C: "PlanarConfiguration", + 0x0212: "YCbCrSubSampling", + 0x0213: "YCbCrPositioning", + 0x011A: "XResolution", + 0x011B: "YResolution", + 0x0128: "ResolutionUnit", + 0x0111: "StripOffsets", + 0x0116: "RowsPerStrip", + 0x0117: "StripByteCounts", + 0x0201: "JPEGInterchangeFormat", + 0x0202: "JPEGInterchangeFormatLength", + 0x012D: "TransferFunction", + 0x013E: "WhitePoint", + 0x013F: "PrimaryChromaticities", + 0x0211: "YCbCrCoefficients", + 0x0214: "ReferenceBlackWhite", + 0x0132: "DateTime", + 0x010E: "ImageDescription", + 0x010F: "Make", + 0x0110: "Model", + 0x0131: "Software", + 0x013B: "Artist", + 0x8298: "Copyright" + }; + + var GPSTags = { + 0x0000: "GPSVersionID", + 0x0001: "GPSLatitudeRef", + 0x0002: "GPSLatitude", + 0x0003: "GPSLongitudeRef", + 0x0004: "GPSLongitude", + 0x0005: "GPSAltitudeRef", + 0x0006: "GPSAltitude", + 0x0007: "GPSTimeStamp", + 0x0008: "GPSSatellites", + 0x0009: "GPSStatus", + 0x000A: "GPSMeasureMode", + 0x000B: "GPSDOP", + 0x000C: "GPSSpeedRef", + 0x000D: "GPSSpeed", + 0x000E: "GPSTrackRef", + 0x000F: "GPSTrack", + 0x0010: "GPSImgDirectionRef", + 0x0011: "GPSImgDirection", + 0x0012: "GPSMapDatum", + 0x0013: "GPSDestLatitudeRef", + 0x0014: "GPSDestLatitude", + 0x0015: "GPSDestLongitudeRef", + 0x0016: "GPSDestLongitude", + 0x0017: "GPSDestBearingRef", + 0x0018: "GPSDestBearing", + 0x0019: "GPSDestDistanceRef", + 0x001A: "GPSDestDistance", + 0x001B: "GPSProcessingMethod", + 0x001C: "GPSAreaInformation", + 0x001D: "GPSDateStamp", + 0x001E: "GPSDifferential" + }; + + var StringValues = { + ExposureProgram: { + 0: "Not defined", + 1: "Manual", + 2: "Normal program", + 3: "Aperture priority", + 4: "Shutter priority", + 5: "Creative program", + 6: "Action program", + 7: "Portrait mode", + 8: "Landscape mode" + }, + MeteringMode: { + 0: "Unknown", + 1: "Average", + 2: "CenterWeightedAverage", + 3: "Spot", + 4: "MultiSpot", + 5: "Pattern", + 6: "Partial", + 255: "Other" + }, + LightSource: { + 0: "Unknown", + 1: "Daylight", + 2: "Fluorescent", + 3: "Tungsten (incandescent light)", + 4: "Flash", + 9: "Fine weather", + 10: "Cloudy weather", + 11: "Shade", + 12: "Daylight fluorescent (D 5700 - 7100K)", + 13: "Day white fluorescent (N 4600 - 5400K)", + 14: "Cool white fluorescent (W 3900 - 4500K)", + 15: "White fluorescent (WW 3200 - 3700K)", + 17: "Standard light A", + 18: "Standard light B", + 19: "Standard light C", + 20: "D55", + 21: "D65", + 22: "D75", + 23: "D50", + 24: "ISO studio tungsten", + 255: "Other" + }, + Flash: { + 0x0000: "Flash did not fire", + 0x0001: "Flash fired", + 0x0005: "Strobe return light not detected", + 0x0007: "Strobe return light detected", + 0x0009: "Flash fired, compulsory flash mode", + 0x000D: "Flash fired, compulsory flash mode, return light not detected", + 0x000F: "Flash fired, compulsory flash mode, return light detected", + 0x0010: "Flash did not fire, compulsory flash mode", + 0x0018: "Flash did not fire, auto mode", + 0x0019: "Flash fired, auto mode", + 0x001D: "Flash fired, auto mode, return light not detected", + 0x001F: "Flash fired, auto mode, return light detected", + 0x0020: "No flash function", + 0x0041: "Flash fired, red-eye reduction mode", + 0x0045: "Flash fired, red-eye reduction mode, return light not detected", + 0x0047: "Flash fired, red-eye reduction mode, return light detected", + 0x0049: "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004D: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004F: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059: "Flash fired, auto mode, red-eye reduction mode", + 0x005D: "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005F: "Flash fired, auto mode, return light detected, red-eye reduction mode" + }, + SensingMethod: { + 1: "Not defined", + 2: "One-chip color area sensor", + 3: "Two-chip color area sensor", + 4: "Three-chip color area sensor", + 5: "Color sequential area sensor", + 7: "Trilinear sensor", + 8: "Color sequential linear sensor" + }, + SceneCaptureType: { + 0: "Standard", + 1: "Landscape", + 2: "Portrait", + 3: "Night scene" + }, + SceneType: { + 1: "Directly photographed" + }, + CustomRendered: { + 0: "Normal process", + 1: "Custom process" + }, + WhiteBalance: { + 0: "Auto white balance", + 1: "Manual white balance" + }, + GainControl: { + 0: "None", + 1: "Low gain up", + 2: "High gain up", + 3: "Low gain down", + 4: "High gain down" + }, + Contrast: { + 0: "Normal", + 1: "Soft", + 2: "Hard" + }, + Saturation: { + 0: "Normal", + 1: "Low saturation", + 2: "High saturation" + }, + Sharpness: { + 0: "Normal", + 1: "Soft", + 2: "Hard" + }, + SubjectDistanceRange: { + 0: "Unknown", + 1: "Macro", + 2: "Close view", + 3: "Distant view" + }, + FileSource: { + 3: "DSC" + }, + Components: { + 0: "", + 1: "Y", + 2: "Cb", + 3: "Cr", + 4: "R", + 5: "G", + 6: "B" + } + }; + + function addEvent(element, event, handler) { + if (element.addEventListener) { + element.addEventListener(event, handler, false); + } else if (element.attachEvent) { + element.attachEvent("on" + event, handler); + } + } + + function imageHasData(img) { + return !!(img.exifdata); + } + + function getImageData(img, callback) { + BinaryAjax(img.src, function(http) { + var data = findEXIFinJPEG(http.binaryResponse); + img.exifdata = data || {}; + if (callback) { + callback.call(img) + } + }); + } + + function findEXIFinJPEG(file) { + if (file.getByteAt(0) != 0xFF || file.getByteAt(1) != 0xD8) { + return false; // not a valid jpeg + } + + var offset = 2, + length = file.getLength(), + marker; + + while (offset < length) { + if (file.getByteAt(offset) != 0xFF) { + if (debug) + console.log("Not a valid marker at offset " + offset + ", found: " + file.getByteAt(offset)); + return false; // not a valid marker, something is wrong + } + + marker = file.getByteAt(offset + 1); + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if (marker == 22400) { + if (debug) + console.log("Found 0xFFE1 marker"); + + return readEXIFData(file, offset + 4, file.getShortAt(offset + 2, true) - 2); + + // offset += 2 + file.getShortAt(offset+2, true); + + } else if (marker == 225) { + // 0xE1 = Application-specific 1 (for EXIF) + if (debug) + console.log("Found 0xFFE1 marker"); + + return readEXIFData(file, offset + 4, file.getShortAt(offset + 2, true) - 2); + + } else { + offset += 2 + file.getShortAt(offset + 2, true); + } + + } + + } + + + function readTags(file, tiffStart, dirStart, strings, bigEnd) { + var entries = file.getShortAt(dirStart, bigEnd), + tags = {}, + entryOffset, tag, + i; + + for (i = 0; i < entries; i++) { + entryOffset = dirStart + i * 12 + 2; + tag = strings[file.getShortAt(entryOffset, bigEnd)]; + if (!tag && debug) + console.log("Unknown tag: " + file.getShortAt(entryOffset, bigEnd)); + tags[tag] = readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd); + } + return tags; + } + + + function readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) { + var type = file.getShortAt(entryOffset + 2, bigEnd), + numValues = file.getLongAt(entryOffset + 4, bigEnd), + valueOffset = file.getLongAt(entryOffset + 8, bigEnd) + tiffStart, + offset, + vals, val, n, + numerator, denominator; + + switch (type) { + case 1: // byte, 8-bit unsigned int + case 7: // undefined, 8-bit byte, value depending on field + if (numValues == 1) { + return file.getByteAt(entryOffset + 8, bigEnd); + } else { + offset = numValues > 4 ? valueOffset : (entryOffset + 8); + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getByteAt(offset + n); + } + return vals; + } + + case 2: // ascii, 8-bit byte + offset = numValues > 4 ? valueOffset : (entryOffset + 8); + return file.getStringAt(offset, numValues - 1); + + case 3: // short, 16 bit int + if (numValues == 1) { + return file.getShortAt(entryOffset + 8, bigEnd); + } else { + offset = numValues > 2 ? valueOffset : (entryOffset + 8); + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getShortAt(offset + 2 * n, bigEnd); + } + return vals; + } + + case 4: // long, 32 bit int + if (numValues == 1) { + return file.getLongAt(entryOffset + 8, bigEnd); + } else { + vals = []; + for (var n = 0; n < numValues; n++) { + vals[n] = file.getLongAt(valueOffset + 4 * n, bigEnd); + } + return vals; + } + + case 5: // rational = two long values, first is numerator, second is denominator + if (numValues == 1) { + numerator = file.getLongAt(valueOffset, bigEnd); + denominator = file.getLongAt(valueOffset + 4, bigEnd); + val = new Number(numerator / denominator); + val.numerator = numerator; + val.denominator = denominator; + return val; + } else { + vals = []; + for (n = 0; n < numValues; n++) { + numerator = file.getLongAt(valueOffset + 8 * n, bigEnd); + denominator = file.getLongAt(valueOffset + 4 + 8 * n, bigEnd); + vals[n] = new Number(numerator / denominator); + vals[n].numerator = numerator; + vals[n].denominator = denominator; + } + return vals; + } + + case 9: // slong, 32 bit signed int + if (numValues == 1) { + return file.getSLongAt(entryOffset + 8, bigEnd); + } else { + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getSLongAt(valueOffset + 4 * n, bigEnd); + } + return vals; + } + + case 10: // signed rational, two slongs, first is numerator, second is denominator + if (numValues == 1) { + return file.getSLongAt(valueOffset, bigEnd) / file.getSLongAt(valueOffset + 4, bigEnd); + } else { + vals = []; + for (n = 0; n < numValues; n++) { + vals[n] = file.getSLongAt(valueOffset + 8 * n, bigEnd) / file.getSLongAt(valueOffset + 4 + 8 * n, bigEnd); + } + return vals; + } + } + } + + + function readEXIFData(file, start) { + if (file.getStringAt(start, 4) != "Exif") { + if (debug) + console.log("Not valid EXIF data! " + file.getStringAt(start, 4)); + return false; + } + + var bigEnd, + tags, tag, + exifData, gpsData, + tiffOffset = start + 6; + + // test for TIFF validity and endianness + if (file.getShortAt(tiffOffset) == 0x4949) { + bigEnd = false; + } else if (file.getShortAt(tiffOffset) == 0x4D4D) { + bigEnd = true; + } else { + if (debug) + console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); + return false; + } + + if (file.getShortAt(tiffOffset + 2, bigEnd) != 0x002A) { + if (debug) + console.log("Not valid TIFF data! (no 0x002A)"); + return false; + } + + if (file.getLongAt(tiffOffset + 4, bigEnd) != 0x00000008) { + if (debug) + console.log("Not valid TIFF data! (First offset not 8)", file.getShortAt(tiffOffset + 4, bigEnd)); + return false; + } + + tags = readTags(file, tiffOffset, tiffOffset + 8, TiffTags, bigEnd); + + if (tags.ExifIFDPointer) { + exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd); + for (tag in exifData) { + switch (tag) { + case "LightSource" : + case "Flash" : + case "MeteringMode" : + case "ExposureProgram" : + case "SensingMethod" : + case "SceneCaptureType" : + case "SceneType" : + case "CustomRendered" : + case "WhiteBalance" : + case "GainControl" : + case "Contrast" : + case "Saturation" : + case "Sharpness" : + case "SubjectDistanceRange" : + case "FileSource" : + exifData[tag] = StringValues[tag][exifData[tag]]; + break; + + case "ExifVersion" : + case "FlashpixVersion" : + exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]); + break; + + case "ComponentsConfiguration" : + exifData[tag] = + StringValues.Components[exifData[tag][0]] + + StringValues.Components[exifData[tag][1]] + + StringValues.Components[exifData[tag][2]] + + StringValues.Components[exifData[tag][3]]; + break; + } + tags[tag] = exifData[tag]; + } + } + + if (tags.GPSInfoIFDPointer) { + gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd); + for (tag in gpsData) { + switch (tag) { + case "GPSVersionID" : + gpsData[tag] = gpsData[tag][0] + + "." + gpsData[tag][1] + + "." + gpsData[tag][2] + + "." + gpsData[tag][3]; + break; + } + tags[tag] = gpsData[tag]; + } + } + + return tags; + } + + + function getData(img, callback) { + if (!img.complete) + return false; + if (!imageHasData(img)) { + getImageData(img, callback); + } else { + if (callback) { + callback.call(img); + } + } + return true; + } + + function getTag(img, tag) { + if (!imageHasData(img)) + return; + return img.exifdata[tag]; + } + + function getAllTags(img) { + if (!imageHasData(img)) + return {}; + var a, + data = img.exifdata, + tags = {}; + for (a in data) { + if (data.hasOwnProperty(a)) { + tags[a] = data[a]; + } + } + return tags; + } + + function pretty(img) { + if (!imageHasData(img)) + return ""; + var a, + data = img.exifdata, + strPretty = ""; + for (a in data) { + if (data.hasOwnProperty(a)) { + if (typeof data[a] == "object") { + if (data[a] instanceof Number) { + strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n"; + } else { + strPretty += a + " : [" + data[a].length + " values]\r\n"; + } + } else { + strPretty += a + " : " + data[a] + "\r\n"; + } + } + } + return strPretty; + } + + function readFromBinaryFile(file) { + return findEXIFinJPEG(file); + } + + + return { + readFromBinaryFile: readFromBinaryFile, + pretty: pretty, + getTag: getTag, + getAllTags: getAllTags, + getData: getData, + Tags: ExifTags, + TiffTags: TiffTags, + GPSTags: GPSTags, + StringValues: StringValues + }; + + })(); + + angular.module('ImageCropper',[]) + .directive('imageCrop', function() { + + return { + template: '<div id="image-crop-{{ rand }}" class="ng-image-crop ng-image-crop--{{ shape }}" ng-style="moduleStyles"><section ng-style="sectionStyles" ng-show="step==1"></section><section ng-style="sectionStyles" ng-show="step==2"><canvas class="cropping-canvas" width="{{ canvasWidth }}" height="{{ canvasHeight }}" ng-mousemove="onCanvasMouseMove($event)" ng-mousedown="onCanvasMouseDown($event)"></canvas><div ng-style="croppingGuideStyles" class="cropping-guide"></div><div class="zoom-handle" ng-mousemove="onHandleMouseMove($event)" ng-mousedown="onHandleMouseDown($event)" ng-mouseup="onHandleMouseUp($event)"><span>← zoom →</span></div></section><section ng-style="sectionStyles" class="image-crop-section-final" ng-show="step==3"><img class="image-crop-final" ng-src="{{ croppedDataUri }}" /></section></div>', + replace: true, + restrict: 'AE', + scope: { + crop: '=', + width: '@', + height: '@', + shape: '@', + src: '=', + resultBlob: '=', + result: '=', + step: '=', + padding: '@', + maxSize: '@' + }, + link: function (scope, element, attributes) { + + var padding = scope.padding ? Number(scope.padding) : 200; + + scope.rand = Math.round(Math.random() * 99999); + scope.step = scope.step || 1; + scope.shape = scope.shape || 'circle'; + scope.width = parseInt(scope.width, 10) || 300; + scope.height = parseInt(scope.height, 10) || 300; + + scope.canvasWidth = scope.width + padding; + scope.canvasHeight = scope.height + padding; + + var $elm = element[0]; + + var $canvas = $elm.getElementsByClassName('cropping-canvas')[0]; + var $handle = $elm.getElementsByClassName('zoom-handle')[0]; + var $finalImg = $elm.getElementsByClassName('image-crop-final')[0]; + var $img = new Image(); + var fileReader = new FileReader(); + + var maxLeft = 0, minLeft = 0, maxTop = 0, minTop = 0, imgLoaded = false, imgWidth = 0, imgHeight = 0; + var currentX = 0, currentY = 0, dragging = false, startX = 0, startY = 0, zooming = false; + var newWidth = imgWidth, newHeight = imgHeight; + var targetX = 0, targetY = 0; + var zoom = 1; + var maxZoomGestureLength = 0; + var maxZoomedInLevel = 0, maxZoomedOutLevel = 2; + var minXPos = 0, maxXPos = (padding/2), minYPos = 0, maxYPos = (padding/2); // for dragging bounds + var maxSize = scope.maxSize ? Number(scope.maxSize) : null; //max size of the image in px + + var zoomWeight = .6; + var ctx = $canvas.getContext('2d'); + var exif = null; + var files = []; + + // ---------- INLINE STYLES ----------- // + scope.moduleStyles = { + width: (scope.width + padding) + 'px', + height: (scope.height + padding) + 'px' + }; + + scope.sectionStyles = { + width: (scope.width + padding) + 'px', + height: (scope.height + padding) + 'px' + }; + + scope.croppingGuideStyles = { + width: scope.width + 'px', + height: scope.height + 'px', + top: (padding/2)+'px', + left: (padding/2)+'px' + }; + + function handleSize(base64ImageSrc) { + + return new Promise(function(resolve, reject) { + + if(!maxSize) { + return resolve(base64ImageSrc); + } + + var img = new Image(); + img.src = base64ImageSrc; + + img.onload = function() { + + var height = img.height; + var width = img.width; + + //if the size is already ok, just return the image + if(height <= maxSize && width <= maxSize) { + return resolve(base64ImageSrc); + } + + var ratio = width/height; + + if(ratio > 1) { + width = maxSize; + height = maxSize/ratio; + } + else { + width = maxSize*ratio; + height = maxSize; + } + + width = Math.round(width); + height = Math.round(height); + + var canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + var context = canvas.getContext("2d"); + + context.drawImage(img, 0, 0, img.width, img.height, // source + 0, 0, canvas.width, canvas.height); // destination + + context.save(); + + var dataUrl = canvas.toDataURL(); + + resolve(dataUrl); + + }; + + }); + + } + + function handleEXIF(base64ImageSrc, exif) { + + return new Promise(function(resolve, reject) { + + var img = new Image(); + img.src = base64ImageSrc; + + img.onload = function() { + + var canvas = document.createElement("canvas"); + + if(exif.Orientation >= 5) { + canvas.width = img.height; + canvas.height = img.width; + } else { + canvas.width = img.width; + canvas.height = img.height; + } + + var context = canvas.getContext("2d"); + + // change mobile orientation, if required + switch(exif.Orientation){ + case 1: + // nothing + break; + case 2: + // horizontal flip + context.translate(img.width, 0); + context.scale(-1, 1); + break; + case 3: + // 180 rotate left + context.translate(img.width, img.height); + context.rotate(Math.PI); + break; + case 4: + // vertical flip + context.translate(0, img.height); + context.scale(1, -1); + break; + case 5: + // vertical flip + 90 rotate right + context.rotate(0.5 * Math.PI); + context.scale(1, -1); + break; + case 6: + // 90 rotate right + context.rotate(0.5 * Math.PI); + context.translate(0, -img.height); + break; + case 7: + // horizontal flip + 90 rotate right + context.rotate(0.5 * Math.PI); + context.translate(img.width, -img.height); + context.scale(-1, 1); + break; + case 8: + // 90 rotate left + context.rotate(-0.5 * Math.PI); + context.translate(-img.width, 0); + break; + default: + break; + } + + context.drawImage(img, 0, 0); + context.save(); + + var dataUrl = canvas.toDataURL(); + + resolve(dataUrl); + + }; + + }); + + } + + function loadImage(base64ImageSrc) { + + //get the EXIF information from the image + var byteString = atob(base64ImageSrc.split(',')[1]); + var binary = new BinaryFile(byteString, 0, byteString.length); + exif = EXIF.readFromBinaryFile(binary); + + //handle image size + handleSize(base64ImageSrc).then(function(base64ImageSrc) { + + //if the image has EXIF orientation.. + if (exif && exif.Orientation && exif.Orientation > 1) { + return handleEXIF(base64ImageSrc, exif); + } + //otherwise, just return the image without any treatment + else { + return base64ImageSrc; + } + + }).then(function(base64ImageSrc) { + + $img.src = base64ImageSrc; + + }).catch(function(error) { + console.log(error); + }); + + }; + + // ---------- EVENT HANDLERS ---------- // + fileReader.onload = function(e) { + + loadImage(this.resultBlob); + + }; + + $img.onload = function() { + + scope.step = 2; + scope.$apply(); + + ctx.drawImage($img, 0, 0); + + imgWidth = $img.width; + imgHeight = $img.height; + + minLeft = (scope.width + padding) - this.width; + minTop = (scope.height + padding) - this.height; + newWidth = imgWidth; + newHeight = imgHeight; + + if(imgWidth >= imgHeight) { + maxZoomedInLevel = ($canvas.height - padding) / imgHeight; + } else { + maxZoomedInLevel = ($canvas.width - padding) / imgWidth; + } + + maxZoomGestureLength = to2Dp(Math.sqrt(Math.pow($canvas.width, 2) + Math.pow($canvas.height, 2))); + + updateDragBounds(); + + var initialX = Math.round((minXPos + maxXPos)/2); + var initialY = Math.round((minYPos + maxYPos)/2); + + moveImage(initialX, initialY); + + }; + + function reset() { + files = []; + zoom = 1; + currentX = 0; + currentY = 0; + dragging = false; + startX = 0; + startY = 0; + zooming = false; + ctx.clearRect(0, 0, $canvas.width, $canvas.height); + $img.src = ''; + } + + // ---------- PRIVATE FUNCTIONS ---------- // + function moveImage(x, y) { + + x = x < minXPos ? minXPos : x; + x = x > maxXPos ? maxXPos : x; + y = y < minYPos ? minYPos : y; + y = y > maxYPos ? maxYPos : y; + + targetX = x; + targetY = y; + + ctx.clearRect(0, 0, $canvas.width, $canvas.height); + ctx.drawImage($img, x, y, newWidth, newHeight); + + return x == minXPos || x == maxXPos || y == minYPos || y == maxYPos; + } + + function to2Dp(val) { + return Math.round(val * 1000) / 1000; + } + + function updateDragBounds() { + // $img.width, $canvas.width, zoom + + minXPos = $canvas.width - ($img.width * zoom) - (padding/2); + minYPos = $canvas.height - ($img.height * zoom) - (padding/2); + + } + + function zoomImage(val) { + + if (!val) { + return; + } + + var proposedZoomLevel = to2Dp(zoom + val); + + if ((proposedZoomLevel < maxZoomedInLevel) || (proposedZoomLevel > maxZoomedOutLevel)) { + // image wont fill whole canvas + // or image is too far zoomed in, it's gonna get pretty pixelated! + return; + } + + zoom = proposedZoomLevel; + // console.log('zoom', zoom); + + updateDragBounds(); + + newWidth = $img.width * zoom; + newHeight = $img.height * zoom; + + var newXPos = currentX * zoom; + var newYPos = currentY * zoom; + + // check if we've exposed the gutter + if (newXPos < minXPos) { + newXPos = minXPos; + } else if (newXPos > maxXPos) { + newXPos = maxXPos; + } + + if (newYPos < minYPos) { + newYPos = minYPos; + } else if (newYPos > maxYPos) { + newYPos = maxYPos; + } + + // check if image is still going to fit the bounds of the box + ctx.clearRect(0, 0, $canvas.width, $canvas.height); + ctx.drawImage($img, newXPos, newYPos, newWidth, newHeight); + } + + function calcZoomLevel(diffX, diffY) { + + var hyp = Math.sqrt( Math.pow(diffX, 2) + Math.pow(diffY, 2) ); + var zoomGestureRatio = to2Dp(hyp / maxZoomGestureLength); + var newZoomDiff = to2Dp((maxZoomedOutLevel - maxZoomedInLevel) * zoomGestureRatio * zoomWeight); + return diffX > 0 ? -newZoomDiff : newZoomDiff; + + } + + function dataURItoBlob(dataURI) { + var byteString, + mimestring; + + if(dataURI.split(',')[0].indexOf('base64') !== -1 ) { + byteString = atob(dataURI.split(',')[1]); + } else { + byteString = decodeURI(dataURI.split(',')[1]); + } + + mimestring = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + var content = new Array(); + for (var i = 0; i < byteString.length; i++) { + content[i] = byteString.charCodeAt(i); + } + + return new Blob([new Uint8Array(content)], {type: mimestring}); + } + + // ---------- SCOPE FUNCTIONS ---------- // + + scope.$watch('src', function(){ + if(scope.src) { + if(scope.step != 3) { + if(typeof(scope.src) == 'Blob') { + fileReader.readAsDataURL(scope.src); + } else { + loadImage(scope.src); + } + } + } else { + scope.step = 1; + reset(); + } + }); + + scope.$watch('crop',function(){ + if(scope.crop) { + scope.doCrop(); + scope.crop = false; + } + }); + + $finalImg.onload = function() { + var tempCanvas = document.createElement('canvas'); + tempCanvas.width = this.width - padding; + tempCanvas.height = this.height - padding; + tempCanvas.style.display = 'none'; + + var tempCanvasContext = tempCanvas.getContext('2d'); + tempCanvasContext.drawImage($finalImg, -(padding/2), -(padding/2)); + + $elm.getElementsByClassName('image-crop-section-final')[0].appendChild(tempCanvas); + + var dataUrl = tempCanvas.toDataURL(); + + scope.result = dataUrl; + scope.resultBlob = dataURItoBlob(dataUrl); + + scope.$apply(); + }; + + scope.doCrop = function() { + scope.croppedDataUri = $canvas.toDataURL(); + scope.step = 3; + }; + + scope.onCanvasMouseUp = function(e) { + + if (!dragging) { + return; + } + + e.preventDefault(); + e.stopPropagation(); // if event was on canvas, stop it propagating up + + startX = 0; + startY = 0; + dragging = false; + currentX = targetX; + currentY = targetY; + + removeBodyEventListener('mouseup', scope.onCanvasMouseUp); + removeBodyEventListener('touchend', scope.onCanvasMouseUp); + removeBodyEventListener('mousemove', scope.onCanvasMouseMove); + removeBodyEventListener('touchmove', scope.onCanvasMouseMove); + }; + + $canvas.addEventListener('touchend', scope.onCanvasMouseUp, false); + + scope.onCanvasMouseDown = function(e) { + startX = e.type === 'touchstart' ? e.changedTouches[0].clientX : e.clientX; + startY = e.type === 'touchstart' ? e.changedTouches[0].clientY : e.clientY; + zooming = false; + dragging = true; + + addBodyEventListener('mouseup', scope.onCanvasMouseUp); + addBodyEventListener('mousemove', scope.onCanvasMouseMove); + }; + + $canvas.addEventListener('touchstart', scope.onCanvasMouseDown, false); + + function addBodyEventListener(eventName, func) { + document.documentElement.addEventListener(eventName, func, false); + } + + function removeBodyEventListener(eventName, func) { + document.documentElement.removeEventListener(eventName, func); + } + + scope.onHandleMouseDown = function(e) { + + e.preventDefault(); + e.stopPropagation(); // if event was on handle, stop it propagating up + + startX = lastHandleX = (e.type === 'touchstart') ? e.changedTouches[0].clientX : e.clientX; + startY = lastHandleY = (e.type === 'touchstart') ? e.changedTouches[0].clientY : e.clientY; + dragging = false; + zooming = true; + + addBodyEventListener('mouseup', scope.onHandleMouseUp); + addBodyEventListener('touchend', scope.onHandleMouseUp); + addBodyEventListener('mousemove', scope.onHandleMouseMove); + addBodyEventListener('touchmove', scope.onHandleMouseMove); + + }; + + $handle.addEventListener('touchstart', scope.onHandleMouseDown, false); + + scope.onHandleMouseUp = function(e) { + + // this is applied on the whole section so check we're zooming + if (!zooming) { + return; + } + + e.preventDefault(); + e.stopPropagation(); // if event was on canvas, stop it propagating up + + startX = 0; + startY = 0; + zooming = false; + currentX = targetX; + currentY = targetY; + + removeBodyEventListener('mouseup', scope.onHandleMouseUp); + removeBodyEventListener('touchend', scope.onHandleMouseUp); + removeBodyEventListener('mousemove', scope.onHandleMouseMove); + removeBodyEventListener('touchmove', scope.onHandleMouseMove); + }; + + $handle.addEventListener('touchend', scope.onHandleMouseUp, false); + + scope.onCanvasMouseMove = function(e) { + + e.preventDefault(); + e.stopPropagation(); + + if (!dragging) { + return; + } + + var diffX = startX - ((e.type === 'touchmove') ? e.changedTouches[0].clientX : e.clientX); // how far mouse has moved in current drag + var diffY = startY - ((e.type === 'touchmove') ? e.changedTouches[0].clientY : e.clientY); // how far mouse has moved in current drag + /*targetX = currentX - diffX; // desired new X position + targetY = currentY - diffY; // desired new X position*/ + + moveImage(currentX - diffX, currentY - diffY); + + }; + + $canvas.addEventListener('touchmove', scope.onCanvasMouseMove, false); + + var lastHandleX = null, lastHandleY = null; + + scope.onHandleMouseMove = function(e) { + + e.stopPropagation(); + e.preventDefault(); + + // this is applied on the whole section so check we're zooming + if (!zooming) { + return false; + } + + var diffX = lastHandleX - ((e.type === 'touchmove') ? e.changedTouches[0].clientX : e.clientX); // how far mouse has moved in current drag + var diffY = lastHandleY - ((e.type === 'touchmove') ? e.changedTouches[0].clientY : e.clientY); // how far mouse has moved in current drag + + lastHandleX = (e.type === 'touchmove') ? e.changedTouches[0].clientX : e.clientX; + lastHandleY = (e.type === 'touchmove') ? e.changedTouches[0].clientY : e.clientY; + + var zoomVal = calcZoomLevel(diffX, diffY); + zoomImage(zoomVal); + + }; + + $handle.addEventListener('touchmove', scope.onHandleMouseMove, false); + + scope.onHandleMouseWheel = function(e){ + e.preventDefault(); + + zoomImage(e.deltaY > 0 ? -0.05 : 0.05); + }; + + $canvas.addEventListener('mousewheel', scope.onHandleMouseWheel); + $handle.addEventListener('mousewheel', scope.onHandleMouseWheel); + + } + }; + }); + + +})(); \ No newline at end of file diff --git a/www/plugins/es/i18n/locale-en-GB.json b/www/plugins/es/i18n/locale-en-GB.json index 42af33dd47fff5d9c06351198c053ad34b8657c7..c153ae0d7f1fe438bc7a6c99647e509e878c770b 100644 --- a/www/plugins/es/i18n/locale-en-GB.json +++ b/www/plugins/es/i18n/locale-en-GB.json @@ -272,6 +272,13 @@ "LOCATION_DIVIDER": "Localisation", "SOCIAL_NETWORKS_DIVIDER": "Social networks and web site", "TECHNICAL_DIVIDER": "Technical data", + "MODAL_AVATAR": { + "TITLE": "Avatar", + "SELECT_FILE_HELP": "<b>Choose an image file</b>, by clicking on the button below:", + "BTN_SELECT_FILE": "Choose an image", + "RESIZE_HELP": "<b>Re-crop the image</b> if necessary. A click on the image allows to move it. Click on the area at the bottom left to zoom in.", + "RESULT_HELP": "<b>Here is the result</b> as seen on your profile:" + }, "ERROR": { "LOAD_PROFILE_FAILED": "Could not load user profile.", "SAVE_PROFILE_FAILED": "Saving profile failed", diff --git a/www/plugins/es/i18n/locale-en.json b/www/plugins/es/i18n/locale-en.json index 42af33dd47fff5d9c06351198c053ad34b8657c7..c153ae0d7f1fe438bc7a6c99647e509e878c770b 100644 --- a/www/plugins/es/i18n/locale-en.json +++ b/www/plugins/es/i18n/locale-en.json @@ -272,6 +272,13 @@ "LOCATION_DIVIDER": "Localisation", "SOCIAL_NETWORKS_DIVIDER": "Social networks and web site", "TECHNICAL_DIVIDER": "Technical data", + "MODAL_AVATAR": { + "TITLE": "Avatar", + "SELECT_FILE_HELP": "<b>Choose an image file</b>, by clicking on the button below:", + "BTN_SELECT_FILE": "Choose an image", + "RESIZE_HELP": "<b>Re-crop the image</b> if necessary. A click on the image allows to move it. Click on the area at the bottom left to zoom in.", + "RESULT_HELP": "<b>Here is the result</b> as seen on your profile:" + }, "ERROR": { "LOAD_PROFILE_FAILED": "Could not load user profile.", "SAVE_PROFILE_FAILED": "Saving profile failed", diff --git a/www/plugins/es/i18n/locale-fr-FR.json b/www/plugins/es/i18n/locale-fr-FR.json index 79081a55b34f03e5e401216380226f265eb02f50..74a7011d81caf378337a560c8605901460e21389 100644 --- a/www/plugins/es/i18n/locale-fr-FR.json +++ b/www/plugins/es/i18n/locale-fr-FR.json @@ -272,6 +272,13 @@ "LOCATION_DIVIDER": "Adresse", "SOCIAL_NETWORKS_DIVIDER": "Réseaux sociaux, sites web", "TECHNICAL_DIVIDER": "Informations techniques", + "MODAL_AVATAR": { + "TITLE": "Photo de profil", + "SELECT_FILE_HELP": "Veuillez <b>choisir un fichier image</b>, en cliquant sur le bouton ci-dessous :", + "BTN_SELECT_FILE": "Choisir une photo", + "RESIZE_HELP": "<b>Recadrez l'image</b>, si besoin. Un clic maintenu sur l'image permet de la déplacer. Cliquez sur la zone en bas à gauche pour zoomer.", + "RESULT_HELP": "<b>Voici le résultat</b> tel que visible sur votre profil :" + }, "ERROR": { "LOAD_PROFILE_FAILED": "Erreur de chargement du profil utilisateur.", "SAVE_PROFILE_FAILED": "Erreur lors de la sauvegarde", diff --git a/www/plugins/es/js/controllers/common-controllers.js b/www/plugins/es/js/controllers/common-controllers.js index 9b0d6112ca6a5e4dc1ed6fe1262b608cc6ac5e20..71cd3aff9180c58392abfdaf0084d90309b3acf9 100644 --- a/www/plugins/es/js/controllers/common-controllers.js +++ b/www/plugins/es/js/controllers/common-controllers.js @@ -1,6 +1,6 @@ angular.module('cesium.es.common.controllers', ['ngResource', 'cesium.es.services']) - .controller('ESPicturesEditCtrl', ESPicturesEditController) + .controller('ESPicturesEditCtrl', ESPicturesEditController) .controller('ESPicturesEditCtrl', ESPicturesEditController) diff --git a/www/plugins/es/js/controllers/user-controllers.js b/www/plugins/es/js/controllers/user-controllers.js index 49dd2c76a93b33f9d459158f16ae0b9e01e92324..2054cda0559cbe52e2da56fdd750e03f2eff8254 100644 --- a/www/plugins/es/js/controllers/user-controllers.js +++ b/www/plugins/es/js/controllers/user-controllers.js @@ -17,10 +17,11 @@ angular.module('cesium.es.user.controllers', ['cesium.es.services']) .controller('ProfileCtrl', ProfileController) + .controller('AvatarModalCtrl', AvatarModalController) ; function ProfileController($scope, $rootScope, $timeout, $state, $focus, $translate, $ionicHistory, - esUser, SocialUtils, UIUtils, esHttp) { + esUser, SocialUtils, UIUtils, esHttp, ModalUtils, Device) { 'ngInject'; $scope.loading = true; @@ -52,7 +53,7 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl }); $scope.$on('$stateChangeStart', function (event, next, nextParams, fromState) { - /*if ($scope.dirty && !$scope.saving) { + if ($scope.dirty && !$scope.saving) { // stop the change state action event.preventDefault(); @@ -78,7 +79,7 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl $state.go(next.name, nextParams); }); } - }*/ + } }); $scope.load = function(walletData) { @@ -92,8 +93,6 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl $scope.existing = true; $scope.updateView(walletData, profile); } - UIUtils.loading.hide(); - $scope.loading = false; // removeIf(device) $focus('profile-name'); @@ -103,7 +102,6 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl UIUtils.loading.hide(10); if (err && err.ucode == 404) { $scope.updateView(walletData, {}); - $scope.loading = false; $scope.existing = false; } else { @@ -128,6 +126,13 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl // Set Ink UIUtils.ink({selector: 'ion-list > .item.ink'}); }, 10); + + // Update loading var + // Should be done with a delay, to avoid trigger onFormDataChanged() + UIUtils.loading.hide(); + $timeout(function() { + $scope.loading = false; + }, 1000); }; $scope.onFormDataChanged = function() { @@ -136,21 +141,8 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl }; $scope.$watch('formData', $scope.onFormDataChanged, true); - $scope.fileChanged = function(event) { - return UIUtils.loading.show() - .then(function() { - var file = event.target.files[0]; - return UIUtils.image.resizeFile(file, true); - }) - .then(function(imageData) { - $scope.avatar = {src: imageData}; - $scope.dirty = true; - return UIUtils.loading.hide(10); - }) - .catch(UIUtils.onError('PROFILE.ERROR.IMAGE_RESIZE_FAILED')); - }; - $scope.save = function(silent) { + console.debug('saving'); if(!$scope.form.$valid || !$rootScope.walletData) { return; } @@ -173,6 +165,7 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl else { delete $scope.walletData.avatar; } + $scope.walletData.profile = formData; } }; @@ -248,12 +241,78 @@ function ProfileController($scope, $rootScope, $timeout, $state, $focus, $transl }; $scope.close = function() { - if ($ionicHistory.backView()) { - return $ionicHistory.goBack(); + return $state.go('app.view_wallet'); + }; + + $scope.showAvatarModal = function() { + if (Device.enable) { + return Device.camera.getPicture() + .then(function(imageData) { + $scope.avatar = {src: "data:image/png;base64," + imageData}; + $scope.dirty = true; + }) + .catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED')); } else { - return $scope.showHome(); + return ModalUtils.show('plugins/es/templates/user/modal_edit_avatar.html','AvatarModalCtrl', + {}) + .then(function(imageData) { + if (!imageData) return; + $scope.avatar = {src: imageData}; + $scope.dirty = true; + }); } }; } + +function AvatarModalController($scope) { + + $scope.openFileSelector = function() { + var fileInput = angular.element(document.querySelector('.modal-avatar #fileInput')); + if (fileInput && fileInput.length > 0) { + fileInput[0].click(); + } + }; + + $scope.fileChanged = function(e) { + + var files = e.target.files; + + var fileReader = new FileReader(); + fileReader.readAsDataURL(files[0]); + + fileReader.onload = function(e) { + $scope.imgSrc = this.result; + $scope.$apply(); + }; + + }; + + + /*$scope.fileChanged = function(event) { + return UIUtils.loading.show() + .then(function() { + var file = event.target.files[0]; + return UIUtils.image.resizeFile(file, true); + }) + .then(function(imageData) { + $scope.avatar = {src: imageData}; + $scope.dirty = true; + return UIUtils.loading.hide(10); + }) + .catch(UIUtils.onError('PROFILE.ERROR.IMAGE_RESIZE_FAILED')); + };*/ + + $scope.doCrop = function() { + $scope.initCrop = true; + }; + + $scope.clear = function() { + $scope.imageCropStep = 1; + delete $scope.imgSrc; + delete $scope.result; + delete $scope.resultBlob; + }; + +} diff --git a/www/plugins/es/js/services/http-services.js b/www/plugins/es/js/services/http-services.js index 43f61f71153774cafe19358d6440608ba3946693..c32d58f5949cafaa56a28c3e9972469a38c5d467 100644 --- a/www/plugins/es/js/services/http-services.js +++ b/www/plugins/es/js/services/http-services.js @@ -229,7 +229,6 @@ angular.module('cesium.es.http.services', ['ngResource', 'cesium.services', 'ces } that = { - copy: copy, get: get, post: post, getUrl : csHttp.getUrl, diff --git a/www/plugins/es/js/services/message-services.js b/www/plugins/es/js/services/message-services.js index db8a43bbcd0095d36835a1c7a48c708d85af35a9..819037935d54e7efec99691fd98e9ae8c1725bec 100644 --- a/www/plugins/es/js/services/message-services.js +++ b/www/plugins/es/js/services/message-services.js @@ -336,7 +336,8 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.services', ' message.title = title; }) .catch(function(err){ - console.warn('[ES] [message] invalid cypher title'); + console.error(err); + console.warn('[ES] [message] may have invalid cypher title'); message.valid = false; }), @@ -346,7 +347,8 @@ angular.module('cesium.es.message.services', ['ngResource', 'cesium.services', ' message.content = content; }) .catch(function(err){ - console.warn('[ES] [message] invalid cypher content'); + console.error(err); + console.warn('[ES] [message] may have invalid cypher content'); message.valid = false; }) ); diff --git a/www/plugins/es/js/services/user-services.js b/www/plugins/es/js/services/user-services.js index 7939c0a68c4d9dba795e4002e0fcf98026251646..183fb4f777bc1efc610153a0c1b0715762d30d65 100644 --- a/www/plugins/es/js/services/user-services.js +++ b/www/plugins/es/js/services/user-services.js @@ -204,6 +204,7 @@ angular.module('cesium.es.user.services', ['cesium.services', 'cesium.es.http.se // Waiting to load crypto libs if (!CryptoUtils.isLoaded()) { console.debug('[ES] [user] Waiting crypto lib loading...'); + //throw 'stop'; $timeout(function() { onWalletLogin(data, deferred); }, 50); diff --git a/www/plugins/es/templates/user/edit_profile.html b/www/plugins/es/templates/user/edit_profile.html index e2ccca819ecbcfb47ed9abd15d50eff3463186e6..d8ad9dede6e7b9f30b17f9e87ca7f9cbd9655e88 100644 --- a/www/plugins/es/templates/user/edit_profile.html +++ b/www/plugins/es/templates/user/edit_profile.html @@ -15,9 +15,11 @@ <i class="avatar" style="background-image: url('{{avatar.src}}')" ng-class="{'avatar-wallet': !loading && !avatar && walletData && !walletData.isMember, 'avatar-member': !loading && !avatar && walletData.isMember}"> - <button class="button button-positive button-avatar button-large button-clear flat icon ion-camera hidden-no-device" ng-click="openPicturePopup()"></button> - <button class="button button-positive button-avatar button-large button-clear flat icon ion-camera hidden-device" - onclick="angular.element(document.querySelector('form #avatarFile'))[0].click();"></button> + <button class="button button-positive button-large button-clear flat icon ion-camera visible-xs visible-sm" + style="display: inline-block;" + ng-click="showAvatarModal()"></button> + <button class="button button-positive button-large button-clear icon ion-camera hidden-xs hidden-sm" + ng-click="showAvatarModal()"></button> </i> <div ng-if="!loading"> <h3 class="light" ng-if="!formData.title && walletData && walletData.isMember">{{walletData.uid}}</h3> @@ -41,9 +43,6 @@ <div class="col"> <form name="profileForm" novalidate="" ng-submit="saveAndClose()"> - <input type="file" id="avatarFile" accept=".png,.jpeg,.jpg" onchange="angular.element(this).scope().fileChanged(event)" - style="visibility:hidden; position:absolute;"/> - <ion-list class="animate-ripple item-text-wrap" ng-init="setForm(profileForm)"> <!-- Public info --> @@ -134,7 +133,9 @@ <button class="button button-clear button-dark ink" ng-click="cancel()" type="button" translate>COMMON.BTN_CANCEL </button> - <button class="button button-assertive ink" type="submit"> + <button class="button button-calm ink" + ng-class="{'button-assertive': dirty}" + type="submit"> {{'COMMON.BTN_SAVE' | translate}} </button> </div> diff --git a/www/plugins/es/templates/user/modal_edit_avatar.html b/www/plugins/es/templates/user/modal_edit_avatar.html new file mode 100644 index 0000000000000000000000000000000000000000..611fe591e1a22382d83e28400240475bcd843802 --- /dev/null +++ b/www/plugins/es/templates/user/modal_edit_avatar.html @@ -0,0 +1,87 @@ +<ion-modal-view> + <ion-header-bar class="bar-positive"> + <button class="button button-clear visible-xs visible-sm" ng-click="closeModal()" translate>COMMON.BTN_CANCEL</button> + <h1 class="title" translate>PROFILE.MODAL_AVATAR.TITLE</h1> + <button class="button button-icon button-clear ion-android-done visible-xs visible-sm" ng-click="closeModal(result)"> + </button> + </ion-header-bar> + + <ion-content class="modal-avatar padding"> + + + <div ng-show="imageCropStep == 1"> + <p translate>PROFILE.MODAL_AVATAR.SELECT_FILE_HELP</p> + + <!-- Add picture button --> + <div class="item card text-center padding ink" + ng-click="openFileSelector()"> + <i class="ion-image stable" style="font-size:150px"></i> + <b class="ion-plus gray" style="position:relative; font-size:80px; top:-51px; right: 19px;"></b> + <p translate>PROFILE.MODAL_AVATAR.BTN_SELECT_FILE</p> + </div> + + <input type="file" name="fileInput" accept=".png,.jpeg,.jpg" id="fileInput" onchange="angular.element(this).scope().fileChanged(event)" + accept=".png,.jpeg,.jpg" + style="visibility:hidden; position:absolute;"/> + </div> + + <div ng-show="imageCropStep == 2"> + <p translate>PROFILE.MODAL_AVATAR.RESIZE_HELP</p> + + <!-- <image-crop + data-height="200" //shape's height + data-width="150" //shape's width + data-shape="square" //the shape.. square or circle + data-step="imageCropStep"//scope variable that will contain the current step of the crop (1. Waiting for source image; 2. Image loaded, waiting for crop; 3. Crop done) + src="imgSrc" //scope variable that will be the source image for the crop (may be a Blob or base64 string) + data-result-blob="result" //scope variable that will contain the Blob information + data-result="resultDataUrl" //scope variable that will contain the image's base64 string representation + crop="initCrop" //scope variable that must be set to true when the image is ready to be cropped + padding="250" //space, in pixels, rounding the shape + max-size="1024" //max of the image, in pixels + ></image-crop> --> + + <div class="item card text-center padding ink"> + <image-crop + data-height="150" + data-width="150" + data-shape="circle" + data-step="imageCropStep" + src="imgSrc" + data-result="result" + data-result-blob="resultBlob" + crop="initCrop" + padding="150" + max-size="1024" + ></image-crop> + </div> + </div> + + <div ng-show="imageCropStep == 3"> + <p translate>PROFILE.MODAL_AVATAR.RESULT_HELP</p> + + <div class="item card padding hero" style="height: 110px;"> + <div class="content"> + <img class="avatar" ng-src="{{result}}" style="height: 88px; width: 88px;"> + </div> + </div> + </div> + + <!-- buttons bar --> + <div class="padding hidden-xs text-right"> + <button class="button button-clear button-dark ink" ng-click="closeModal()" type="button" translate> + COMMON.BTN_CANCEL + </button> + <button class="button button-calm icon-right ion-chevron-right ink" ng-click="doCrop()" translate + ng-disabled="imageCropStep == 1" + ng-if="imageCropStep <= 2"> + COMMON.BTN_NEXT + </button> + <button class="button button-positive ink" ng-click="closeModal(result)" translate + ng-if="imageCropStep == 3"> + COMMON.BTN_CONTINUE + </button> + </div> + + </ion-content> +</ion-modal-view> diff --git a/www/templates/home/home.html b/www/templates/home/home.html index 96e8526434de71f54ecbcd82d47ec3f94d8afe52..e85e4e1f99e780b615e5db04b1137fa2f1f96cac 100644 --- a/www/templates/home/home.html +++ b/www/templates/home/home.html @@ -33,9 +33,14 @@ <button type="button" class="button button-block button-positive button-raised icon icon-left ion-locked ink-dark" ng-click="loginAndGo('app.view_wallet')" ng-show="!login" translate>COMMON.BTN_LOGIN</button> + + <button type="button" + class="button button-block button-positive button-raised icon icon-left ion-person ink-dark" + ui-sref="app.view_wallet" ng-show="login" translate>MENU.ACCOUNT</button> + <button type="button" - class="button button-block button-positive button-raised icon icon-left ion-card ink-dark" - ui-sref="app.view_wallet" ng-show="login" translate>HOME.BTN_ACCOUNT</button> + class="button button-block button-positive button-raised icon icon-left ion-card ink-dark visible-xs" + ui-sref="app.view_wallet_tx" ng-show="login" translate>MENU.TRANSACTIONS</button> <br class="visible-xs visible-sm"/> <div class="text-center no-padding "