diff --git a/.gitignore b/.gitignore index 8ed8afb5688418b53548f372904d1f877eb8c93a..2818f8321e56cb6589534d8d7ed80ee5d9141046 100644 --- a/.gitignore +++ b/.gitignore @@ -3,22 +3,22 @@ # Specifies intentionally untracked files to ignore when using Git # http://git-scm.com/docs/gitignore -node_modules/ -platforms/ -plugins/ +/node_modules/ +/platforms/ +/plugins/ .idea/ -cesium.iml -pom.xml -www/css/ionic.app.css -www/css/ionic.app.min.css +/cesium.iml +/pom.xml +/www/css/ionic.app.css +/www/css/ionic.app.min.css -www/lib/angular -www/lib/angular-animate -www/lib/angular-messages -www/lib/angular-sanitize -www/lib/angular-ui-router -www/lib/angular-moment -www/lib/moment -www/lib/waves -www/dist -hooks/playstore-config.json +/www/lib/angular +/www/lib/angular-animate +/www/lib/angular-messages +/www/lib/angular-sanitize +/www/lib/angular-ui-router +/www/lib/angular-moment +/www/lib/moment +/www/lib/waves +/www/dist +/hooks/playstore-config.json diff --git a/www/plugins/es/css/style.market.css b/www/plugins/es/css/style.market.css new file mode 100644 index 0000000000000000000000000000000000000000..923467f5201127e33ec48a3928b754d27c523610 --- /dev/null +++ b/www/plugins/es/css/style.market.css @@ -0,0 +1,74 @@ + +/********** + Categories select +**********/ + +.item-category{ + overflow: inherit ; + text-overflow: inherit ; + white-space: normal !important; +} + +.row-record { + border-bottom: solid 1px #ccc !important; + padding: 0 !important; + height: 100px !important; + overflow: hidden; +} + +.row-record .col { + padding-top: 5px; + padding-bottom: 0px; +} + +.row-record .badge-picture-count { + position: absolute !important; + left: 67px !important; + bottom: 15px !important; + right: inherit !important; + top: inherit !important; +} + +.row-record .badge-price { + position: absolute !important; + /*left: 80px !important;*/ + bottom: 15px !important; + right: inherit !important; + top: inherit !important; +} + +.col-text-wrap { + padding: 0; + margin: 0; +} +.col .text-wrap { + height: 93px; + white-space: normal; + position: relative; + word-wrap: break-word !important; + overflow: hidden !important; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -webkit-hyphens: auto; + -moz-hyphens: auto; + -ms-hyphens: auto; + -o-hyphens: auto; + hyphens: auto; +} + +/* Source: See doc: http://stackoverflow.com/questions/15814346 */ +.col .text-wrap:after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 100%; + height: 30%; + background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0)), color-stop(100%,rgba(0255,255,255,1))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */ +} diff --git a/www/plugins/es/i18n/locale-en.json b/www/plugins/es/i18n/locale-en.json new file mode 100644 index 0000000000000000000000000000000000000000..46686b1ddee6764b6c1d614cc052a75dd5ee7797 --- /dev/null +++ b/www/plugins/es/i18n/locale-en.json @@ -0,0 +1,89 @@ +{ + "MARKET": { + "COMMON": { + "CATEGORY": "Category", + "CATEGORIES": "Categories", + "ISSUER": "Issuer", + "PRICE": "Price", + "BTN_BUY": "Buy", + "BTN_NEW_AD": "New ad" + }, + "SEARCH": { + "TITLE": "Market", + "SEARCH_HELP": "Search (car, store)", + "BTN_ADD": "New", + "BTN_OPTIONS": "Advanced search", + "BTN_AROUND_ME": "Around me", + "LOCATION": "Location", + "LOCATION_HELP": "City", + "LAST_RECORDS": "Last records:", + "RESULTS": "Results:" + }, + "VIEW": { + "TITLE": "Ad" + }, + "EDIT": { + "TITLE": "Edit", + "RECORD_TITLE": "Title", + "RECORD_TITLE_HELP": "Title", + "RECORD_DESCRIPTION": "Description", + "RECORD_DESCRIPTION_HELP": "Description", + "RECORD_LOCATION": "Localization", + "RECORD_LOCATION_HELP": "Address, City", + "RECORD_PRICE": "Price", + "RECORD_PRICE_HELP": "Price (optional)", + "BTN_ADD_PICTURES": "Add pictures", + "BTN_PUBLISH": "Publish", + "BTN_SAVE": "Save" + }, + "ERROR": { + "LOAD_CATEGORY_FAILED": "Error while loading market categories", + "LOAD_RECORD_FAILED": "Error while loading record." + } + }, + "REGISTRY": { + "COMMON": { + "CATEGORY": "Category", + "CATEGORIES": "Categories", + "ISSUER": "Issuer", + "BTN_NEW": "Add" + }, + "SEARCH": { + "TITLE": "Registry", + "SEARCH_HELP": "Search (name, pseudo, public key...)", + "BTN_ADD": "New", + "BTN_OPTIONS": "Search tool", + "LOCATION": "Location", + "LOCATION_HELP": "City", + "LAST_RECORDS": "Last records:", + "RESULTS": "Results:" + }, + "VIEW": { + "TITLE": "Registry" + }, + "NEW": { + "TITLE": "New", + "SELECT_TYPE": "Choose type:", + "TYPE_PARTICULAR": "Private individual", + "TYPE_COMPANY": "Company, organisation or institution" + }, + "EDIT": { + "TITLE": "Edit", + "RECORD_TITLE": "Name", + "RECORD_TITLE_HELP": "Name", + "RECORD_DESCRIPTION": "About me", + "RECORD_DESCRIPTION_HELP": "Something about me", + "RECORD_LOCATION": "Localization", + "RECORD_LOCATION_HELP": "Localization (address, city)", + "RECORD_SOCIAL_NETWORKS": "Social networks", + "RECORD_PUBKEY": "Public key", + "BTN_ADD_PICTURES": "Add pictures", + "BTN_PUBLISH": "Publish", + "BTN_SAVE": "Save" + }, + "ERROR": { + "LOAD_RECORD_FAILED": "Loading failed", + "SAVE_RECORD_FAILED": "Sending failed" + } + } +} diff --git a/www/plugins/es/i18n/locale-fr-FR.json b/www/plugins/es/i18n/locale-fr-FR.json new file mode 100644 index 0000000000000000000000000000000000000000..395e30770e90c646e35adb6ff37103732437ef88 --- /dev/null +++ b/www/plugins/es/i18n/locale-fr-FR.json @@ -0,0 +1,89 @@ +{ + "MARKET": { + "COMMON": { + "CATEGORY": "Catégorie", + "CATEGORIES": "Catégories", + "ISSUER": "Soumis par", + "PRICE": "Prix", + "BTN_BUY": "Acheter", + "BTN_NEW_AD": "Nouvelle annonce" + }, + "SEARCH": { + "TITLE": "Annonces", + "SEARCH_HELP": "Recherche (voiture, livre...)", + "BTN_ADD": "Nouveau", + "BTN_OPTIONS": "Recherche avancée", + "BTN_AROUND_ME": "Autour de moi", + "LOCATION": "Localisation", + "LOCATION_HELP": "Ville", + "LAST_RECORDS": "Dernières annonces :", + "RESULTS": "Résultats :" + }, + "VIEW": { + "TITLE": "Annonce" + }, + "EDIT": { + "TITLE": "Edition", + "RECORD_TITLE": "Titre", + "RECORD_TITLE_HELP": "Titre", + "RECORD_DESCRIPTION": "Description", + "RECORD_DESCRIPTION_HELP": "Description", + "RECORD_LOCATION": "Ville", + "RECORD_LOCATION_HELP": "Adresse, Ville", + "RECORD_PRICE": "Prix", + "RECORD_PRICE_HELP": "Prix (optionnel)", + "BTN_ADD_PICTURES": "Ajouter des photos", + "BTN_PUBLISH": "Publier", + "BTN_SAVE": "Sauvegarder" + }, + "ERROR": { + "LOAD_CATEGORY_FAILED": "Erreur d'initialisation des catégories", + "LOAD_RECORD_FAILED": "Erreur lors du chargement de l'enregistrement." + } + }, + "REGISTRY": { + "COMMON": { + "CATEGORY": "Catégorie", + "CATEGORIES": "Catégories", + "ISSUER": "Soumis par", + "BTN_NEW": "Ajouter" + }, + "SEARCH": { + "TITLE": "Annuaire", + "SEARCH_HELP": "Recherche (nom, pseudo, clé publique...)", + "BTN_ADD": "Nouveau", + "BTN_OPTIONS": "Recherche avancée", + "LOCATION": "Localisation", + "LOCATION_HELP": "Ville", + "LAST_RECORDS": "Derniers enregistrements :", + "RESULTS": "Résultats :" + }, + "VIEW": { + "TITLE": "Annuaire" + }, + "NEW": { + "TITLE": "Nouveau", + "SELECT_TYPE": "Type de référencement :", + "TYPE_PARTICULAR": "Particulier", + "TYPE_COMPANY": "Entreprise, organisme ou institution" + }, + "EDIT": { + "TITLE": "Edition", + "RECORD_TITLE": "Nom", + "RECORD_TITLE_HELP": "Nom", + "RECORD_DESCRIPTION": "À propos de moi", + "RECORD_DESCRIPTION_HELP": "À propos de moi", + "RECORD_LOCATION": "Localisation", + "RECORD_LOCATION_HELP": "Localisation (adresse, ville)", + "RECORD_SOCIAL_NETWORKS": "Réseaux sociaux", + "RECORD_PUBKEY": "Clé publique", + "BTN_ADD_PICTURES": "Ajouter des photos", + "BTN_PUBLISH": "Publier", + "BTN_SAVE": "Sauvegarder" + }, + "ERROR": { + "LOAD_RECORD_FAILED": "Chargement impossible", + "SAVE_RECORD_FAILED": "Erreur lors de la sauvegarde" + } + } +} diff --git a/www/plugins/es/js/controllers/market-controllers.js b/www/plugins/es/js/controllers/market-controllers.js new file mode 100644 index 0000000000000000000000000000000000000000..74e3fcdbb2d719beed58ab78fc3d596e7903cd35 --- /dev/null +++ b/www/plugins/es/js/controllers/market-controllers.js @@ -0,0 +1,550 @@ +angular.module('cesium.market.controllers', ['cesium.services', 'ngSanitize']) + + .config(function($stateProvider, $urlRouterProvider) { + 'ngInject'; + + $stateProvider + + .state('app.market_lookup', { + url: "/market?q", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/market/lookup.html", + controller: 'MarketLookupCtrl' + } + } + }) + + .state('app.market_view_record', { + url: "/market/:id/:title", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/market/view_record.html", + controller: 'MarketRecordViewCtrl' + } + } + }) + + .state('app.market_add_record', { + url: "/market/add", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/market/edit_record.html", + controller: 'MarketRecordEditCtrl' + } + } + }) + + .state('app.market_edit_record', { + url: "/market/:id/edit", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/market/edit_record.html", + controller: 'MarketRecordEditCtrl' + } + } + }); + }) + + .controller('MarketLookupCtrl', MarketLookupController) + + .controller('MarketRecordViewCtrl', MarketRecordViewController) + + .controller('MarketRecordEditCtrl', MarketRecordEditController) + +; + +function MarketCategoryModalController($scope, Market, $state, $ionicModal, UIUtils) { + 'ngInject'; + + $scope.categoryModal = null; + $scope.categories = { + all: null, + search: { + text: '', + results: {}, + options: false + } + }; + + // category lookup modal + $ionicModal.fromTemplateUrl('plugins/es/templates/market/modal_category.html', { + scope: $scope, + focusFirstInput: true + }).then(function(modal) { + $scope.categoryModal = modal; + $scope.categoryModal.hide(); + }); + + $scope.openCategoryModal = function() { + + // load categories + Market.category.all() + .then(function(categories){ + $scope.categories.search.text = ''; + $scope.categories.search.results = categories; + $scope.categories.all = categories; + UIUtils.ink(); + $scope.categoryModal.show(); + }); + }; + + $scope.closeCategoryModal = function() { + $scope.categoryModal.hide(); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + console.log('Category ' + cat.name + 'selected. Method selectCategory(cat) not overwritten.'); + $scope.closeCategoryModal(); + }; + + $scope.searchCategoryChanged = function() { + $scope.categories.search.text = $scope.categories.search.text.toLowerCase(); + if ($scope.categories.search.text.length > 1) { + $scope.doSearchCategory($scope.categories.search.text); + } + else { + $scope.categories.search.results = $scope.categories.all; + } + }; + + $scope.doSearchCategory = function(text) { + $scope.search.looking = true; + + $scope.categories.search.results = $scope.categories.all.reduce(function(result, cat) { + if (cat.parent && cat.name.toLowerCase().search(text) != -1) { + return result.concat(cat); + } + return result; + }, []); + + $scope.categories.search.looking = false; + }; +} + +function MarketLookupController($scope, Market, $state, $ionicModal, $focus, $timeout, UIUtils) { + 'ngInject'; + + MarketCategoryModalController.call(this, $scope, Market, $state, $ionicModal, UIUtils); + + $scope.search = { + text: '', + lastRecords: true, + results: [], + looking: true, + category: null, + location: null, + options: false + }; + + $scope.$on('$ionicView.enter', function(e, $state) { + if (!$scope.entered || !$scope.search.results || $scope.search.results.length === 0) { + if ($state.stateParams && $state.stateParams.q) { // Query parameter + $scope.search.text=$state.stateParams.q; + $timeout(function() { + $scope.doSearch(); + }, 100); + } + else { + $timeout(function() { + $scope.doGetLastRecord(); + }, 100); + } + $scope.entered = true; + } + $focus('searchText'); + }); + + $scope.$watch('search.options', $scope.doSearch, true); + + $scope.isFilter = function(filter) { + return ($scope.filter == filter); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + $scope.search.category = cat; + $scope.closeCategoryModal(); + $scope.doSearch(); + }; + + $scope.doSearch = function() { + $scope.search.looking = true; + $scope.search.lastRecords = false; + if (!$scope.search.options) { + $scope.search.options = false; + } + + var request = { + query: {}, + highlight: { + fields : { + title : {}, + description : {} + } + }, + from: 0, + size: 20, + _source: Market.record.fields.commons + }; + var text = $scope.search.text.toLowerCase().trim(); + var matches = []; + var filters = []; + if (text.length > 1) { + var matchFields = ["title", "description", "issuer", "location"]; + matches.push({multi_match : { query: text, + fields: matchFields, + type: "phrase_prefix" + }}); + matches.push({match: { title: text}}); + matches.push({match: { description: text}}); + matches.push({prefix: { location: text}}); + } + if ($scope.search.options && $scope.search.category) { + filters.push({term: { category: $scope.search.category.id}}); + } + if ($scope.search.options && $scope.search.location && $scope.search.location.length > 0) { + filters.push({match_phrase: { location: $scope.search.location}}); + } + + if (matches.length === 0 && filters.length === 0) { + $scope.doGetLastRecord(); + return; + } + request.query.bool = {}; + if (matches.length > 0) { + request.query.bool.should = matches; + } + if (filters.length > 0) { + request.query.bool.filter = filters; + } + + $scope.doRequest(request); + }; + + $scope.doGetLastRecord = function() { + $scope.search.looking = true; + $scope.search.lastRecords = true; + + var request = { + sort: { + "time" : "desc" + }, + from: 0, + size: 20, + _source: Market.record.fields.commons + }; + + $scope.doRequest(request); + }; + + + $scope.doRequest = function(request) { + $scope.search.looking = true; + + Market.category.all() + .then(function(categories) { + return Market.record.search(request) + .then(function(res){ + if (res.hits.total === 0) { + $scope.search.results = []; + } + else { + var records = res.hits.hits.reduce(function(result, hit) { + var record = hit._source; + record.id = hit._id; + record.type = hit._type; + record.urlTitle = record.title; + if (record.category && record.category.id) { + record.category = categories[record.category.id]; + } + if (record.thumbnail) { + record.thumbnail = UIUtils.image.fromAttachment(record.thumbnail); + } + if (hit.highlight) { + if (hit.highlight.title) { + record.title = hit.highlight.title[0]; + } + if (hit.highlight.description) { + record.description = hit.highlight.description[0]; + } + if (hit.highlight.location) { + record.location = hit.highlight.location[0]; + } + } + return result.concat(record); + }, []); + $scope.search.results = records; + + // Set Motion + $timeout(function() { + UIUtils.motion.fadeSlideInRight({ + startVelocity: 3000 + }); + }, 10); + + // Set Ink + UIUtils.ink(); + } + + $scope.search.looking = false; + }) + .catch(function(err) { + $scope.search.looking = false; + $scope.search.results = []; + }); + }) + .catch(function(err) { + $scope.search.looking = false; + $scope.search.results = []; + }); + }; +} + +function MarketRecordViewController($scope, $ionicModal, Wallet, Market, UIUtils, $state, CryptoUtils, $q, $timeout) { + 'ngInject'; + + $scope.formData = {}; + $scope.id = null; + $scope.isMember = false; + $scope.category = {}; + $scope.pictures = []; + $scope.canEdit = false; + + $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.market_lookup'); + } + }); + + $scope.load = function(id) { + UIUtils.loading.show(); + Market.category.all() + .then(function(categories) { + Market.record.getCommons({id: id}) + .then(function (hit) { + $scope.formData = hit._source; + if (hit._source.category && hit._source.category.id) { + $scope.category = categories[hit._source.category.id]; + } + $scope.id= hit._id; + if (hit._source.thumbnail) { + $scope.thumbnail = UIUtils.image.fromAttachment(hit._source.thumbnail); + } + $scope.canEdit = !$scope.isLogged() || ($scope.formData && $scope.formData.issuer === Wallet.getData().pubkey); + UIUtils.loading.hide(); + + // launch get pictures + Market.record.getPictures({id: id}) + .then(function(hit) { + if (hit._source.pictures) { + $scope.pictures = hit._source.pictures.reduce(function(res, pic) { + return res.concat(UIUtils.image.fromAttachment(pic.file)); + }, []); + // Set Motion + $timeout(function() { + UIUtils.motion.fadeSlideIn({ + startVelocity: 3000 + }); + }, 10); + } + }) + .catch(function(err) { + $scope.pictures = []; + }); + }) + .catch(function(err) { + if (!$scope.secondTry) { + $scope.secondTry = true; + $q(function() { + $scope.load(id); // loop once + }, 100); + } + else { + UIUtils.onError('MARKET.ERROR.LOAD_RECORD_FAILED')(err); + } + }); + }) + .catch(function(){ + $scope.loading = false; + UIUtils.onError('MARKET.ERROR.LOAD_CATEGORY_FAILED')(err); + }); + }; + + $scope.edit = function() { + $state.go('app.market_edit_record', {id: $scope.id}); + }; +} + +function MarketRecordEditController($scope, $ionicModal, Wallet, Market, UIUtils, $state, CryptoUtils, $q, $ionicPopup, Device, $timeout) { + 'ngInject'; + + MarketCategoryModalController.call(this, $scope, Market, $state, $ionicModal, UIUtils); + + $scope.walletData = {}; + $scope.formData = {}; + $scope.id = null; + $scope.isMember = false; + $scope.category = {}; + $scope.pictures = []; + + $scope.$on('$ionicView.enter', function(e, $state) { + $scope.loadWallet() + .then(function(walletData) { + $scope.walletData = walletData; + if ($state.stateParams && $state.stateParams.id) { // Load by id + UIUtils.loading.show(); + $scope.load($state.stateParams.id); + } + else { + UIUtils.loading.hide(); + } + }); + }); + + $scope.load = function(id) { + UIUtils.loading.show(); + $q.all([ + Market.category.all() + .then(function(categories) { + Market.record.get({id: id}) + .then(function (hit) { + $scope.formData = hit._source; + if (hit._source.category && hit._source.category.id) { + $scope.category = categories[hit._source.category.id]; + } + $scope.id= hit._id; + if (hit._source.pictures) { + $scope.pictures = hit._source.pictures.reduce(function(res, pic) { + return res.concat(UIUtils.image.fromAttachment(pic.file)); + }, []); + } + UIUtils.loading.hide(); + UIUtils.motion.pushDown({ + selector: '.push-down' + }); + UIUtils.motion.fadeSlideInRight({ + selector: '.animate-fade-slide-in .item' + }); + // Set Ink + UIUtils.ink(); + }); + }) + ]) + .catch(UIUtils.onError('Could not load market')); + }; + + $scope.save = function() { + UIUtils.loading.show(); + return $q(function(resolve, reject) { + var doFinishSave = function(formData) { + if (!$scope.id) { // Create + // Set time (UTC) + // TODO : use the block chain time + formData.time = Math.floor(moment().utc().valueOf() / 1000); + Market.record.add(formData, $scope.walletData.keypair) + .then(function(id) { + UIUtils.loading.hide(); + $state.go('app.market_view_record', {id: id}); + resolve(); + }) + .catch(UIUtils.onError('Could not save market')); + } + else { // Update + if (formData.time) { + // Set time (UTC) + // TODO : use the block chain time + formData.time = Math.floor(moment().utc().valueOf() / 1000); + } + Market.record.update(formData, {id: $scope.id}, $scope.walletData.keypair) + .then(function() { + UIUtils.loading.hide(); + $state.go('app.market_view_record', {id: $scope.id}); + resolve(); + }) + .catch(UIUtils.onError('Could not update market')); + } + }; + + $scope.formData.picturesCount = $scope.pictures.length + if ($scope.formData.picturesCount > 0) { + $scope.formData.pictures = $scope.pictures.reduce(function(res, pic) { + return res.concat({file: UIUtils.image.toAttachment(pic)}); + }, []); + UIUtils.image.resizeSrc($scope.pictures[0].src, true) + .then(function(imageSrc) { + $scope.formData.thumbnail = UIUtils.image.toAttachment({src: imageSrc}); + + doFinishSave($scope.formData); + }); + } + else { + delete $scope.formData.thumbnail; + delete $scope.formData.pictures; + doFinishSave($scope.formData); + } + }); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + $scope.category = cat; + $scope.formData.category = cat; + $scope.closeCategoryModal(); + }; + + $scope.getPicture = function() { + Device.camera.getPicture() + .then(function(imageData) { + $scope.pictures.push({src: "data:image/png;base64," + imageData}); + $scope.$apply(); + }) + .catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED')); + }; + + $scope.fileChanged = function(event) { + UIUtils.loading.show(); + return $q(function(resolve, reject) { + var file = event.target.files[0]; + UIUtils.image.resizeFile(file) + .then(function(imageData) { + $scope.pictures.push({src: imageData}); + UIUtils.loading.hide(); + //$scope.$apply(); + resolve(); + }); + }); + }; + + $scope.removePicture = function(index){ + $scope.pictures.splice(index, 1); + }; + + $scope.favoritePicture = function(index){ + if (index > 0) { + var item = $scope.pictures[index]; + $scope.pictures.splice(index, 1); + $scope.pictures.splice(0, 0, item); + } + }; + + $scope.auth = function() { + $scope.loadWallet() + .then(function(walletData) { + UIUtils.loading.show(); + $scope.walletData = walletData; + Market.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')); + }; + +} diff --git a/www/plugins/es/js/controllers/registry-controllers.js b/www/plugins/es/js/controllers/registry-controllers.js new file mode 100644 index 0000000000000000000000000000000000000000..840e4993c60ee8b83e9f21676fbe501e0987dce1 --- /dev/null +++ b/www/plugins/es/js/controllers/registry-controllers.js @@ -0,0 +1,711 @@ +angular.module('cesium.registry.controllers', ['cesium.services', 'ngSanitize']) + + .config(function($stateProvider, $urlRouterProvider) { + 'ngInject'; + + $stateProvider + + .state('app.registry_lookup', { + url: "/registry?q", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/registry/lookup.html", + controller: 'RegistryLookupCtrl' + } + } + }) + + .state('app.registry_view_record', { + url: "/registry/:id/:title", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/registry/view_record.html", + controller: 'RegistryRecordViewCtrl' + } + } + }) + + .state('app.registry_add_record', { + url: "/registry/add", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/registry/edit_record.html", + controller: 'RegistryRecordEditCtrl' + } + } + }) + + .state('app.registry_edit_record', { + url: "/registry/:id/edit", + views: { + 'menuContent': { + templateUrl: "plugins/es/templates/registry/edit_record.html", + controller: 'RegistryRecordEditCtrl' + } + } + }) + ; + }) + + .controller('RegistryLookupCtrl', RegistryLookupController) + + .controller('RegistryRecordViewCtrl', RegistryRecordViewController) + + .controller('RegistryRecordEditCtrl', RegistryRecordEditController) + +; + +function RegistryCategoryModalController($scope, Registry, $state, $ionicModal, UIUtils) { + 'ngInject'; + + $scope.categoryModal = null; + $scope.categories = { + all: null, + search: { + text: '', + results: {}, + options: false + } + }; + + // category lookup modal + $ionicModal.fromTemplateUrl('plugins/es/templates/registry/modal_category.html', { + scope: $scope, + focusFirstInput: true + }).then(function(modal) { + $scope.categoryModal = modal; + $scope.categoryModal.hide(); + }); + + $scope.openCategoryModal = function() { + // load categories + Registry.category.all() + .then(function(categories){ + $scope.categories.search.text = ''; + $scope.categories.search.results = categories; + $scope.categories.all = categories; + UIUtils.ink(); + $scope.categoryModal.show(); + }); + }; + + $scope.closeCategoryModal = function() { + $scope.categoryModal.hide(); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + console.log('Category ' + cat.name + 'selected. Method selectCategory(cat) not overwritten.'); + $scope.closeCategoryModal(); + }; + + $scope.searchCategoryChanged = function() { + $scope.categories.search.text = $scope.categories.search.text.toLowerCase(); + if ($scope.categories.search.text.length > 1) { + $scope.doSearchCategory($scope.categories.search.text); + } + else { + $scope.categories.search.results = $scope.categories.all; + } + }; + + $scope.doSearchCategory = function(text) { + $scope.search.looking = true; + + $scope.categories.search.results = $scope.categories.all.reduce(function(result, cat) { + if (cat.parent && cat.name.toLowerCase().search(text) != -1) { + return result.concat(cat); + } + return result; + }, []); + + $scope.categories.search.looking = false; + }; +} + +function RegistryLookupController($scope, $state, $ionicModal, $focus, $q, $timeout, Registry, UIUtils, $sanitize) { + 'ngInject'; + + RegistryCategoryModalController.call(this, $scope, Registry, $state, $ionicModal, UIUtils); + RegistryNewRecordWizardController.call(this, $scope, $ionicModal, $state, UIUtils, $q, $timeout, Registry); + + $scope.search = { + text: '', + results: [], + lastRecords: true, + category: null, + location: null, + options: null + }; + + $scope.$on('$ionicView.enter', function(e, $state) { + if (!$scope.entered || !$scope.search.results || $scope.search.results.length === 0) { + if ($state.stateParams && $state.stateParams.q) { // Query parameter + $scope.search.text=$state.stateParams.q; + $timeout(function() { + $scope.doSearch(); + }, 100); + } + else { + $timeout(function() { + $scope.doGetLastRecord(); + }, 100); + } + $scope.entered = true; + } + $focus('searchText'); + }); + + $scope.$watch('search.options', $scope.doSearch, true); + + $scope.isFilter = function(filter) { + return ($scope.filter == filter); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + $scope.search.category = cat; + $scope.closeCategoryModal(); + $scope.doSearch(); + }; + + $scope.doSearch = function() { + $scope.search.looking = true; + $scope.search.lastRecords = false; + if (!$scope.search.options) { + $scope.search.options = false; + } + + var request = { + query: {}, + highlight: { + fields : { + title : {}, + description : {} + } + }, + from: 0, + size: 20, + _source: ["title", "description", "time", "location", "pictures", "issuer", "category"] + }; + var text = $scope.search.text.toLowerCase().trim(); + var matches = []; + var filters = []; + if (text.length > 1) { + var matchFields = ["title", "description", "issuer", "location"]; + matches.push({multi_match : { query: text, + fields: matchFields, + type: "phrase_prefix" + }}); + matches.push({match : { title: text}}); + matches.push({match : { description: text}}); + matches.push({prefix : { location: text}}); + matches.push({prefix : { issuer: text}}); + } + if ($scope.search.options && $scope.search.category) { + filters.push({term: { category: $scope.search.category.id}}); + } + if ($scope.search.options && $scope.search.location && $scope.search.location.length > 0) { + filters.push({match_phrase: { location: $scope.search.location}}); + } + + if (matches.length === 0 && filters.length === 0) { + $scope.search.results = []; + $scope.search.looking = false; + return; + } + request.query.bool = {}; + if (matches.length > 0) { + request.query.bool.should = matches; + } + if (filters.length > 0) { + request.query.bool.filter = filters; + } + + $scope.doRequest(request); + }; + + + $scope.doGetLastRecord = function() { + $scope.search.looking = true; + $scope.search.lastRecords = true; + + var request = { + sort: { + "time" : "desc" + }, + from: 0, + size: 20, + _source: ["title", "description", "time", "location", "pictures", "issuer", "category"] + }; + + $scope.doRequest(request); + }; + + $scope.doRequest = function(request) { + $scope.search.looking = true; + + Registry.category.all() + .then(function(categories) { + Registry.record.search(request) + .then(function(res){ + if (res.hits.total === 0) { + $scope.search.results = []; + } + else { + var items = res.hits.hits.reduce(function(result, hit) { + var registry = hit._source; + registry.id = hit._id; + registry.type = hit._type; + registry.urlTitle = registry.title; + if (registry.category) { + registry.category = categories[registry.category]; + } + if (hit.highlight) { + if (hit.highlight.title) { + registry.title = hit.highlight.title[0]; + } + if (hit.highlight.description) { + registry.description = hit.highlight.description[0]; + } + if (hit.highlight.location) { + registry.description = hit.highlight.location[0]; + } + } + return result.concat(registry); + }, []); + $scope.search.results = items; + + // Set Motion + $timeout(function() { + UIUtils.motion.fadeSlideInRight({ + startVelocity: 3000 + }); + }, 10); + + // Set Ink + UIUtils.ink(); + } + + $scope.search.looking = false; + }) + .catch(function(err) { + $scope.search.looking = false; + $scope.search.results = []; + }); + }) + .catch(function(err) { + $scope.search.looking = false; + $scope.search.results = []; + }); + }; + + // TODO: remove auto add account when done + /* $timeout(function() { + $scope.search.text='lavenier'; + $scope.doSearch(); + }, 400); + */ +} + +function RegistryRecordViewController($scope, $ionicModal, Wallet, Registry, UIUtils, $state, CryptoUtils, $q, BMA) { + 'ngInject'; + + $scope.formData = {}; + $scope.id = null; + $scope.isMember = false; + $scope.category = {}; + $scope.pictures = []; + $scope.canEdit = false; + $scope.hasSelf = false; + $scope.identity = null; + $scope.isCompany = false; + + $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_registry'); + } + }); + + $scope.load = function(id) { + UIUtils.loading.show(); + $q.all([ + Registry.category.all() + .then(function(categories) { + Registry.record.get({id: id}) + .then(function (hit) { + $scope.formData = hit._source; + if (hit._source.category && hit._source.category.id){ + $scope.category = categories[hit._source.category.id]; + } + $scope.id= hit._id; + if (hit._source.pictures) { + $scope.pictures = hit._source.pictures.reduce(function(res, pic) { + return res.concat({src: pic.src}); + }, []); + } + $scope.canEdit = !$scope.isLogged() || ($scope.formData && $scope.formData.issuer == Wallet.getData().pubkey); + + $scope.isCompany = $scope.category.id == 'particulier'; + if (!$scope.isCompany) { + BMA.wot.lookup({ search: $scope.formData.issuer }) + .then(function(res){ + $scope.identity = res.results.reduce(function(idties, res) { + return idties.concat(res.uids.reduce(function(uids, idty) { + return uids.concat({ + uid: idty.uid, + pub: res.pubkey, + timestamp: idty.meta.timestamp, + sig: idty.self + }); + }, [])); + }, [])[0]; + $scope.hasSelf = ($scope.identity.uid && $scope.identity.timestamp && $scope.identity.sig); + UIUtils.loading.hide(); + }) + .catch(function(err) { + if (err && err.ucode == 2001) { + $scope.identity = { + pub: $scope.formData.issuer + }; + $scope.hasSelf = false; + UIUtils.loading.hide(); + } + else { + UIUtils.onError('ERROR.WOT_LOOKUP_FAILED')(err); + } + }); + } + else { + $scope.hasSelf = false; + $scope.identity = null; + UIUtils.loading.hide(); + } + }) + .catch(function(err) { + // Retry (ES could have error) + if (!$scope.secondTry) { + $scope.secondTry = true; + $q(function() { + $scope.load(id); + }, 100); + } + else { + UIUtils.onError('ERROR.LOAD_IDENTITY_FAILED')(err); + } + }); + }) + ]).catch(UIUtils.onError('REGISTRY.ERROR.LOAD_RECORD_FAILED')); + }; + + // Edit click + $scope.edit = function() { + UIUtils.loading.show(); + $state.go('app.registry_edit_record', {id: $scope.id}); + }; + + // Certify click + $scope.certifyIdentity = function(identity) { + $scope.loadWallet() + .then(function(walletData) { + UIUtils.loading.show(); + Wallet.certify($scope.identity.uid, + $scope.identity.pub, + $scope.identity.timestamp, + $scope.identity.sig) + .then(function() { + UIUtils.loading.hide(); + UIUtils.alert.info('INFO.CERTIFICATION_DONE'); + }) + .catch(UIUtils.onError('ERROR.SEND_CERTIFICATION_FAILED')); + }) + .catch(UIUtils.onError('ERROR.LOGIN_FAILED')); + }; + +} + +function RegistryRecordEditController($scope, $ionicModal, Wallet, Registry, UIUtils, $state, CryptoUtils, $q, $ionicPopup, $translate, Device, + $ionicHistory) { + 'ngInject'; + + RegistryCategoryModalController.call(this, $scope, Registry, $state, $ionicModal, UIUtils); + + $scope.walletData = {}; + $scope.recordData = {}; + $scope.recordForm = {}; + $scope.isCompany = null; + $scope.id = null; + $scope.isMember = false; + $scope.category = {}; + $scope.pictures = []; + + $scope.setRecordForm = function(recordForm) { + $scope.recordForm = recordForm; + }; + + $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); + } + else { + UIUtils.loading.hide(); + } + }); + }); + + $scope.load = function(id) { + UIUtils.loading.show(); + $q.all([ + Registry.category.all() + .then(function(categories) { + Registry.record.get({id: id}) + .then(function (hit) { + $scope.recordData = hit._source; + if (hit._source.category && hit._source.category.id){ + $scope.category = categories[hit._source.category.id]; + } + $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('REGISTRY.ERROR.LOAD_RECORD_FAILED')); + }; + + $scope.save = function() { + UIUtils.loading.show(); + return $q(function(resolve, reject) { + $scope.recordData.pictures = $scope.pictures.reduce(function(res, pic) { + return res.concat({src: pic.src}); + }, []); + if (!$scope.id) { // Create + Registry.record.add($scope.recordData, $scope.walletData.keypair) + .then(function(id) { + UIUtils.loading.hide(); + $state.go('app.registry_view_record', {id: id}); + resolve(); + }) + .catch(UIUtils.onError('REGISTRY.ERROR.SAVE_RECORD_FAILED')); + } + else { // Update + Registry.record.update($scope.recordData, {id: $scope.id}, $scope.walletData.keypair) + .then(function() { + UIUtils.loading.hide(); + $state.go('app.registry_view_record', {id: $scope.id}); + resolve(); + }) + .catch(UIUtils.onError('REGISTRY.ERROR.SAVE_RECORD_FAILED')); + } + }); + }; + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + $scope.category = cat; + $scope.recordData.category = cat.id; + $scope.closeCategoryModal(); + }; + + $scope.openPicturePopup = function() { + Device.camera.getPicture() + .then(function(imageData) { + $scope.pictures.push({src: "data:image/png;base64," + imageData}); + $scope.$apply(); + }) + .catch(UIUtils.onError('ERROR.TAKE_PICTURE_FAILED')); + }; + + $scope.fileChanged = function(event) { + UIUtils.loading.show(); + return $q(function(resolve, reject) { + var file = event.target.files[0]; + UIUtils.image.resizeFile(file) + .then(function(imageData) { + $scope.pictures.push({src: imageData}); + UIUtils.loading.hide(); + //$scope.$apply(); + resolve(); + }); + }); + }; + + $scope.removePicture = function(index){ + $scope.pictures.splice(index, 1); + }; + + $scope.favoritePicture = function(index){ + if (index > 0) { + var item = $scope.pictures[index]; + $scope.pictures.splice(index, 1); + $scope.pictures.splice(0, 0, item); + } + }; + + $scope.auth = function() { + $scope.loadWallet() + .then(function(walletData) { + UIUtils.loading.show(); + $scope.walletData = walletData; + Registry.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')); + }; + + $scope.goBack = function() { + $ionicHistory.goBack(); + }; +} + +function RegistryNewRecordWizardController($scope, $ionicModal, $state, UIUtils, $q, $timeout, Registry) { + 'ngInject'; + + $scope.recordData = {}; + $scope.isCompany = null; + $scope.recordForm = {}; + $scope.pictures = []; + $scope.slides = { + slider: null, + options: { + loop: false, + effect: 'slide', + speed: 500 + } + }; + + // Called to navigate to the main app + $scope.cancel = function() { + if ($scope.newRecordModal) { + $scope.newRecordModal.hide(); + $scope.newRecordModal.remove(); + $scope.newRecordModal = null; + $timeout(function(){ + $scope.recordData = {}; + $scope.recordForm = {}; + $scope.isCompany = null; + $scope.pictures = []; + $scope.slides.slider.destroy(); + delete $scope.slides.slider; + }, 200); + } + }; + + $scope.setRecordForm = function(recordForm) { + $scope.recordForm = recordForm; + }; + + $scope.slidePrev = function() { + $scope.slides.slider.unlockSwipes(); + $scope.slides.slider.slidePrev(); + $scope.slides.slider.lockSwipes(); + }; + + $scope.slideNext = function() { + $scope.slides.slider.unlockSwipes(); + $scope.slides.slider.slideNext(); + $scope.slides.slider.lockSwipes(); + }; + + $scope.newRecord = function() { + var showModal = function() { + $scope.loadWallet() + .then(function(walletData) { + $scope.walletData = walletData; + $scope.slides.slider.slideTo(0); + $scope.slides.slider.lockSwipes(); + UIUtils.loading.hide(); + $scope.newRecordModal.show(); + // TODO: remove default + /*$timeout(function() { + $scope.recordData.title="Benoit Lavenier"; + $scope.recordData.description="J'aime le Sou !"; + $scope.setIsCompany(false); + }, 300);*/ + }); + }; + + if (!$scope.newRecordModal) { + UIUtils.loading.show(); + // Create the account modal that we will use later + $ionicModal.fromTemplateUrl('plugins/es/templates/registry/new_record_wizard.html', { + scope: $scope, + animation: 'slide-in-down' + }).then(function(modal) { + $scope.newRecordModal = modal; + $scope.newRecordModal.hide() + .then(function(){ + showModal(); + }); + + }); + } + else { + showModal(); + } + }; + + $scope.setIsCompany = function(bool) { + $scope.isCompany = bool; + $scope.slideNext(); + }; + + $scope.doNewRecord = function() { + $scope.recordForm.$submitted=true; + if(!$scope.recordForm.$valid) { + return; + } + + UIUtils.loading.show(); + return $q(function(resolve, reject) { + $scope.recordData.pictures = $scope.pictures.reduce(function(res, pic) { + return res.concat({src: pic.src}); + }, []); + // Use a default category if not a company + if (!$scope.isCompany) { + $scope.recordData.category={id:'particulier'}; + } + // Set time (UTC) + // TODO : use the block chain time + $scope.recordData.time = Math.floor(moment().utc().valueOf() / 1000); + Registry.record.add($scope.recordData, $scope.walletData.keypair) + .then(function(id) { + $scope.cancel(); + $state.go('app.registry_view_record', {id: id}); + resolve(); + }) + .catch(UIUtils.onError('Could not save registry')); + }); + }; + + //Cleanup the modal when hidden + $scope.$on('newRecordModal.hidden', function() { + $scope.cancel(); + }); + + $scope.selectCategory = function(cat) { + if (!cat.parent) return; + $scope.recordData.category = cat; + $scope.closeCategoryModal(); + }; + + // TODO: remove auto add account when done + /*$timeout(function() { + $scope.newRecord(); + }, 400); + */ +} diff --git a/www/plugins/es/js/services/market-services.js b/www/plugins/es/js/services/market-services.js new file mode 100644 index 0000000000000000000000000000000000000000..cbceb34197a1c4e43c7766c1ad586c8b5854feb1 --- /dev/null +++ b/www/plugins/es/js/services/market-services.js @@ -0,0 +1,247 @@ +angular.module('cesium.market.services', ['ngResource', 'cesium.services', 'cesium.config']) + +.factory('Market', function($http, $q, CryptoUtils, APP_CONFIG) { + 'ngInject'; + + function Market(server, wsServer) { + + var + categories = [], + fields = { + commons: ["category", "title", "description", "issuer", "time", "location", "price", "unit", "currency", "thumbnail", "picturesCount"] + } + ; + + if (wsServer) { + wsServer = server; + } + + function processError(reject, data) { + if (data && data.message) { + reject(data); + } + else { + reject('Unknown error from Duniter ES node'); + } + } + + function prepare(uri, params, config, callback) { + var pkeys = [], queryParams = {}, newUri = uri; + if (typeof params == 'object') { + pkeys = _.keys(params); + } + + _.forEach(pkeys, 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 + '/market/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 + _.forEach(categories, 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); + }); + } + + var postRecord = postResource('http://' + server + '/market/record'); + + function addRecord(record, keypair) { + return $q(function(resolve, reject) { + var errorFct = function(err) { + reject(err); + }; + var query = {}; + + + var obj = {}; + angular.copy(record, obj); + delete obj.signature; + delete obj.hash; + obj.issuer = CryptoUtils.util.encode_base58(keypair.signPk); + var str = JSON.stringify(obj); + + CryptoUtils.util.hash(str) + .then(function(hash) { + CryptoUtils.sign(str, keypair) + .then(function(signature) { + obj.hash = hash; + obj.signature = signature; + postRecord(obj) + .then(function (id){ + resolve(id); + }) + .catch(errorFct); + }) + .catch(errorFct); + }) + .catch(errorFct); + }); + } + + function emptyHit() { + return { + _id: null, + _index: null, + _type: null, + _version: null, + _source: {} + }; + } + + function getCommons() { + var _source = fields.commons.reduce(function(res, field){ + return res + ',' + field + }, '').substring(1); + return getResource('http://' + server + '/market/record/:id?_source=' + _source); + } + + return { + auth: { + get: getResource('http://' + server + '/auth'), + post: postResource('http://' + server + '/auth'), + token: getToken + }, + hit: { + empty: emptyHit + }, + category: { + all: getCategories, + searchText: getResource('http://' + server + '/market/category/_search?q=:search'), + search: postResource('http://' + server + '/market/category/_search?pretty') + }, + record: { + get: getResource('http://' + server + '/market/record/:id'), + getCommons: getCommons(), + getPictures: getResource('http://' + server + '/market/record/:id?_source=pictures'), + add: addRecord, + update: postResource('http://' + server + '/market/record/:id'), + searchText: getResource('http://' + server + '/market/record/_search?q=:search'), + search: postResource('http://' + server + '/market/record/_search?pretty'), + fields: { + commons: fields.commons + } + } + }; + } + + var ESNodeConfigured = !!APP_CONFIG.DUNITER_NODE_ES; + if (!ESNodeConfigured) { + return null; + } + + var service = Market(APP_CONFIG.DUNITER_NODE_ES); + + service.instance = Market; + return service; +}) +; diff --git a/www/plugins/es/js/services/registry-services.js b/www/plugins/es/js/services/registry-services.js new file mode 100644 index 0000000000000000000000000000000000000000..25ba108332dd1def16b8e60f9d4851f0c0977b63 --- /dev/null +++ b/www/plugins/es/js/services/registry-services.js @@ -0,0 +1,275 @@ +angular.module('cesium.registry.services', ['ngResource', 'cesium.services']) + +.factory('Registry', function($http, $q, CryptoUtils, APP_CONFIG) { + 'ngInject'; + + function Registry(server) { + + var categories = []; + + function processError(reject, data) { + if (data && data.message) { + reject(data); + } + else { + reject('Unknown error from Duniter node'); + } + } + + function prepare(uri, params, config, callback) { + var pkeys = [], queryParams = {}, newUri = uri; + if (typeof params == 'object') { + pkeys = _.keys(params); + } + + _.forEach(pkeys, 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 + '/registry/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 + _.forEach(categories, 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); + }); + } + + var addRecordRequest = postResource('http://' + server + '/registry/record'); + var updateRecordRequest = postResource('http://' + server + '/registry/record/:id'); + + function sendRecord(record, keypair, postRequest, params) { + return $q(function(resolve, reject) { + var errorFct = function(err) { + reject(err); + }; + var obj = {}; + angular.copy(record, obj); + delete obj.signature; + delete obj.hash; + obj.issuer = CryptoUtils.util.encode_base58(keypair.signPk); + var str = JSON.stringify(obj); + + CryptoUtils.util.hash(str) + .then(function(hash) { + CryptoUtils.sign(str, keypair) + .then(function(signature) { + obj.hash = hash; + obj.signature = signature; + postRequest(obj, params) + .then(function (id){ + resolve(id); + }) + .catch(errorFct); + }) + .catch(errorFct); + }) + .catch(errorFct); + }); + } + + function addRecord(record, keypair) { + return sendRecord(record, keypair, addRecordRequest); + } + + function updateRecord(record, params, keypair) { + return sendRecord(record, keypair, updateRecordRequest, params); + } + + var postAvatar = postResource('http://' + server + '/registry/record/_search'); + + function getAvatar(pubkey) { + return $q(function(resolve, reject) { + var errorFct = function(err) { + reject(err); + }; + var request = { + query: { + bool: { + should: [ + {match_phrase: {issuer: pubkey}}, + {match_phrase: {category: 'particulier'}} + ] + } + }, + from: 0, + size: 1, + _source: ["pictures.src"] + }; + + postAvatar(request) + .then(function(res) { + var imageData; + if (res.hits.total > 0) { + imageData = res.hits.hits.reduce(function(res, hit) { + return res.concat(hit._source.pictures.reduce(function(res, pic) { + return res.concat(pic.src); + }, [])[0]); + }, [])[0]; + } + else { + imageData = null; + } + resolve(imageData); + }) + .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 + }, + category: { + all: getCategories + }, + record: { + get: getResource('http://' + server + '/registry/record/:id'), + add: addRecord, + update: updateRecord, + searchText: getResource('http://' + server + '/registry/record/_search?q=:search'), + search: postResource('http://' + server + '/registry/record/_search?pretty'), + avatar: getAvatar + }, + currency: { + all: getResource('http://' + server + '/registry/currency/_search?_source=currencyName,peers.host,peers.port'), + get: getResource('http://' + server + '/registry/currency/:id/_source') + } + }; + } + + var ESNodeConfigured = !!APP_CONFIG.DUNITER_NODE_ES; + if (!ESNodeConfigured) { + return null; + } + + var service = Registry(APP_CONFIG.DUNITER_NODE_ES); + service.instance = Registry; + + return service; +}) +; diff --git a/www/plugins/es/templates/market/edit_record.html b/www/plugins/es/templates/market/edit_record.html new file mode 100644 index 0000000000000000000000000000000000000000..0b183b3abf280858a2f6db5270fcc0ba88790b6c --- /dev/null +++ b/www/plugins/es/templates/market/edit_record.html @@ -0,0 +1,121 @@ +<ion-view left-buttons="leftButtons"> + <ion-nav-title> + <span translate>MARKET.EDIT.TITLE</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + <button class="button button-clear button-icon icon visible-xs visible-sm" + ng-class="{'ion-android-send':!id, 'ion-android-done': id}" + ng-click="save()"> + </button> + </ion-nav-buttons> + + <ion-content> + <div class="scroll"> + <div class="list gallery" ng-if="pictures && pictures.length>0" > + <div ng-repeat="picture in pictures" + class="item card card-gallery"> + <div> + <h2 ng-if="picture.title">{{picture.title}}</h2> + <img ng-src="{{picture.src}}" ></img> + </div> + <div class="item tabs tabs-secondary tabs-icon-left"> + <a class="tab-item stable-bg assertive-900" ng-click="removePicture($index)"> + <i class="icon ion-close"></i> + {{'COMMON.BTN_PICTURE_DELETE'|translate}} + </a> + <a class="tab-item stable-bg" + ng-click="favoritePicture($index)" + ng-class="{'dark': $index !== 0, 'positive-900': $index === 0}"> + <i class="icon ion-star"></i> + {{'COMMON.BTN_PICTURE_FAVORISE'|translate}} + </a> + </div> + </div> + </div> + + <div class="list"> + + <div class="item item-icon-right hidden-no-device"> + <span translate>MARKET.EDIT.BTN_ADD_PICTURES</span> + <a class="dark" href="#" ng-click="openPicturePopup()"> + <i class="icon ion-camera"></i> + </a> + </div> + + <div class="item item-input item-icon-right hidden-device"> + <span class="input-label has-input" translate>MARKET.EDIT.BTN_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> + <span translate>MARKET.COMMON.ISSUER</span> + <span class="badge">{{formData.issuer | formatPubkey}}</span> + </span> + + <span class="item item-button-right" ng-click="openCategoryModal()"> + <span translate>MARKET.COMMON.CATEGORY</span> + <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" translate>MARKET.EDIT.RECORD_TITLE</span> + <input type="text" placeholder="{{'MARKET.EDIT.RECORD_TITLE_HELP'|translate}}" ng-model="formData.title"></input> + </div> + + <div class="item item-input item-floating-label"> + <span class="input-label" translate>MARKET.EDIT.RECORD_DESCRIPTION</span> + <textarea placeholder="{{'MARKET.EDIT.RECORD_DESCRIPTION_HELP'|translate}}" + ng-model="formData.description" + rows="8" cols="10"></textarea> + </div> + + <div class="item item-input item-floating-label"> + <span class="input-label" translate>MARKET.EDIT.RECORD_PRICE</span> + <input type="text" placeholder="{{'MARKET.EDIT.RECORD_PRICE_HELP'|translate}}" + ng-model="formData.price"></input> + </div> + + <div class="item item-floating-label" ng-if="location.enable"> + <span class="input-label" translate>MARKET.EDIT.RECORD_LOCATION</span> + <div class="item-input-inset"> + <label class="item-input-wrapper"> + <input type="text" placeholder="{{'MARKET.EDIT.RECORD_LOCATION_HELP'|translate}}" 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-input item-floating-label" ng-if="!location.enable"> + <span class="input-label" translate>MARKET.EDIT.RECORD_LOCATION</span> + <textarea placeholder="{{'MARKET.EDIT.RECORD_LOCATION_HELP'|translate}}" ng-model="formData.location"></textarea> + </div> + + </div> + + <div class="padding hidden-xs hidden-sm text-right"> + <button class="button button-clear button-dark ink" ng-click="goBack()" type="button" translate>COMMON.BTN_CANCEL + </button> + <!--<button class="button button-small button-stable ink" ng-click="saveAsDirty()">--> + <!--{{'MARKET.EDIT.BTN_SAVE' | translate}}--> + <!--</button>--> + <button class="button button-positive button-raised ink" ng-click="save()" ng-show="!id"> + {{'MARKET.EDIT.BTN_PUBLISH' | translate}} + </button> + <button class="button button-positive button-raised ink" ng-click="save()" ng-show="id"> + {{'MARKET.EDIT.BTN_SAVE' | translate}} + </button> + </div> + </div> + <div class="scroll-bar scroll-bar-v"></div> + </ion-content> + + +</ion-view> diff --git a/www/plugins/es/templates/market/lookup.html b/www/plugins/es/templates/market/lookup.html new file mode 100644 index 0000000000000000000000000000000000000000..217619a59151f4196b8c4bec30e19c57c5432e1b --- /dev/null +++ b/www/plugins/es/templates/market/lookup.html @@ -0,0 +1,150 @@ +<ion-view left-buttons="leftButtons"> + <ion-nav-title> + <span translate>MARKET.SEARCH.TITLE</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + <button class="button button-icon button-clear visible-xs" + ui-sref="app.market_add_record"> + <i class="icon ion-plus"></i> + </button> + </ion-nav-buttons> + + <ion-content class="lookupForm padding no-padding-xs"> + + + <form ng-submit="doSearch()"> + <label class="item item-input"> + <i class="icon ion-search placeholder-icon"></i> + <input type="text" placeholder="{{'MARKET.SEARCH.SEARCH_HELP'|translate}}" + ng-model="search.text" + ng-model-options="{ debounce: 350 }" + ng-change="doSearch()" id="searchText"> + </label> + + <div class="item item-toggle dark" ng-if="!search.lastRecords"> + <span translate>MARKET.SEARCH.BTN_OPTIONS</span> + <label class="toggle toggle-royal"> + <input type="checkbox" ng-model="search.options"> + <div class="track"> + <div class="handle"></div> + </div> + </label> + </div> + + <span class="item item-button-right" ng-click="openCategoryModal()" ng-if="search.options"> + <span translate>MARKET.COMMON.CATEGORY</span> + <span class="badge badge-royal">{{search.category.name}}</span> + <i class="button button-clear ion-chevron-right"></i> + </span> + + <span class="item item-button-right" ng-if="search.options && location.enable"> + <span translate>MARKET.SEARCH.BTN_AROUND_ME</span> + <label class="toggle toggle-royal"> + <input type="checkbox" ng-model="search.localize"> + <div class="track"> + <div class="handle"></div> + </div> + </label> + </span> + + <span class="item item-button-right" ng-if="search.options && !location.enable"> + <span translate>MARKET.SEARCH.LOCATION</span> + <div class="item-input-inset"> + <label class="item-input-wrapper"> + <input type="text" placeholder="{{'MARKET.SEARCH.LOCATION_HELP'|translate}}" + ng-model="search.location" + ng-model-options="{ debounce: 350 }" + ng-change="doSearch()"/> + </label> + </div> + </span> + </form> + + <div class="row no-padding"> + <div class="col"> + <h4 + ng-if="!search.looking && search.lastRecords && search.results.length > 0" translate> + MARKET.SEARCH.LAST_RECORDS + </h4> + <h4 + ng-if="!search.looking && !search.lastRecords && search.results.length > 0" translate> + MARKET.SEARCH.RESULTS + </h4> + </div> + + <div class="col col-25 hidden-xs hidden-sm padding" style="text-align:right"> + <button class="button button-raised button-full button-calm ink" + ui-sref="app.market_add_record"> + {{'MARKET.COMMON.BTN_NEW_AD' | translate}} + </button> + </div> + </div> + + <label class="center" ng-if="search.looking"> + <ion-spinner icon="android"></ion-spinner> + </label> + + <label class="center assertive animate-fade-slide-in-right" ng-if="!search.looking && search.results.length===0 && search.options != null" translate> + COMMON.SEARCH_NO_RESULT + </label> + + <div class="list animate-fade-slide-in-right" + ng-if="!search.looking && search.results && search.results.length > 0"> + + <a ng-repeat="rec in search.results" + class="item item-record ink padding-xs" + ui-sref="app.market_view_record({id: rec.id, title: rec.title})" + > + + <div class="visible-xs item-text-wrap" ng-class="{'item-thumbnail-left': rec.thumbnail}"> + <img ng-src="{{::rec.thumbnail.src}}" ng-if="rec.thumbnail"></img> + <h2 ng-bind-html="rec.title"></h2> + <h4 class="gray"> + <span ng-if="rec.location"> + <i class="icon ion-location"></i> + <span ng-bind-html="rec.location"></span> + </span> + <span ng-show="rec.time"> + <span ng-show="rec.location">|</span> + {{::rec.time | formatFromNow}} + </span> + </h4> + </div> + <!--<div class="visible-xs item-text-wrap"> + <p ng-bind-html="rec.description"></p> + </div>--> + + <div class="row row-record hidden-xs"> + <div class="col item-text-wrap item-thumbnail-left-padding" + ng-class="{'item-thumbnail-left': rec.thumbnail}"> + <img ng-src="{{::rec.thumbnail.src}}" ng-if="rec.thumbnail"> + <h2 ng-bind-html="rec.title"></h2> + <h4 class="gray"> + <i class="icon ion-location" ng-if="rec.location"></i> + <span ng-bind-html="rec.location"></span> + </h4> + <h4 class="gray" ng-if="rec.time"> + <i class="icon ion-clock" ></i> + {{::rec.time | formatFromNow}} + </h4> + <span ng-if="rec.picturesCount > 0" + class="badge badge-balanced badge-picture-count">{{::rec.picturesCount}} <i class="icon ion-camera"></i></span> + </div> + <div class="col col-20"> + <h3 class="gray" ng-if="rec.category">{{::rec.category.name|formatCategory}}</h3> + <h2 class="badge badge-price badge-calm" ng-bind-html="rec.price" ng-if="rec.price"></h2> + </div> + <div class="col"> + <h3 class="gray text-wrap" ng-bind-html="rec.description" ng-if="rec.description"></h3> + </div> + </div> + </a> + + </div> + </ion-content> + + <button class="button button-fab button-fab-bottom-right button-assertive icon ion-plus hidden-md hidden-lg" + ui-sref="app.market_add_record"> + </button> +</ion-view> diff --git a/www/plugins/es/templates/market/modal_category.html b/www/plugins/es/templates/market/modal_category.html new file mode 100644 index 0000000000000000000000000000000000000000..c29b34d43dc1907d5f090cea4017dae5f07b5bba --- /dev/null +++ b/www/plugins/es/templates/market/modal_category.html @@ -0,0 +1,25 @@ +<ion-modal-view> + <ion-header-bar class="bar-positive"> + <button class="button button-clear" ng-click="closeCategoryModal()" translate>COMMON.BTN_CANCEL</button> + <h1 class="title" translate>MARKET.COMMON.CATEGORIES</h1> + </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="categories.search.text" ng-change="searchCategoryChanged()"> + </label> + + <label class="item center" ng-if="modal.categories.search.looking"> + <ion-spinner icon="android"></ion-spinner> + </label> + + <a class="item" ng-repeat="found in categories.search.results" + ng-class="{'item-divider': !found.parent, 'positive': !found.parent}" + ng-click="selectCategory(found)"> + <h2 ng-bind-html="found.name"></h2> + </a> + </div> +</ion-content> +</ion-modal-view> diff --git a/www/plugins/es/templates/market/view_record.html b/www/plugins/es/templates/market/view_record.html new file mode 100644 index 0000000000000000000000000000000000000000..053923033f7aef89920fee9ddc957f2827fdd8df --- /dev/null +++ b/www/plugins/es/templates/market/view_record.html @@ -0,0 +1,89 @@ +<ion-view left-buttons="leftButtons"> + <ion-nav-title> + <span translate>MARKET.VIEW.TITLE</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + <button class="button button-positive button-clear button-icon ion-android-create" ng-click="edit()" ng-if="canEdit"> + </button> + </ion-nav-buttons> + + <ion-content class="viewRecord item-text-wrap"> + + <div class="scroll"> + <div class="row no-padding"> + <div class="col-20 visible-lg"> + </div> + <div class="col-10 visible-md"> + </div> + + <div class="col no-padding"> + <div class="list"> + + <div class="item" ng-class="{'item-thumbnail-left': pictures && pictures.length>0}"> + <img ng-src="{{pictures[0].src}}" nf-if="pictures && pictures.length>0"> + <h2 ng-bind-html="formData.title"></h2> + <h4 class="gray"> + <i class="icon ion-location" ng-show="formData.location"></i> + <span ng-bind-html="formData.location"></span> + <span ng-shwo="formData.location && formData.time"> </span> + <i class="icon ion-calendar" ng-show="formData.time"></i> + {{formData.time|formatDate}} + </h4> + </div> + + <div class="item" style="margin-top:10px"> + <p ng-bind-html="formData.description"></p> + </div> + + <span class="item item-icon-left item-button-right" > + <i class="icon ion-pricetag" ng-show="formData.price"></i> + <span class="dark">{{formData.price}}</span> + <button class="button button-raised button-calm ink-dark" + ng-click="transfer(formData.issuer, null, formData.price)"> + {{'MARKET.COMMON.BTN_BUY' | translate}} + </button> + </span> + + <div class="hidden-xs hidden-sm padding"> + + </div> + + <div class="item-divider visible-xs visible-sm"> </div> + + <a class="item item-icon-left ink" ui-sref="app.view_identity({pub:formData.issuer})"> + <i class="icon ion-person"></i> + <span translate>MARKET.COMMON.ISSUER</span> + <span class="badge" ng-class="{'badge-positive': isMember, 'badge-assertive': !isMember}">{{formData.issuer | formatPubkey}}</span> + </a> + + <div class="item item-icon-left"> + <i class="icon ion-flag"></i> + <span translate>MARKET.COMMON.CATEGORY</span> + <span class="badge badge-positive">{{category.name}}</span> + </div> + </div> + + <div class="list gallery" ng-if="pictures && pictures.length>0" > + <div ng-repeat="picture in pictures" + class="item card card-gallery"> + <div class="ink"> + <h2 ng-if="picture.title">{{::picture.title}}</h2> + <img ng-src="{{::picture.src}}" ></img> + </div> + </div> + </div> + </div> + + <div class="col-20 visible-lg"> + </div> + <div class="col-10 visible-md"> + </div> + </div> + </div> + + + <div class="scroll-bar scroll-bar-v"></div> + </div> + </ion-content> +</ion-view> diff --git a/www/plugins/es/templates/registry/edit_record.html b/www/plugins/es/templates/registry/edit_record.html new file mode 100644 index 0000000000000000000000000000000000000000..575c8c8dafa0e8b49dc26c341f8aa2ef61a98b2b --- /dev/null +++ b/www/plugins/es/templates/registry/edit_record.html @@ -0,0 +1,30 @@ +<ion-view left-buttons="leftButtons"> + <ion-nav-title> + <span translate>REGISTRY.EDIT.TITLE</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + <button class="button button-icon button-clear" 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"> + <ng-include src="'plugins/es/templates/registry/record_form.html'"></ng-include> + <div class="scroll-bar scroll-bar-v"></div> + </div> + + <div class="padding hidden-xs hidden-sm text-right"> + <button class="button button-clear button-dark ink" ng-click="goBack()" type="button" translate>COMMON.BTN_CANCEL + </button> + <button class="button button-positive button-raised ink" ng-click="save()" ng-show="!id"> + {{'REGISTRY.EDIT.BTN_PUBLISH' | translate}} + </button> + <button class="button button-positive button-raised ink" ng-click="save()" ng-show="id"> + {{'REGISTRY.EDIT.BTN_SAVE' | translate}} + </button> + </div> + </ion-content> +</ion-view> diff --git a/www/plugins/es/templates/registry/lookup.html b/www/plugins/es/templates/registry/lookup.html new file mode 100644 index 0000000000000000000000000000000000000000..851c0cb060a5d9e2924d9fdba5cd08bdd3701dd5 --- /dev/null +++ b/www/plugins/es/templates/registry/lookup.html @@ -0,0 +1,99 @@ +<ion-view view-title="{{'REGISTRY.SEARCH.TITLE'|translate}}" left-buttons="leftButtons"> + <ion-nav-buttons side="secondary"> + <!--<button class="button button-positive button-icon button-clear visible-xs" + ng-click="newRecord()"> + <i class="icon ion-plus"></i> + </button>--> + </ion-nav-buttons> + + <ion-content class="lookupForm padding no-padding-xs"> + + <form ng-submit="doSearch()"/> + <label class="item item-input"> + <i class="icon ion-search placeholder-icon"></i> + <input type="text" placeholder="{{'REGISTRY.SEARCH.SEARCH_HELP'|translate}}" + ng-model="search.text" + ng-model-options="{ debounce: 350 }" + ng-change="doSearch()" + id="searchText"> + </label> + + <div class="item item-toggle dark" ng-if="!search.lastRecords"> + <span translate>REGISTRY.SEARCH.BTN_OPTIONS</span> + <label class="toggle toggle-royal"> + <input type="checkbox" ng-model="search.options"> + <div class="track"> + <div class="handle"></div> + </div> + </label> + </div> + + <span class="item item-button-right" ng-click="openCategoryModal()" ng-if="search.options"> + <span translate>REGISTRY.COMMON.CATEGORY</span> + <span class="badge badge-royal">{{search.category.name}}</span> + <i class="button button-clear ion-chevron-right"></i> + </span> + + <span class="item item-button-right" ng-if="search.options && !location.enable"> + <span translate>REGISTRY.SEARCH.LOCATION</span> + <div class="item-input-inset"> + <label class="item-input-wrapper"> + <input type="text" placeholder="{{'REGISTRY.SEARCH.LOCATION_HELP'|translate}}" + ng-model="search.location" + ng-model-options="{ debounce: 350 }" + ng-change="doSearch()"/> + </label> + </div> + </span> + + </form> + + <div class="row no-padding"> + <div class="col"> + <h4 + ng-if="!search.looking && search.lastRecords && search.results.length > 0" translate> + REGISTRY.SEARCH.LAST_RECORDS + </h4> + <h4 + ng-if="!search.looking && !search.lastRecords && search.results.length > 0" translate> + REGISTRY.SEARCH.RESULTS + </h4> + </div> + + <div class="col col-25 hidden-xs hidden-sm" style="text-align:right"> + <button class="button button-full button-raised button-calm ink" + ng-click="newRecord()"> + {{'REGISTRY.COMMON.BTN_NEW' | translate}} + </button> + </div> + </div> + + <label class="center" ng-if="search.looking"> + <ion-spinner icon="android"></ion-spinner> + </label> + + <label class="center assertive animate-fade-slide-in-right" ng-if="!search.looking && search.results.length===0 && search.options != null" translate> + COMMON.SEARCH_NO_RESULT + </label> + + <div class="list animate-fade-slide-in-right" + ng-if="!search.looking && search.results && search.results.length > 0"> + + <a ng-repeat="found in search.results" + class="item item-avatar ink" + ui-sref="app.registry_view_record({id: found.id, title: found.title})"> + + <img ng-src="{{::found.pictures[0].src}}" ng-if="found.pictures && found.pictures.length > 0"> + <h2 class="item-text-wrap" ng-bind-html="found.title"></h2> + <h4 class="gray" ng-if="found.location">{{::found.location}}</h4> + <p class="gray item-text-wrap" ng-bind-html="found.description" ng-if="found.description"></p> + <span class="badge" ng-if="found.category && found.category.id=='particulier'">{{::found.issuer | formatPubkey}}</span> + </a> + + </div> + </ion-content> + + <button class="button button-fab button-fab-bottom-right button-assertive icon ion-plus hidden-md hidden-lg" + ng-click="newRecord()"> + </button> +</ion-view> diff --git a/www/plugins/es/templates/registry/modal_category.html b/www/plugins/es/templates/registry/modal_category.html new file mode 100644 index 0000000000000000000000000000000000000000..e332b5757cea627e40d71d3322c733caf32c560a --- /dev/null +++ b/www/plugins/es/templates/registry/modal_category.html @@ -0,0 +1,25 @@ +<ion-modal-view> + <ion-header-bar class="bar-positive"> + <button class="button button-clear" ng-click="closeCategoryModal()" translate>COMMON.BTN_CANCEL</button> + <h1 class="title" translate>REGISTRY.COMMON.CATEGORIES</h1> + </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="categories.search.text" ng-change="searchCategoryChanged()"> + </label> + + <label class="item center" ng-if="categories.search.looking"> + <ion-spinner icon="android"></ion-spinner> + </label> + + <a ng-repeat="found in categories.search.results" + ng-class="{'item-divider': !found.parent, 'item': !!found.parent}" + ng-click="selectCategory(found)"> + <h2 ng-bind-html="found.name" class="item-category"></h2> + </a> + </div> +</ion-content> +</ion-modal-view> diff --git a/www/plugins/es/templates/registry/new_record_wizard.html b/www/plugins/es/templates/registry/new_record_wizard.html new file mode 100644 index 0000000000000000000000000000000000000000..f03d3124efbd8ebb9bbd468641e94a06553d76f6 --- /dev/null +++ b/www/plugins/es/templates/registry/new_record_wizard.html @@ -0,0 +1,42 @@ +<ion-modal-view class="modal slide-in-up ng-enter active ng-enter-active"> + + <ion-header-bar class="bar-positive"> + + <button class="button back-button button-clear buttons header-item" ng-click="slidePrev()" ng-show="slides.slider.activeIndex > 0"> + <i class="icon ion-ios-arrow-back"></i> + <span translate>COMMON.BTN_BACK</span> + </button> + <button class="button button-clear" ng-click="cancel()" ng-show="slides.slider.activeIndex == 0" translate>COMMON.BTN_CANCEL</button> + + <h1 class="title" translate>REGISTRY.NEW.TITLE</h1> + + <button class="button button-clear button-icon ion-android-send" ng-click="doNewRecord()" ng-show="slides.slider.activeIndex == 1"></button> + + </ion-nav-buttons> + </ion-header-bar> + <ion-slides options="slides.options" slider="slides.slider"> + <ion-slide-page> + <ion-content class="has-header padding"> + <h3 translate>REGISTRY.NEW.SELECT_TYPE</h3> + <button class="button button-block button-stable icon icon-left ion-person" ng-click="setIsCompany(false)" ng-class="{ selected: !isCompany && isCompany != null }" translate>REGISTRY.NEW.TYPE_PARTICULAR</button> + <button class="button button-block button-stable icon icon-left ion-android-cart" ng-click="setIsCompany(true)" ng-class="{ selected: isCompany && isCompany != null }" translate>REGISTRY.NEW.TYPE_COMPANY</button> + </ion-content> + </ion-slide-page> + <ion-slide-page> + <ion-content class="has-header"> + <ng-include src="'plugins/es/templates/registry/record_form.html'"></ng-include> + </ion-content> + </ion-slide-page> + </ion-slides> +</ion-modal-view> + +<script type="text/ng-template" id="error-messages"> + <div class="error" ng-message="required"> + <i class="ion-information-circled"></i> + <div translate="ERROR.FIELD_REQUIRED"></div> + </div> + <div class="error" ng-message="minlength"> + <i class="ion-information-circled"></i> + <div translate="ERROR.FIELD_TOO_SHORT"></div> + </div> +</script> diff --git a/www/plugins/es/templates/registry/record_form.html b/www/plugins/es/templates/registry/record_form.html new file mode 100644 index 0000000000000000000000000000000000000000..5fcdb48d31ad440d630c5372d45dde6e952db745 --- /dev/null +++ b/www/plugins/es/templates/registry/record_form.html @@ -0,0 +1,91 @@ +<form name="recordForm" novalidate="" ng-submit="doNewRecord()"> + <div class="list" + ng-init="setRecordForm(recordForm)"> + + <div class="list gallery" ng-if="pictures && pictures.length>0" > + <div ng-repeat="picture in pictures" + class="item card card-gallery"> + <div> + <h2 ng-if="picture.title">{{picture.title}}</h2> + <img ng-src="{{picture.src}}" ></img> + </div> + <div class="item tabs tabs-secondary tabs-icon-left"> + <a class="tab-item stable-bg assertive-900" ng-click="removePicture($index)"> + <i class="icon ion-close"></i> + {{'COMMON.BTN_PICTURE_DELETE'|translate}} + </a> + <a class="tab-item stable-bg" + ng-click="favoritePicture($index)" + ng-class="{'dark': $index !== 0, 'positive-900': $index === 0}"> + <i class="icon ion-star"></i> + {{'COMMON.BTN_PICTURE_FAVORISE'|translate}} + </a> + </div> + </div> + </div> + + <div class="item item-icon-right hidden-no-device"> + <span translate>REGISTRY.EDIT.BTN_ADD_PICTURES</span> + <a class="dark" href="#" ng-click="openPicturePopup()"> + <i class="icon ion-camera"></i> + </a> + </div> + + <div class="item item-input item-icon-right hidden-device"> + <span class="input-label has-input" translate>REGISTRY.EDIT.BTN_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-button-right ink" ng-click="openCategoryModal()" ng-show="isCompany"> + <span translate>REGISTRY.COMMON.CATEGORY</span> + <span class="badge badge-royal">{{category.name | formatCategory}}</span> + <i class="button button-clear ion-chevron-right"></i> + </span> + + <div class="item item-input item-floating-label"> + <span class="input-label" translate>REGISTRY.EDIT.RECORD_TITLE</span> + <input type="text" placeholder="{{'REGISTRY.EDIT.RECORD_TITLE_HELP'|translate}}" ng-model="recordData.title" /> + </div> + + <div class="item item-input item-floating-label"> + <span class="input-label" translate>REGISTRY.EDIT.RECORD_DESCRIPTION</span> + <textarea placeholder="{{'REGISTRY.EDIT.RECORD_DESCRIPTION_HELP'|translate}}" + ng-model="recordData.description" + rows="8" cols="10"> + </textarea> + </div> + + <div class="item item-floating-label" ng-if="location.enable"> + <span class="input-label" translate>REGISTRY.EDIT.RECORD_LOCATION</span> + <div class="item-input-inset"> + <label class="item-input-wrapper"> + <input type="text" placeholder="{{'REGISTRY.EDIT.RECORD_LOCATION_HELP'|translate}}" ng-model="recordData.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-input item-floating-label" ng-if="!location.enable"> + <span class="input-label" translate>REGISTRY.EDIT.RECORD_LOCATION</span> + <textarea placeholder="{{'REGISTRY.EDIT.RECORD_LOCATION_HELP'|translate}}" ng-model="recordData.location"></textarea> + </div> + + <a class="item item-icon-left" ng-if="id && recordData.issuer && !isCompany"> + <i class="icon ion-key"></i> + <span translate>REGISTRY.EDIT.RECORD_PUBKEY</span> + <h4 class="gray">{{recordData.issuer}}</h4> + </a> + + <a class="item item-icon-left" ng-if="id && recordData.issuer && isCompany" > + <i class="icon ion-key"></i> + <span translate>REGISTRY.COMMON.ISSUER</span> + <h4>{{recordData.issuer | formatPubkey}}</h4> + </a> + + </div> +</form> diff --git a/www/plugins/es/templates/registry/view_record.html b/www/plugins/es/templates/registry/view_record.html new file mode 100644 index 0000000000000000000000000000000000000000..9956a87d4a6c1ec8ff4a3888cdcf27ce2dee965b --- /dev/null +++ b/www/plugins/es/templates/registry/view_record.html @@ -0,0 +1,87 @@ +<ion-view left-buttons="leftButtons"> + <ion-nav-title> + <span translate>REGISTRY.VIEW.TITLE</span> + </ion-nav-title> + + <ion-nav-buttons side="secondary"> + <button class="button button-icon button-clear ion-android-create" ng-click="edit()" ng-if="canEdit"> + </button> + </ion-nav-buttons> + + <ion-content class="item-text-wrap"> + + <div class="scroll"> + + <div class="positive-900-bg hero"> + <div class="content"> + <div class="avatar" + ng-if="pictures && pictures.length > 0" + style="background-image: url({{pictures[0].src}});"></div> + <i class="avatar avatar-member" ng-if="!pictures || pictures.length === 0"></i> + <h3 class="light">{{formData.title}}</h3> + <h4 class="gray"> + <i class="icon ion-location" ng-show="formData.location"></i> + <span ng-bind-html="formData.location"></span> + <span ng-shwo="formData.location && formData.time"> </span> + <i class="icon ion-calendar" ng-show="formData.time"></i> + {{formData.time|formatDate}} + </h4> + </div> + </div> + + <div class="hidden-xs hidden-sm padding" style="text-align:center"> + <button class="button button-raised button-assertive ink-dark" + ng-click="transfer(formData.issuer, formData.title)"> + {{'ACCOUNT.BTN_SEND_MONEY' | translate}} + </button> + + <button class="button button-raised button-icon icon ion-ribbon-b" ng-click="certifyIdentity()" ng-if="hasSelf"> + {{'WOT.BTN_CERTIFY' | translate}} + </button> + </div> + + <div class="list"> + + <span class="item item-icon-left ink" + ng-if="!isCompany" + copy-on-click> + <i class="icon ion-key"></i> + <p class="gray">{{formData.issuer}}</p> + </span> + + <div class="item"> + <p ng-bind-html="formData.description"></p> + </div> + + <div ng-if="isCompany"> + <div class="item-divider"></div> + + <div class="item"> + <span translate>REGISTRY.COMMON.CATEGORY</span> + <span class="badge badge-positive">{{category.name || formatCategory}}</span> + </div> + + <span class="item item-icon-left" > + <i class="icon ion-person"></i> + <h3><span translate>REGISTRY.COMMON.ISSUER</span></h3> + <span class="badge" ng-class="{'badge-positive': isMember, 'badge-assertive': !isMember}">{{formData.issuer | formatPubkey}}</span> + </span> + + </div> + + </div> + <div class="scroll-bar scroll-bar-v"></div> + </div> + </ion-content> + + <button class="button button-fab button-fab-bottom-right button-assertive icon ion-android-send visible-xs visible-sm" + ng-click="transfer(formData.issuer, formData.title)"> + </button> + + <button class="button button-fab button-fab-bottom-right button-assertive icon ion-ribbon-b visible-xs visible-sm" + ng-click="certifyIdentity()" + ng-if="hasSelf" + style="bottom:78px;"> + </button> + +</ion-view>