/* jshint -W041 */ /* jslint browser: true*/ /* global cordova,StatusBar,angular,console */ var appVersion = "0.0.0"; // core app start stuff angular.module('zmApp', [ 'ionic', 'tc.chartjs', 'zmApp.controllers', 'fileLogger', 'angular-carousel', 'angularAwesomeSlider', ]) // ------------------------------------------ // Various constants central repository // Feel free to change them as you see fit //----------------------------------------------- .constant('zm', { httpTimeout:15000, largeHttpTimeout:60000, logFile:'zmNinjaLog.txt', authoremail:'pliablepixels+zmNinja@gmail.com', logFileMaxSize: 50000, // after this limit log gets reset loginInterval:300000, //5m*60s*1000 - ZM auto login after 5 mins loadingTimeout:15000, safeMontageLimit:10, maxFPS:30, defaultFPS:3, maxMontageQuality:70, defaultMontageQuality:50, progressIntervalCheck:5000, // used for progress indicator on event playback graphFillColor:'rgba(151,187,205,0.5)', graphStrokeColor: 'rgba(151,187,205,0.8)', graphHighlightFill: 'rgba(0,163,124,0.5)', monitorCheckingColor:'#03A9F4', monitorNotRunningColor: '#F44336', monitorPendingColor: '#FF9800', monitorRunningColor: '#4CAF50', monitorErrorColor: '#795548', montageScaleFrequency:300, eventsListDetailsHeight:200, eventsListScrubHeight:300 }) .factory('imageLoadingDataShare', function () { var imageLoading = 0; // 0 = not loading, 1 = loading, -1 = error; return { 'set': function (val) { imageLoading = val; //console.log ("** IMAGE LOADING **"+val); }, 'get': function() { return imageLoading; } }; }) //------------------------------------------------------------------ // this directive will be load any time an image completes loading // via img tags where this directive is added (I am using this in // events and monitor view to show a loader while the image is // downloading from ZM //------------------------------------------------------------------ .directive('imageonload', function () { return { restrict: 'A', link: function (scope, element, attrs) { element.bind('load', function () { //call the function that was passed scope.$apply(attrs.imageonload); }); } }; }) //-------------------------------------------------------------------------------------------- // This directive is adapted from https://github.com/paveisistemas/ionic-image-lazy-load // I've removed lazyLoad and only made it show a spinner when an image is loading //-------------------------------------------------------------------------------------------- .directive('imageSpinnerSrc', ['$document', '$compile','imageLoadingDataShare', function ($document , $compile, imageLoadingDataShare) { return { restrict: 'A', scope: { imageSpinnerBackgroundImage: "@imageSpinnerBackgroundImage" }, link: function ($scope, $element, $attributes) { if ($attributes.imageSpinnerLoader) { var loader = $compile('
')($scope); $element.after(loader); } imageLoadingDataShare.set(1); loadImage(); $attributes.$observe('imageSpinnerSrc', function(value){ //console.log ("SOURCE CHANGED"); imageLoadingDataShare.set(1); loadImage(); //deregistration(); }); function loadImage() { $element.bind("load", function (e) { if ($attributes.imageSpinnerLoader) { loader.remove(); imageLoadingDataShare.set(0); } }); //PP $element.bind('error', function(){ loader.remove(); imageLoadingDataShare.set(0); }); if ($scope.imageSpinnerBackgroundImage == "true") { var bgImg = new Image(); bgImg.onload = function () { if ($attributes.imageSpinnerLoader) { loader.remove(); } $element[0].style.backgroundImage = 'url(' + $attributes.imageSpinnerSrc + ')'; // set style attribute on element (it will load image) }; bgImg.src = $attributes.imageSpinnerSrc; } else { $element[0].src = $attributes.imageSpinnerSrc; // set src attribute on element (it will load image) } } function isInView() { return true; } $element.on('$destroy', function () { }); } }; }]) //------------------------------------------------------------------ // In Android, HTTP requests seem to get stuck once in a while // It may be a crosswalk issue. // To tackle this gracefully, I've set up a global interceptor // If the HTTP request does not complete in 15 seconds, it cancels // That way the user can try again, and won't get stuck // Also remember you need to add it to .config //------------------------------------------------------------------ .factory('timeoutHttpIntercept', function ($rootScope, $q,zm) { var zmCookie = ""; return { 'request': function (config) { // config.withCredentials = true; if (zmCookie) { // console.log ("** ADDING COOKIE TO REQUEST "+zmCookie); config.headers.Cookie = "ZMSESSID="+zmCookie; } if ( !(config.url.indexOf("/api/states/change/") > -1 || config.url.indexOf("getDiskPercent.json") > -1 )) { config.timeout = 15000; } else { //console.log ("HTTP INTERCEPT:Skipping HTTP timeout for "+config.url); } return config; }, 'response': function (response) { //console.log ("******** WHOA RESPONSE CAUGHT ************"); var cookies = response.headers("Set-Cookie"); if (cookies !=null) { var zmSess=cookies.match("ZMSESSID=(.*?);"); if (zmSess[1]) { console.log ("***RESPONSE HEADER COOKIE " + zmSess[1]); console.log ("WHOLE STRING " + cookies); zmCookie=zmSess[1]; } } return response; } }; }) //----------------------------------------------------------------- // This service automatically logs into ZM at periodic intervals //------------------------------------------------------------------ .factory('zmAutoLogin', function($interval, ZMDataModel, $http,zm, $browser,$timeout) { var zmAutoLoginHandle; function doLogin() { console.log ("**** ZM AUTO LOGIN CALLED"); ZMDataModel.zmLog("zmAutologin timer started"); var loginData = ZMDataModel.getLogin(); $http({ method:'POST', //withCredentials: true, url:loginData.url + '/index.php', headers:{ 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, transformRequest: function (obj) { var str = []; for (var p in obj) str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); var foo = str.join("&"); //console.log("****RETURNING " + foo); return foo; }, data: { username:loginData.username, password:loginData.password, action:"login", view:"console" } }) .success(function(data,status,headers) { console.log ("**** ZM Login OK"); ZMDataModel.zmLog("zmAutologin successfully logged into Zoneminder"); //$cookies.myFavorite = 'oatmeal'; $timeout( function() {console.log ("***** ALL COOKIES:" + JSON.stringify( $browser.cookies()));},1000); console.log ("***** ALL HEADERS:" + headers('cookie')); }) .error(function(error) { console.log ("**** ZM Login FAILED"); ZMDataModel.zmLog ("zmAutologin Error " + JSON.stringify(error), "error"); }); } function start() { $interval.cancel(zmAutoLoginHandle); doLogin(); zmAutoLoginHandle = $interval(function() { doLogin(); },zm.loginInterval); // Auto login every 5 minutes // PHP timeout is around 10 minutes // should be ok? } function stop() { $interval.cancel(zmAutoLoginHandle); ZMDataModel.zmLog("Cancelling zmAutologin timer"); } return { start: start, stop: stop }; }) /* For future use - does not work with img src intercepts .factory ('httpAuthIntercept', function ($rootScope, $q) { return { requestError: function (response) { console.log ("**** REJECT REQUEST: "+JSON.stringify(response)); return $q.reject(response); }, responseError: function (response) { console.log ("**** REJECT RESPONSE: "+JSON.stringify(response)); return $q.reject(response); }, response: function (response) { console.log("*******RESPONSE with status: "+response.status+"****************"); if (response.status == 500) { console.log ("**** RESPONSE: "+JSON.stringify(response)); } return (response); } }; }) */ //------------------------------------------------------------------ // First run in ionic //------------------------------------------------------------------ .run(function ($ionicPlatform, $ionicPopup, $rootScope, zm, $state, $stateParams, ZMDataModel, $cordovaSplashscreen, $http, $interval, zmAutoLogin, $fileLogger,$timeout, $ionicHistory, $window, $ionicSideMenuDelegate) { $rootScope.zmGlobalCookie=""; $rootScope.isEventFilterOn = false; $rootScope.fromDate = ""; $rootScope.fromTime= ""; $rootScope.toDate = ""; $rootScope.toTime=""; $rootScope.fromString=""; $rootScope.toString=""; ZMDataModel.init(); // for making sure we canuse $state.go with ng-click // needed for views that use popovers $rootScope.$state = $state; $rootScope.$stateParams = $stateParams; var loginData = ZMDataModel.getLogin(); if (ZMDataModel.isLoggedIn()) { ZMDataModel.zmLog ("User is logged in"); console.log("VALID CREDENTIALS. Grabbing Monitors"); ZMDataModel.getMonitors(0); ZMDataModel.getKeyConfigParams(1); } // This code takes care of trapping the Android back button // and takes it to the menu. $ionicPlatform.registerBackButtonAction(function(e) { e.preventDefault(); if (!$ionicSideMenuDelegate.isOpenLeft()) { $ionicSideMenuDelegate.toggleLeft(); console.log ("Status of SIDE MENU IS : " + $ionicSideMenuDelegate.isOpen()); } else { navigator.app.exitApp(); } }, 1000); // this works reliably on both Android and iOS. The "onorientation" seems to reverse w/h in Android. Go figure. // http://stackoverflow.com/questions/1649086/detect-rotation-of-android-phone-in-the-browser-with-javascript var checkOrientation = function () { var pixelRatio = window.devicePixelRatio || 1; $rootScope.devWidth = ((window.innerWidth > 0) ? window.innerWidth : screen.width); $rootScope.devHeight = ((window.innerHeight > 0) ? window.innerHeight : screen.height); console.log("********NEW Computed Dev Width & Height as" + $rootScope.devWidth + "*" + $rootScope.devHeight); //ZMDataModel.zmLog("Device orientation change: "+$rootScope.devWidth + "*" + $rootScope.devHeight); }; window.addEventListener("resize", checkOrientation, false); $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { var requireLogin = toState.data.requireLogin; if (ZMDataModel.isLoggedIn()) { console.log("State transition is authorized"); return; } if (requireLogin) { console.log("**** STATE from " + "**** STATE TO " + toState.name); $ionicPopup.alert({ title: "Credentials Required", template: "Please provide your ZoneMinder credentials" }); // for whatever reason, .go was resulting in digest loops. // if you don't prevent, states will stack event.preventDefault(); $state.transitionTo('login'); } }); $ionicPlatform.ready(function () { // generates and error in desktops but works fine ZMDataModel.zmLog("Device is ready"); console.log("**** DEVICE READY ***"); $fileLogger.checkFile().then(function(resp) { if (parseInt(resp.size) > zm.logFileMaxSize) { console.log ("Deleting old log file as it exceeds 50K bytes"); $fileLogger.deleteLogfile().then(function() { console.log('Logfile deleted'); }); } else { console.log ("Log file size is " + resp.size + " bytes"); } }); //fileLogger is an excellent cross platform library // that allows you to manage log files without worrying about // paths etc.https://github.com/pbakondy/filelogger $fileLogger.setStorageFilename(zm.logFile); if (window.cordova) { // getAppVersion is a handy library // that lets you extract the app version in config.xml // given that you are always changing versions while // uploading to app/play stores, this is very useful // to keep in sync and use within your app cordova.getAppVersion(function(version) { appVersion = version; ZMDataModel.zmLog ("zmNinja Version: " + appVersion); ZMDataModel.setAppVersion(appVersion); }); } setTimeout(function () { if (window.cordova) { $cordovaSplashscreen.hide(); } }, 2000); /*if(window.navigator && window.navigator.splashscreen) { window.navigator.splashscreen.hide(); console.log ("Unlocking portrait mode after splash"); window.plugins.orientationLock.unlock(); }*/ var pixelRatio = window.devicePixelRatio || 1; $rootScope.devWidth = ((window.innerWidth > 0) ? window.innerWidth : screen.width); $rootScope.devHeight = ((window.innerHeight > 0) ? window.innerHeight : screen.height); console.log("********Computed Dev Width & Height as" + $rootScope.devWidth + "*" + $rootScope.devHeight); // What I noticed is when I moved the app to the device // the montage screens were not redrawn after resuming from background mode // Everything was fine if I switched back to the montage screen // so as a global hack I'm just reloading the current state if you switch // from foreground to background and back document.addEventListener("resume", function () { console.log("****The application is resuming from the background"); ZMDataModel.zmLog("App is resuming from background"); $rootScope.rand = Math.floor((Math.random() * 100000) + 1); //$scope.rand = Math.floor((Math.random() * 100000) + 1); console.log("** generated Random of " + $rootScope.rand); //console.log ("*******************************CURRENT STATE: " + JSON.stringify($state.current)); if ($state.current.url == "/timeline") { ZMDataModel.zmLog("Skipping state refresh for Timeline"); } else { ZMDataModel.zmLog ("Reloading screen for state " + $state.current.url); $state.go($state.current, {}, { reload: true }); } //$window.location.reload(true); //$route.reload(); // This sort of solves the problem of inactive windows // if you switch the screen off and on // not ideal as reload removes the Modal and shows the view // but better than an inactive/unresponsive screen // FIXME: see if we can get the modal back $window.location.reload(); zmAutoLogin.stop(); //safety zmAutoLogin.start(); }, false); document.addEventListener("pause", function () { console.log("****The application is going into background"); ZMDataModel.zmLog("App is going into background"); zmAutoLogin.stop(); }, false); if (window.cordova && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); // solves screen bouncing on form input // since I am using JS Scroll cordova.plugins.Keyboard.disableScroll(true); } if (window.StatusBar) { // org.apache.cordova.statusbar required StatusBar.styleDefault(); } }); //platformReady // lets POST so we get a session ID right hre //console.log ("Setting up POST LOGIN timer"); zmAutoLogin.start(); }) //run //------------------------------------------------------------------ // Route configuration //------------------------------------------------------------------ // My route map connecting menu options to their respective templates and controllers .config(function ($stateProvider, $urlRouterProvider, $httpProvider) { // If you do this, Allow Origin can't be * //$httpProvider.defaults.withCredentials = true; $httpProvider.interceptors.push('timeoutHttpIntercept'); //$httpProvider.interceptors.push('httpAuthIntercept'); $stateProvider .state('login', { data: { requireLogin: false }, url: "/login", templateUrl: "templates/login.html", controller: 'zmApp.LoginCtrl', }); $stateProvider .state('help', { data: { requireLogin: false }, url: "/help", templateUrl: "templates/help.html", controller: 'zmApp.HelpCtrl', }) /* .state('app', { url: '/', abstract: true, templateUrl: 'index.html', //controller: 'AppCtrl' })*/ .state('monitors', { data: { requireLogin: true }, resolve: { message: function (ZMDataModel) { console.log("Inside app.montage resolve"); return ZMDataModel.getMonitors(0); } }, url: "/monitors", templateUrl: "templates/monitors.html", controller: 'zmApp.MonitorCtrl', }) .state('events', { data: { requireLogin: true }, resolve: { message: function (ZMDataModel) { console.log("Inside app.events resolve"); return ZMDataModel.getMonitors(0); } }, url: "/events/:id", templateUrl: "templates/events.html", controller: 'zmApp.EventCtrl', }) .state('events-graphs', { data: { requireLogin: true }, url: "/events-graphs", templateUrl: "templates/events-graphs.html", controller: 'zmApp.EventsGraphsCtrl', }) .state('events-date-time-filter', { data: { requireLogin: true }, url: "/events-date-time-filter", templateUrl: "templates/events-date-time-filter.html", controller: 'zmApp.EventDateTimeFilterCtrl', }) .state('state', { data: { requireLogin: true }, url: "/state", templateUrl: "templates/state.html", controller: 'zmApp.StateCtrl', }) .state('devoptions', { data: { requireLogin: true }, url: "/devoptions", templateUrl: "templates/devoptions.html", controller: 'zmApp.DevOptionsCtrl', }) .state('timeline', { data: { requireLogin: true }, resolve: { message: function (ZMDataModel) { console.log("Inside app.events resolve"); return ZMDataModel.getMonitors(0); } }, url: "/timeline", templateUrl: "templates/timeline.html", controller: 'zmApp.TimelineCtrl', }) .state('log', { data: { requireLogin: false }, url: "/log", templateUrl: "templates/log.html", controller: 'zmApp.LogCtrl', }) .state('montage', { data: { requireLogin: true }, resolve: { message: function (ZMDataModel) { console.log("Inside app.montage resolve"); return ZMDataModel.getMonitors(0); } }, url: "/montage", templateUrl: "templates/montage.html", controller: 'zmApp.MontageCtrl', params: {minimal:false, isRefresh:false} }); // if none of the above states are matched, use this as the fallback var defaultState = "/montage"; //var defaultState = "/login"; // as it turns out I can't really inject a factory in config the normal way //$urlRouterProvider.otherwise(defaultState); // https://github.com/angular-ui/ui-router/issues/600 // If I start using the urlRouterProvider above and the // first state is monitors it goes into a digest loop. $urlRouterProvider.otherwise(function ($injector, $location) { var $state = $injector.get("$state"); $state.go("montage"); }); }); //config