diff --git a/www/index.html b/www/index.html index eb0d15a85c8a49e7e65cfd2ec1d6fa6a6cc2a096..f5f7fd41cc568e1f0fd31f0757be49b0d77b5571 100644 --- a/www/index.html +++ b/www/index.html @@ -34,6 +34,7 @@ <script src="js/services/utils-services.js"></script> <script src="js/services/wallet-services.js"></script> <script src="js/services/bma-services.js"></script> + <script src="js/services/record-services.js"></script> <!-- entities --> <script src="js/entity/peer.js"></script> @@ -44,6 +45,7 @@ <script src="js/controllers/peer-controllers.js"></script> <script src="js/controllers/currency-controllers.js"></script> <script src="js/controllers/wallet-controllers.js"></script> + <script src="js/controllers/record-controllers.js"></script> <!-- App --> <script src="js/app.js"></script> diff --git a/www/js/controllers.js b/www/js/controllers.js index 0d217aaafe891f05ccaee098e1a8cec11a7da1bb..6fc900c15ef3ab26c018863cc37afdf2f69096e1 100644 --- a/www/js/controllers.js +++ b/www/js/controllers.js @@ -3,7 +3,8 @@ angular.module('cesium.controllers', [ 'cesium.home.controllers', 'cesium.wallet.controllers', 'cesium.currency.controllers', - 'cesium.wot.controllers' + 'cesium.wot.controllers', + 'cesium.record.controllers' ]) .config(function($httpProvider) { diff --git a/www/js/controllers/record-controllers.js b/www/js/controllers/record-controllers.js new file mode 100644 index 0000000000000000000000000000000000000000..b6c51fad5bd0d46106724f91eb927f55f59a5d04 --- /dev/null +++ b/www/js/controllers/record-controllers.js @@ -0,0 +1,371 @@ +angular.module('cesium.record.controllers', ['cesium.services']) + + .config(function($stateProvider, $urlRouterProvider) { + $stateProvider + + .state('app.lookup_record', { + url: "/record", + views: { + 'menuContent': { + templateUrl: "templates/record/lookup.html", + controller: 'RecordLookupCtrl' + } + } + }) + + .state('app.view_record', { + url: "/record/:id", + views: { + 'menuContent': { + templateUrl: "templates/record/view_record.html", + controller: 'RecordCtrl' + } + } + }) + + .state('app.add_record', { + url: "/record/add", + views: { + 'menuContent': { + templateUrl: "templates/record/edit_record.html", + controller: 'RecordEditCtrl' + } + } + }) + + .state('app.edit_record', { + url: "/record/:id/edit", + views: { + 'menuContent': { + templateUrl: "templates/record/edit_record.html", + controller: 'RecordEditCtrl' + } + } + }); + }) + + .controller('RecordLookupCtrl', RecordLookupController) + + .controller('RecordCtrl', RecordController) + + .controller('RecordEditCtrl', RecordEditController) + +; + +function RecordLookupController($scope, Record, $state) { + + $scope.queryData = {}; + $scope.search = { text: '', results: {} }; + + function createQuery() { + return { + query: { + match: { + title: $scope.search.text/*, + description: $scope.search.text*/ + } + }, + highlight: { + fields : { + title : {}, + description : {} + } + }, + from: 0, + size: 20, + _source: ["title", "time", "description", "pictures"] + } + } + + $scope.searchChanged = function() { + $scope.search.text = $scope.search.text.toLowerCase(); + if ($scope.search.text.length > 1) { + $scope.search.looking = true; + $scope.queryData = createQuery(); + return Record.record.search($scope.queryData) + .then(function(res){ + $scope.search.looking = false; + if (res.hits.total == 0) { + $scope.search.results = []; + } + else { + $scope.search.results = res.hits.hits.reduce(function(result, hit) { + var record = hit._source; + record.id = hit._id; + record.type = hit._type; + if (hit.highlight.title) { + record.title = hit.highlight.title[0]; + } + if (hit.highlight.description) { + record.description = hit.highlight.description[0]; + } + return result.concat(record); + }, []); + } + }) + .catch(function(err) { + $scope.search.looking = false; + $scope.search.results = []; + }); + } + else { + $scope.search.results = []; + } + }; + + $scope.select = function(id) { + $state.go('app.view_record', {id: id}); + }; +} + +function RecordController($scope, $ionicModal, Wallet, Record, UIUtils, $state, CryptoUtils, $q) { + + $scope.formData = {}; + $scope.id = null; + $scope.isMember = false; + $scope.category = {}; + $scope.pictures = []; + + $scope.$on('$ionicView.enter', function(e, $state) { + if ($state.stateParams && $state.stateParams.id) { // Load by id + $scope.load($state.stateParams.id); + } + else { + $state.go('app.lookup_record'); + } + }); + + $scope.load = function(id) { + UIUtils.loading.show(); + $q.all([ + Record.record.category.all() + .then(function(categories) { + Record.record.get({id: id}) + .then(function (hit) { + $scope.formData = hit._source; + $scope.category = categories[hit._source.category]; + $scope.id= hit._id; + if (hit._source.pictures) { + $scope.pictures = hit._source.pictures.reduce(function(res, pic) { + return res.concat({src: pic.src}); + }, []); + }/* + if (hit._source.pictures) { + hit._source.pictures.forEach(function(pic) { + $scope.pictures.concat({src: pic.src}); + }); + }*/ + UIUtils.loading.hide(); + }) + }) + ]).catch(UIUtils.onError('Could not load record')); + }; + + $scope.edit = function() { + $state.go('app.edit_record', {id: $scope.id}); + }; +} + +function RecordEditController($scope, $ionicModal, Wallet, Record, UIUtils, $state, CryptoUtils, $q, $ionicPopup) { + + $scope.walletData = {}; + $scope.formData = {}; + $scope.id = null; + $scope.isMember = false; + $scope.category = {}; + $scope.pictures = []; + + ionic.Platform.ready(function() { + if (!navigator.camera) { + delete $scope.camera; return; + } + $scope.camera = navigator.camera; + }); + + $scope.$on('$ionicView.enter', function(e, $state) { + $scope.loadWallet() + .then(function(walletData) { + $scope.walletData = walletData; + if ($state.stateParams && $state.stateParams.id) { // Load by id + $scope.load($state.stateParams.id); + } + }); + }); + + $scope.load = function(id) { + UIUtils.loading.show(); + $q.all([ + Record.record.category.all() + .then(function(categories) { + Record.record.get({id: id}) + .then(function (hit) { + $scope.formData = hit._source; + $scope.category = categories[hit._source.category]; + $scope.id= hit._id; + if (hit._source.pictures) { + $scope.pictures = hit._source.pictures.reduce(function(res, pic) { + return res.concat({src: pic.src}); + }, []); + } + UIUtils.loading.hide(); + }); + }) + ]) + .catch(UIUtils.onError('Could not load record')) + }; + + $scope.save = function() { + UIUtils.loading.show(); + return $q(function(resolve, reject) { + $scope.formData.pictures = $scope.pictures.reduce(function(res, pic) { + return res.concat({src: pic.src}); + }, []); + if (!$scope.id) { // Create + $scope.formData.issuer = $scope.walletData.pubkey; + Record.record.add($scope.formData) + .then(function(id) { + UIUtils.loading.hide(); + $state.go('app.view_record', {id: id}) + resolve(); + }) + .catch(UIUtils.onError('Could not save record')); + } + else { // Update + Record.record.update($scope.formData, {id: $scope.id}) + .then(function() { + UIUtils.loading.hide(); + $state.go('app.view_record', {id: $scope.id}) + resolve(); + }) + .catch(UIUtils.onError('Could not update record')); + } + }); + }; + + // category lookup modal + $ionicModal.fromTemplateUrl('templates/record/modal_category.html', { + scope: $scope, + focusFirstInput: true + }).then(function(modal) { + $scope.lookupModal = modal; + $scope.lookupModal.hide(); + }); + + $scope.openCategoryModal = function() { + // load categories + Record.record.category.all() + .then(function(categories){ + $scope.categories = categories; + $scope.lookupModal.show(); + }); + }; + + $scope.closeCategoryModal = function() { + $scope.lookupModal.hide(); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + $scope.category = cat; + $scope.formData.category = cat.id; + $scope.closeCategoryModal(); + }; + + $scope.openPicturePopup = function() { + $ionicPopup.show({ + title: 'Choose picture source :', + buttons: [ + { + text: 'Gallery', + type: 'button', + onTap: function(e) { + return navigator.camera.PictureSourceType.PHOTOLIBRARY; + } + }, + { + text: '<b>Camera</b>', + type: 'button button-positive', + onTap: function(e) { + return navigator.camera.PictureSourceType.CAMERA; + } + } + ] + }) + .then(function(sourceType){ + $scope.getPicture(sourceType); + }); + }; + + $scope.getPicture = function(sourceType) { + var options = { + quality: 50, + destinationType: navigator.camera.DestinationType.DATA_URL, + sourceType: sourceType, + encodingType: navigator.camera.EncodingType.PNG, + targetWidth : 400, + targetHeight : 400 + } + $scope.camera.getPicture( + function (imageData) { + $scope.pictures.push({src: "data:image/png;base64," + imageData}); + $scope.$apply(); + }, + UIUtils.onError('Could not get picture'), + options); + }; + + $scope.fileChanged = function(event) { + UIUtils.loading.show(); + return $q(function(resolve, reject) { + var file = event.target.files[0]; + var reader = new FileReader(); + + reader.addEventListener("load", function () { + console.log(reader.result); + $scope.pictures.push({src: reader.result}); + $scope.$apply(); + }, false); + + if (file) { + reader.readAsDataURL(file); + } + UIUtils.loading.hide(); + resolve(); + }); + }; + + /* + // See doc : + // http://stackoverflow.com/questions/20958078/resize-base64-image-in-javascript-without-using-canvas + $scope.imageToDataUri function(img, width, height) { + + // create an off-screen canvas + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'); + + // set its dimension to target size + canvas.width = width; + canvas.height = height; + + // draw source image into the off-screen canvas: + ctx.drawImage(img, 0, 0, width, height); + + // encode image to data-uri with base64 version of compressed image + return canvas.toDataURL(); + }*/ + + $scope.auth = function() { + $scope.loadWallet() + .then(function(walletData) { + UIUtils.loading.show(); + $scope.walletData = walletData; + Record.auth.token(walletData.keypair) + .then(function(token) { + UIUtils.loading.hide(); + console.log('authentication token is:' + token); + }) + .catch(onError('Could not computed authentication token')); + }) + .catch(onError('Could not computed authentication token')); + }; +} \ No newline at end of file diff --git a/www/js/services.js b/www/js/services.js index 44821043a3a0394403c3405523ae03c7e11181ed..f899053048f37b6ff980a907b21a3c83102690c3 100644 --- a/www/js/services.js +++ b/www/js/services.js @@ -2,5 +2,6 @@ angular.module('cesium.services', [ 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services', - 'cesium.wallet.services']) + 'cesium.wallet.services', + 'cesium.record.services']) ; diff --git a/www/js/services/record-services.js b/www/js/services/record-services.js new file mode 100644 index 0000000000000000000000000000000000000000..7b7f4cca21fbf555db95e3541ef4d0c33627785e --- /dev/null +++ b/www/js/services/record-services.js @@ -0,0 +1,188 @@ +angular.module('cesium.record.services', ['ngResource', 'cesium.services']) + +.factory('Record', function($http, $q, CryptoUtils) { + + function Record(server, wsServer) { + + var categories = []; + + if (wsServer == "undefined" || wsServer == null) { + wsServer = server; + } + + function processError(reject, data) { + if (data != null && data.message != "undefined" && data.message != null) { + reject(data.ucode + ": " + data.message); + } + else { + reject('Unknown error from ucoin node'); + } + } + + function prepare(uri, params, config, callback) { + var pkeys = [], queryParams = {}, newUri = uri; + if (typeof params == 'object') { + pkeys = _.keys(params); + } + + pkeys.forEach(function(pkey){ + var prevURI = newUri; + newUri = newUri.replace(new RegExp(':' + pkey), params[pkey]); + if (prevURI == newUri) { + queryParams[pkey] = params[pkey]; + } + }); + config.params = queryParams; + callback(newUri, config); + } + + function getResource(uri) { + return function(params) { + return $q(function(resolve, reject) { + var config = { + timeout: 4000 + }; + + prepare(uri, params, config, function(uri, config) { + $http.get(uri, config) + .success(function(data, status, headers, config) { + resolve(data); + }) + .error(function(data, status, headers, config) { + processError(reject, data); + }); + }); + }); + } + } + + function postResource(uri) { + return function(data, params) { + return $q(function(resolve, reject) { + var config = { + timeout: 4000, + headers : {'Content-Type' : 'application/json'} + }; + + prepare(uri, params, config, function(uri, config) { + $http.post(uri, data, config) + .success(function(data, status, headers, config) { + resolve(data); + }) + .error(function(data, status, headers, config) { + processError(reject, data); + }); + }); + }); + } + } + + function ws(uri) { + var sock = new WebSocket(uri); + return { + on: function(type, callback) { + sock.onmessage = function(e) { + callback(JSON.parse(e.data)); + }; + } + }; + } + + function getCategories() { + return $q(function(resolve, reject) { + if (categories.length != 0) { + resolve(categories); + return; + } + + getResource('http://' + server + '/store/category/_search?pretty&from=0&size=1000')() + .then(function(res) { + if (res.hits.total == 0) { + categories = []; + } + else { + categories = res.hits.hits.reduce(function(result, hit) { + var cat = hit._source; + cat.id = hit._id; + return result.concat(cat); + }, []); + // add as map also + categories.forEach(function(cat) { + categories[cat.id] = cat; + }); + } + resolve(categories); + }) + .catch(function(err) { + reject(err); + }); + }); + } + + function getToken(keypair) { + return $q(function(resolve, reject) { + var errorFct = function(err) { + reject(err); + } + var getChallenge = getResource('http://' + server + '/auth'); + var postAuth = postResource('http://' + server + '/auth'); + + getChallenge() // get the challenge phrase to sign + .then(function(challenge) { + CryptoUtils.sign(challenge, keypair) // sign the challenge + .then(function(signature) { + postAuth({ + pubkey: CryptoUtils.util.encode_base58(keypair.signPk), + challenge: challenge, + signature: signature + }) // get token + .then(function(token) { + resolve(token) + }) + .catch(errorFct); + }) + .catch(errorFct); + }) + .catch(errorFct); + }); + } + + function emptyHit() { + return { + _id: null, + _index: null, + _type: null, + _version: null, + _source: {} + } + } + + return { + auth: { + get: getResource('http://' + server + '/auth'), + post: postResource('http://' + server + '/auth'), + token: getToken + }, + hit: { + empty: emptyHit + }, + record: { + get: getResource('http://' + server + '/store/record/:id'), + add: postResource('http://' + server + '/store/record'), + update: postResource('http://' + server + '/store/record/:id'), + searchText: getResource('http://' + server + '/store/record/_search?q=:search'), + search: postResource('http://' + server + '/store/record/_search?pretty'), + category: { + all: getCategories + } + } + } + } + + var service = Record('localhost:9200'); + //var service = ES('metab.ucoin.fr:9288'); + + service.instance = Record; + return service; +}) +; diff --git a/www/templates/home.html b/www/templates/home.html index ad1ab233b739a218d832e6ddb6a4d7c40672e008..b9eadce031b4bdd7e554b8757bab89c4aed6752f 100644 --- a/www/templates/home.html +++ b/www/templates/home.html @@ -15,6 +15,9 @@ <a ui-sref="app.explore_currency" class="button button-block button-stable icon icon-left ion-search">Explore a currency</a> + <a ui-sref="app.lookup_record" class="button button-block button-stable icon icon-left ion-search">Explore records</a> + + <button ng-click="login()" ng-show="!isLogged()" class="button button-block button-positive icon icon-left ion-log-in">Login</button> <!-- <button ng-click="addAccount()" ng-show="!isLogged()" class="button button-block button-assertive icon icon-left ion-ios-color-wand">Add an account</button> --> diff --git a/www/templates/record/edit_record.html b/www/templates/record/edit_record.html new file mode 100644 index 0000000000000000000000000000000000000000..1a6e3c47bd24688fa124b6a6541e0f9ea962c66e --- /dev/null +++ b/www/templates/record/edit_record.html @@ -0,0 +1,81 @@ +<ion-view view-title="Product" left-buttons="leftButtons"> + <ion-nav-buttons side="secondary"> + <button class="button button-positive" ng-click="save()"> + <i class="icon ion-android-send" ng-if="!id"></i> + <i class="icon ion-android-done" ng-if="id"></i> + </button> + </ion-nav-buttons> + + <ion-content> + <div class="scroll"> + <div class="list"> + + <ion-gallery ion-gallery-items="pictures" + ng-if="pictures && pictures.length>0"></ion-gallery> + + <div class="item item-icon-right" ng-if="camera"> + Add pictures + <a class="dark" href="#" ng-click="openPicturePopup()"> + <i class="icon ion-camera"></i> + </a> + </div> + + <div class="item item-input item-icon-right" ng-if="!camera" > + <span class="input-label has-input">Add pictures</span> + <input type="file" id="file" accept=".png,.jpeg,.jpg" onchange="angular.element(this).scope().fileChanged(event)"/> + <!--a class="dark" href="#" ng-if="!camera" ng-click="addPictureFile()"> + <i class="icon ion-plus"></i> + </a--> + </div> + + <span class="item item-icon-left" ng-if="id && formData.issuer"> + <i class="icon ion-key"></i> + Issuer + <span class="badge">{{formData.issuer | formatPubkey}}</span> + </span> + + <span class="item item-button-right" ng-click="openCategoryModal()"> + Category + <span class="badge badge-royal">{{category.name}}</span> + <i class="button button-clear ion-chevron-right"></i> + </span> + + <div class="item item-input item-floating-label"> + <span class="input-label">Title</span> + <input type="text" placeholder="Title" ng-model="formData.title" /> + </div> + + <div class="item item-input item-floating-label"> + <span class="input-label">Description</span> + <textarea placeholder="Description" ng-model="formData.description"></textarea> + </div> + + <div class="item item-floating-label"> + <span class="input-label">Location</span> + <div class="item-input-inset"> + <label class="item-input-wrapper"> + <input type="text" placeholder="Location" ng-model="formData.location"> + </label> + <button class="button button-small button-positive" ng-click="localize()" ng-if="location.enable"> + <i class="icon ion-pinpoint"></i> + </button> + </div> + </div> + + + <!--<div class="item item-toggle dark"> + Public visibility + <label class="toggle toggle-royal"> + <input type="checkbox" ng-model="formData.public"> + <div class="track"> + <div class="handle"></div> + </div> + </label> + </div>--> + </div> + <div class="scroll-bar scroll-bar-v"></div> + </div> + </ion-content> + + +</ion-view> \ No newline at end of file diff --git a/www/templates/record/lookup.html b/www/templates/record/lookup.html new file mode 100644 index 0000000000000000000000000000000000000000..b3cba6e64894f3fd74a8d4d33b9d547eee45a570 --- /dev/null +++ b/www/templates/record/lookup.html @@ -0,0 +1,47 @@ +<ion-view view-title="Explore" left-buttons="leftButtons"> + <ion-nav-buttons side="secondary"> + <button ui-sref="app.add_record" class="button button-positive"> + <i class="icon ion-plus"></i> + Add + </button> + </ion-nav-buttons> + + <ion-content class="lookupForm padding"> + <label class="item item-input"> + <i class="icon ion-search placeholder-icon"></i> + <input type="text" placeholder="Search a record (e.g. car, store)" ng-model="search.text" ng-change="searchChanged()"> + </label> + + <div class="row"> + <div class="col"> + <a href="#" class="button button-full button-small button-positive" ng-click="">All</a> + </div> + <div class="col"> + <a href="#" class="button button-full button-small button-positive" ng-click="">Image</a> + </div> + <div class="col"> + <a href="#" class="button button-full button-small button-positive" ng-click="">Search tool</a> + </div> + </div> + + <div class="list list-inset"> + + <label class="item center" ng-if="search.looking"> + <ion-spinner icon="android"></ion-spinner> + </label> + + <a class="item row item-product" ng-repeat="found in search.results" ng-click="select('{{found.id}}')"> + <div class="col col-2"> + <img style="height:70px" ng-src="{{found.pictures[0].src}}" nf-if="found.pictures && found.pictures > 0"> + <span nf-if="!found.pictures || found.pictures == 0"> </span> + </div> + <div class="col col-80 padding"> + <h2 ng-bind-html="found.title"></h2> + <h3 class="light" ng-bind-html="found.description"></h3> + <span class="badge item-note">{{found.time | formatDate}}</span> + </div> + </a> + + </div> + </ion-content> +</ion-view> \ No newline at end of file diff --git a/www/templates/record/modal_category.html b/www/templates/record/modal_category.html new file mode 100644 index 0000000000000000000000000000000000000000..9b9edc9e55e2892169e3dc164a99649969f330fb --- /dev/null +++ b/www/templates/record/modal_category.html @@ -0,0 +1,23 @@ +<ion-modal-view> + <ion-header-bar class="bar-positive"> + <h1 class="title">Category</h1> + <button class="button button-positive" ng-click="closeCategoryModal()">Cancel</button> + </ion-header-bar> + + <ion-content class="lookupForm"> + <div class="list"> + <label class="item item-input"> + <i class="icon ion-search placeholder-icon"></i> + <input type="text" placeholder="Search" ng-model="search.text" ng-change="searchChanged()"> + </label> + + <label class="item center" ng-if="search.looking"> + <ion-spinner icon="android"></ion-spinner> + </label> + + <a class="item" ng-repeat="found in categories" ng-class="{'item-divider': (found.parent==null)}" href="#" ng-click="selectCategory(found)"> + <h2 ng-bind-html="found.name"></h2> + </a> + </div> +</ion-content> +</ion-modal-view> \ No newline at end of file diff --git a/www/templates/record/view_record.html b/www/templates/record/view_record.html new file mode 100644 index 0000000000000000000000000000000000000000..8c4077463aedb876725fbfc8636aeb3c70f90b62 --- /dev/null +++ b/www/templates/record/view_record.html @@ -0,0 +1,42 @@ +<ion-view view-title="{{formData.title}}" left-buttons="leftButtons"> + <ion-nav-buttons side="secondary"> + <button class="button button-positive" ng-click="edit()" ng-if="!isLogged() || formData.issuer == walletData.pubkey"> + <i class="icon ion-android-create"></i> + </button> + </ion-nav-buttons> + + <ion-content class="item-text-wrap"> + + <div class="scroll"> + <div class="list"> + + <ion-gallery ion-gallery-items="pictures" ng-if="pictures && pictures.length>0"></ion-gallery> + + <div class="item"> + <h2 ng-bind-html="formData.title"></h2> + </div> + + <div class="item"> + <p ng-bind-html="formData.description"></p> + </div> + + <div class="item-divider"></div> + + <span class="item item-icon-left"> + <i class="icon ion-person"></i> + Issuer + <span class="badge" ng-class="{'badge-positive': isMember, 'badge-assertive': !isMember}">{{formData.issuer | formatPubkey}}</span> + </span> + + <div class="item"> + Category + <span class="badge badge-positive">{{category.name}}</span> + </div> + + + + </div> + <div class="scroll-bar scroll-bar-v"></div> + </div> + </ion-content> +</ion-view> \ No newline at end of file