<commit_before>;(function($$){ 'use strict';

  $$.fn.core({

    autolock: function(bool){
      if( bool !== undefined ){
        this._private.autolock = bool ? true : false;
      } else {
        return this._private.autolock;
      }

      return this; // chaining
    },

    autoungrabify: function(bool){
      if( bool !== undefined ){
        this._private.autoungrabify = bool ? true : false;
      } else {
        return this._private.autoungrabify;
      }

      return this; // chaining
    },

    autounselectify: function(bool){
      if( bool !== undefined ){
        this._private.autounselectify = bool ? true : false;
      } else {
        return this._private.autounselectify;
      }

      return this; // chaining
    },

    panningEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.panningEnabled = bool ? true : false;
      } else {
        return this._private.panningEnabled;
      }

      return this; // chaining
    },

    userPanningEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.userPanningEnabled = bool ? true : false;
      } else {
        return this._private.userPanningEnabled;
      }

      return this; // chaining
    },

    zoomingEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.zoomingEnabled = bool ? true : false;
      } else {
        return this._private.zoomingEnabled;
      }

      return this; // chaining
    },

    userZoomingEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.userZoomingEnabled = bool ? true : false;
      } else {
        return this._private.userZoomingEnabled;
      }

      return this; // chaining
    },

    boxSelectionEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.boxSelectionEnabled = bool ? true : false;
      } else {
        return this._private.boxSelectionEnabled;
      }

      return this; // chaining
    },

    pan: function(){
      var args = arguments;
      var pan = this._private.pan;
      var dim, val, dims, x, y;

      switch( args.length ){
      case 0: // .pan()
        return pan;

      case 1:

        if( $$.is.string( args[0] ) ){ // .pan('x')
          dim = args[0];
          return pan[ dim ];

        } else if( $$.is.plainObject( args[0] ) ) { // .pan({ x: 0, y: 100 })
          if( !this._private.panningEnabled ){
            return this;
          }

          dims = args[0];
          x = dims.x;
          y = dims.y;

          if( $$.is.number(x) ){
            pan.x = x;
          }

          if( $$.is.number(y) ){
            pan.y = y;
          }

          this.trigger('pan viewport');
        }
        break;

      case 2: // .pan('x', 100)
        if( !this._private.panningEnabled ){
          return this;
        }

        dim = args[0];
        val = args[1];

        if( (dim === 'x' || dim === 'y') && $$.is.number(val) ){
          pan[dim] = val;
        }

        this.trigger('pan viewport');
        break;

      default:
        break; // invalid
      }

      this.notify({ // notify the renderer that the viewport changed
        type: 'viewport'
      });

      return this; // chaining
    },

    panBy: function(params){
      var args = arguments;
      var pan = this._private.pan;
      var dim, val, dims, x, y;

      if( !this._private.panningEnabled ){
        return this;
      }

      switch( args.length ){
      case 1:

        if( $$.is.plainObject( args[0] ) ) { // .panBy({ x: 0, y: 100 })
          dims = args[0];
          x = dims.x;
          y = dims.y;

          if( $$.is.number(x) ){
            pan.x += x;
          }

          if( $$.is.number(y) ){
            pan.y += y;
          }

          this.trigger('pan viewport');
        }
        break;

      case 2: // .panBy('x', 100)
        dim = args[0];
        val = args[1];

        if( (dim === 'x' || dim === 'y') && $$.is.number(val) ){
          pan[dim] += val;
        }

        this.trigger('pan viewport');
        break;

      default:
        break; // invalid
      }

      this.notify({ // notify the renderer that the viewport changed
        type: 'viewport'
      });

      return this; // chaining
    },

    fit: function( elements, padding ){
      var viewportState = this.getFitViewport( elements, padding );

      if( viewportState ){
        var _p = this._private;
        _p.zoom = viewportState.zoom;
        _p.pan = viewportState.pan;

        this.trigger('pan zoom viewport');

        this.notify({ // notify the renderer that the viewport changed
          type: 'viewport'
        });
      }

      return this; // chaining
    },

    getFitViewport: function( elements, padding ){
      if( $$.is.number(elements) && padding === undefined ){ // elements is optional
        padding = elements;
        elements = undefined;
      }

      if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
        return;
      }

      var bb;

      if( $$.is.string(elements) ){
        var sel = elements;
        elements = this.$( sel );

      } else if( $$.is.boundingBox(elements) ){ // assume bb
        var bbe = elements;
        bb = {
          x1: bbe.x1,
          y1: bbe.y1,
          x2: bbe.x2,
          y2: bbe.y2
        };

        bb.w = bb.x2 - bb.x1;
        bb.h = bb.y2 - bb.y1;

      } else if( !$$.is.elementOrCollection(elements) ){
        elements = this.elements();
      }

      bb = bb || elements.boundingBox();

      var w = this.width();
      var h = this.height();
      var zoom;
      padding = $$.is.number(padding) ? padding : 0;

      if( !isNaN(w) && !isNaN(h) && w > 0 && h > 0 && !isNaN(bb.w) && !isNaN(bb.h) &&  bb.w > 0 && bb.h > 0 ){
        zoom = Math.min( (w - 2*padding)/bb.w, (h - 2*padding)/bb.h );

        // crop zoom
        zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
        zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;

        var pan = { // now pan to middle
          x: (w - zoom*( bb.x1 + bb.x2 ))/2,
          y: (h - zoom*( bb.y1 + bb.y2 ))/2
        };

        return {
          zoom: zoom,
          pan: pan
        };
      }

      return;
    },

    minZoom: function( zoom ){
      if( zoom === undefined ){
        return this._private.minZoom;
      } else if( $$.is.number(zoom) ){
        this._private.minZoom = zoom;
      }

      return this;
    },

    maxZoom: function( zoom ){
      if( zoom === undefined ){
        return this._private.maxZoom;
      } else if( $$.is.number(zoom) ){
        this._private.maxZoom = zoom;
      }

      return this;
    },

    zoom: function( params ){
      var pos; // in rendered px
      var zoom;

      if( params === undefined ){ // then get the zoom
        return this._private.zoom;

      } else if( $$.is.number(params) ){ // then set the zoom
        zoom = params;

      } else if( $$.is.plainObject(params) ){ // then zoom about a point
        zoom = params.level;

        if( params.position ){
          var p = params.position;
          var pan = this._private.pan;
          var z = this._private.zoom;

          pos = { // convert to rendered px
            x: p.x * z + pan.x,
            y: p.y * z + pan.y
          };
        } else if( params.renderedPosition ){
          pos = params.renderedPosition;
        }

        if( pos && !this._private.panningEnabled ){
          return this; // panning disabled
        }
      }

      if( !this._private.zoomingEnabled ){
        return this; // zooming disabled
      }

      if( !$$.is.number(zoom) || ( pos && (!$$.is.number(pos.x) || !$$.is.number(pos.y)) ) ){
        return this; // can't zoom with invalid params
      }

      // crop zoom
      zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
      zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;

      if( pos ){ // set zoom about position
        var pan1 = this._private.pan;
        var zoom1 = this._private.zoom;
        var zoom2 = zoom;

        var pan2 = {
          x: -zoom2/zoom1 * (pos.x - pan1.x) + pos.x,
          y: -zoom2/zoom1 * (pos.y - pan1.y) + pos.y
        };

        this._private.zoom = zoom;
        this._private.pan = pan2;

        var posChanged = pan1.x !== pan2.x || pan1.y !== pan2.y;
        this.trigger(' zoom ' + (posChanged ? ' pan ' : '') + ' viewport ' );

      } else { // just set the zoom
        this._private.zoom = zoom;
        this.trigger('zoom viewport');
      }

      this.notify({ // notify the renderer that the viewport changed
        type: 'viewport'
      });

      return this; // chaining
    },

    viewport: function( opts ){
      var _p = this._private;
      var zoomDefd = true;
      var panDefd = true;
      var events = []; // to trigger
      var zoomFailed = false;
      var panFailed = false;

      if( !opts ){ return this; }
      if( !$$.is.number(opts.zoom) ){ zoomDefd = false; }
      if( !$$.is.plainObject(opts.pan) ){ panDefd = false; }
      if( !zoomDefd && !panDefd ){ return this; }

      if( zoomDefd ){
        var z = opts.zoom;

        if( z < _p.minZoom || z > _p.maxZoom || !_p.zoomingEnabled ){
          zoomFailed = true;

        } else {
          _p.zoom = z;

          events.push('zoom');
        }
      }

      if( panDefd && (!zoomFailed || !opts.cancelOnFailedZoom) && _p.panningEnabled ){
        var p = opts.pan;

        if( $$.is.number(p.x) ){
          _p.pan.x = p.x;
          panFailed = false;
        }

        if( $$.is.number(p.y) ){
          _p.pan.y = p.y;
          panFailed = false;
        }

        if( !panFailed ){
          events.push('pan');
        }
      }

      if( events.length > 0 ){
        events.push('viewport');
        this.trigger( events.join(' ') );

        this.notify({
          type: 'viewport'
        });
      }

      return this; // chaining
    },

    center: function( elements ){
      var pan = this.getCenterPan( elements );

      if( pan ){
        this._private.pan = pan;

        this.trigger('pan viewport');

        this.notify({ // notify the renderer that the viewport changed
          type: 'viewport'
        });
      }

      return this; // chaining
    },

    getCenterPan: function( elements ){
      if( !this._private.panningEnabled ){
        return;
      }

      if( $$.is.string(elements) ){
        var selector = elements;
        elements = this.elements( selector );
      } else if( !$$.is.elementOrCollection(elements) ){
        elements = this.elements();
      }

      var bb = elements.boundingBox();
      var w = this.width();
      var h = this.height();
      var zoom = this._private.zoom;

      var pan = { // middle
        x: (w - zoom*( bb.x1 + bb.x2 ))/2,
        y: (h - zoom*( bb.y1 + bb.y2 ))/2
      };

      return pan;
    },

    reset: function(){
      if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
        return this;
      }

      this.viewport({
        pan: { x: 0, y: 0 },
        zoom: 1
      });

      return this; // chaining
    },

    width: function(){
      var container = this._private.container;

      if( container ){
        return container.clientWidth;
      }

      return 1; // fallback if no container (not 0 b/c can be used for dividing etc)
    },

    height: function(){
      var container = this._private.container;

      if( container ){
        return container.clientHeight;
      }

      return 1; // fallback if no container (not 0 b/c can be used for dividing etc)
    },

    extent: function(){
      var pan = this._private.pan;
      var zoom = this._private.zoom;
      var rb = this.renderedExtent();

      var b = {
        x1: ( rb.x1 - pan.x )/zoom,
        x2: ( rb.x2 - pan.x )/zoom,
        y1: ( rb.y1 - pan.y )/zoom,
        y2: ( rb.y2 - pan.y )/zoom,
      };

      b.w = b.x2 - b.x1;
      b.h = b.y2 - b.y1;

      return b;
    },

    renderedExtent: function(){
      var width = this.width();
      var height = this.height();

      return {
        x1: 0,
        y1: 0,
        x2: width,
        y2: height,
        w: width,
        h: height
      };
    }
  });

  // aliases
  $$.corefn.centre = $$.corefn.center;

  // backwards compatibility
  $$.corefn.autolockNodes = $$.corefn.autolock;
  $$.corefn.autoungrabifyNodes = $$.corefn.autoungrabifyNodes;

})( cytoscape );
<commit_msg> cy.autoungrabifyNodes() not backwards compatible #704<commit_after>;(function($$){ 'use strict';

  $$.fn.core({

    autolock: function(bool){
      if( bool !== undefined ){
        this._private.autolock = bool ? true : false;
      } else {
        return this._private.autolock;
      }

      return this; // chaining
    },

    autoungrabify: function(bool){
      if( bool !== undefined ){
        this._private.autoungrabify = bool ? true : false;
      } else {
        return this._private.autoungrabify;
      }

      return this; // chaining
    },

    autounselectify: function(bool){
      if( bool !== undefined ){
        this._private.autounselectify = bool ? true : false;
      } else {
        return this._private.autounselectify;
      }

      return this; // chaining
    },

    panningEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.panningEnabled = bool ? true : false;
      } else {
        return this._private.panningEnabled;
      }

      return this; // chaining
    },

    userPanningEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.userPanningEnabled = bool ? true : false;
      } else {
        return this._private.userPanningEnabled;
      }

      return this; // chaining
    },

    zoomingEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.zoomingEnabled = bool ? true : false;
      } else {
        return this._private.zoomingEnabled;
      }

      return this; // chaining
    },

    userZoomingEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.userZoomingEnabled = bool ? true : false;
      } else {
        return this._private.userZoomingEnabled;
      }

      return this; // chaining
    },

    boxSelectionEnabled: function( bool ){
      if( bool !== undefined ){
        this._private.boxSelectionEnabled = bool ? true : false;
      } else {
        return this._private.boxSelectionEnabled;
      }

      return this; // chaining
    },

    pan: function(){
      var args = arguments;
      var pan = this._private.pan;
      var dim, val, dims, x, y;

      switch( args.length ){
      case 0: // .pan()
        return pan;

      case 1:

        if( $$.is.string( args[0] ) ){ // .pan('x')
          dim = args[0];
          return pan[ dim ];

        } else if( $$.is.plainObject( args[0] ) ) { // .pan({ x: 0, y: 100 })
          if( !this._private.panningEnabled ){
            return this;
          }

          dims = args[0];
          x = dims.x;
          y = dims.y;

          if( $$.is.number(x) ){
            pan.x = x;
          }

          if( $$.is.number(y) ){
            pan.y = y;
          }

          this.trigger('pan viewport');
        }
        break;

      case 2: // .pan('x', 100)
        if( !this._private.panningEnabled ){
          return this;
        }

        dim = args[0];
        val = args[1];

        if( (dim === 'x' || dim === 'y') && $$.is.number(val) ){
          pan[dim] = val;
        }

        this.trigger('pan viewport');
        break;

      default:
        break; // invalid
      }

      this.notify({ // notify the renderer that the viewport changed
        type: 'viewport'
      });

      return this; // chaining
    },

    panBy: function(params){
      var args = arguments;
      var pan = this._private.pan;
      var dim, val, dims, x, y;

      if( !this._private.panningEnabled ){
        return this;
      }

      switch( args.length ){
      case 1:

        if( $$.is.plainObject( args[0] ) ) { // .panBy({ x: 0, y: 100 })
          dims = args[0];
          x = dims.x;
          y = dims.y;

          if( $$.is.number(x) ){
            pan.x += x;
          }

          if( $$.is.number(y) ){
            pan.y += y;
          }

          this.trigger('pan viewport');
        }
        break;

      case 2: // .panBy('x', 100)
        dim = args[0];
        val = args[1];

        if( (dim === 'x' || dim === 'y') && $$.is.number(val) ){
          pan[dim] += val;
        }

        this.trigger('pan viewport');
        break;

      default:
        break; // invalid
      }

      this.notify({ // notify the renderer that the viewport changed
        type: 'viewport'
      });

      return this; // chaining
    },

    fit: function( elements, padding ){
      var viewportState = this.getFitViewport( elements, padding );

      if( viewportState ){
        var _p = this._private;
        _p.zoom = viewportState.zoom;
        _p.pan = viewportState.pan;

        this.trigger('pan zoom viewport');

        this.notify({ // notify the renderer that the viewport changed
          type: 'viewport'
        });
      }

      return this; // chaining
    },

    getFitViewport: function( elements, padding ){
      if( $$.is.number(elements) && padding === undefined ){ // elements is optional
        padding = elements;
        elements = undefined;
      }

      if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
        return;
      }

      var bb;

      if( $$.is.string(elements) ){
        var sel = elements;
        elements = this.$( sel );

      } else if( $$.is.boundingBox(elements) ){ // assume bb
        var bbe = elements;
        bb = {
          x1: bbe.x1,
          y1: bbe.y1,
          x2: bbe.x2,
          y2: bbe.y2
        };

        bb.w = bb.x2 - bb.x1;
        bb.h = bb.y2 - bb.y1;

      } else if( !$$.is.elementOrCollection(elements) ){
        elements = this.elements();
      }

      bb = bb || elements.boundingBox();

      var w = this.width();
      var h = this.height();
      var zoom;
      padding = $$.is.number(padding) ? padding : 0;

      if( !isNaN(w) && !isNaN(h) && w > 0 && h > 0 && !isNaN(bb.w) && !isNaN(bb.h) &&  bb.w > 0 && bb.h > 0 ){
        zoom = Math.min( (w - 2*padding)/bb.w, (h - 2*padding)/bb.h );

        // crop zoom
        zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
        zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;

        var pan = { // now pan to middle
          x: (w - zoom*( bb.x1 + bb.x2 ))/2,
          y: (h - zoom*( bb.y1 + bb.y2 ))/2
        };

        return {
          zoom: zoom,
          pan: pan
        };
      }

      return;
    },

    minZoom: function( zoom ){
      if( zoom === undefined ){
        return this._private.minZoom;
      } else if( $$.is.number(zoom) ){
        this._private.minZoom = zoom;
      }

      return this;
    },

    maxZoom: function( zoom ){
      if( zoom === undefined ){
        return this._private.maxZoom;
      } else if( $$.is.number(zoom) ){
        this._private.maxZoom = zoom;
      }

      return this;
    },

    zoom: function( params ){
      var pos; // in rendered px
      var zoom;

      if( params === undefined ){ // then get the zoom
        return this._private.zoom;

      } else if( $$.is.number(params) ){ // then set the zoom
        zoom = params;

      } else if( $$.is.plainObject(params) ){ // then zoom about a point
        zoom = params.level;

        if( params.position ){
          var p = params.position;
          var pan = this._private.pan;
          var z = this._private.zoom;

          pos = { // convert to rendered px
            x: p.x * z + pan.x,
            y: p.y * z + pan.y
          };
        } else if( params.renderedPosition ){
          pos = params.renderedPosition;
        }

        if( pos && !this._private.panningEnabled ){
          return this; // panning disabled
        }
      }

      if( !this._private.zoomingEnabled ){
        return this; // zooming disabled
      }

      if( !$$.is.number(zoom) || ( pos && (!$$.is.number(pos.x) || !$$.is.number(pos.y)) ) ){
        return this; // can't zoom with invalid params
      }

      // crop zoom
      zoom = zoom > this._private.maxZoom ? this._private.maxZoom : zoom;
      zoom = zoom < this._private.minZoom ? this._private.minZoom : zoom;

      if( pos ){ // set zoom about position
        var pan1 = this._private.pan;
        var zoom1 = this._private.zoom;
        var zoom2 = zoom;

        var pan2 = {
          x: -zoom2/zoom1 * (pos.x - pan1.x) + pos.x,
          y: -zoom2/zoom1 * (pos.y - pan1.y) + pos.y
        };

        this._private.zoom = zoom;
        this._private.pan = pan2;

        var posChanged = pan1.x !== pan2.x || pan1.y !== pan2.y;
        this.trigger(' zoom ' + (posChanged ? ' pan ' : '') + ' viewport ' );

      } else { // just set the zoom
        this._private.zoom = zoom;
        this.trigger('zoom viewport');
      }

      this.notify({ // notify the renderer that the viewport changed
        type: 'viewport'
      });

      return this; // chaining
    },

    viewport: function( opts ){
      var _p = this._private;
      var zoomDefd = true;
      var panDefd = true;
      var events = []; // to trigger
      var zoomFailed = false;
      var panFailed = false;

      if( !opts ){ return this; }
      if( !$$.is.number(opts.zoom) ){ zoomDefd = false; }
      if( !$$.is.plainObject(opts.pan) ){ panDefd = false; }
      if( !zoomDefd && !panDefd ){ return this; }

      if( zoomDefd ){
        var z = opts.zoom;

        if( z < _p.minZoom || z > _p.maxZoom || !_p.zoomingEnabled ){
          zoomFailed = true;

        } else {
          _p.zoom = z;

          events.push('zoom');
        }
      }

      if( panDefd && (!zoomFailed || !opts.cancelOnFailedZoom) && _p.panningEnabled ){
        var p = opts.pan;

        if( $$.is.number(p.x) ){
          _p.pan.x = p.x;
          panFailed = false;
        }

        if( $$.is.number(p.y) ){
          _p.pan.y = p.y;
          panFailed = false;
        }

        if( !panFailed ){
          events.push('pan');
        }
      }

      if( events.length > 0 ){
        events.push('viewport');
        this.trigger( events.join(' ') );

        this.notify({
          type: 'viewport'
        });
      }

      return this; // chaining
    },

    center: function( elements ){
      var pan = this.getCenterPan( elements );

      if( pan ){
        this._private.pan = pan;

        this.trigger('pan viewport');

        this.notify({ // notify the renderer that the viewport changed
          type: 'viewport'
        });
      }

      return this; // chaining
    },

    getCenterPan: function( elements ){
      if( !this._private.panningEnabled ){
        return;
      }

      if( $$.is.string(elements) ){
        var selector = elements;
        elements = this.elements( selector );
      } else if( !$$.is.elementOrCollection(elements) ){
        elements = this.elements();
      }

      var bb = elements.boundingBox();
      var w = this.width();
      var h = this.height();
      var zoom = this._private.zoom;

      var pan = { // middle
        x: (w - zoom*( bb.x1 + bb.x2 ))/2,
        y: (h - zoom*( bb.y1 + bb.y2 ))/2
      };

      return pan;
    },

    reset: function(){
      if( !this._private.panningEnabled || !this._private.zoomingEnabled ){
        return this;
      }

      this.viewport({
        pan: { x: 0, y: 0 },
        zoom: 1
      });

      return this; // chaining
    },

    width: function(){
      var container = this._private.container;

      if( container ){
        return container.clientWidth;
      }

      return 1; // fallback if no container (not 0 b/c can be used for dividing etc)
    },

    height: function(){
      var container = this._private.container;

      if( container ){
        return container.clientHeight;
      }

      return 1; // fallback if no container (not 0 b/c can be used for dividing etc)
    },

    extent: function(){
      var pan = this._private.pan;
      var zoom = this._private.zoom;
      var rb = this.renderedExtent();

      var b = {
        x1: ( rb.x1 - pan.x )/zoom,
        x2: ( rb.x2 - pan.x )/zoom,
        y1: ( rb.y1 - pan.y )/zoom,
        y2: ( rb.y2 - pan.y )/zoom,
      };

      b.w = b.x2 - b.x1;
      b.h = b.y2 - b.y1;

      return b;
    },

    renderedExtent: function(){
      var width = this.width();
      var height = this.height();

      return {
        x1: 0,
        y1: 0,
        x2: width,
        y2: height,
        w: width,
        h: height
      };
    }
  });

  // aliases
  $$.corefn.centre = $$.corefn.center;

  // backwards compatibility
  $$.corefn.autolockNodes = $$.corefn.autolock;
  $$.corefn.autoungrabifyNodes = $$.corefn.autoungrabify;

})( cytoscape );
<|endoftext|>


<commit_before>(function() {
  'use strict';

  angular.module('dimApp')
    .factory('dimSearchService', function() {
      return {
        query: ''
      };
    })
    .directive('dimSearchFilter', SearchFilter);

  SearchFilter.$inject = [];

  function SearchFilter() {
    return {
      controller: SearchFilterCtrl,
      controllerAs: 'vm',
      link: Link,
      bindToController: true,
      restrict: 'A',
      template: [
        '<input id="filter-input" placeholder="Search item/perk or is:arc" type="search" name="filter" ng-model="vm.search.query" ng-model-options="{ debounce: 500 }" ng-trim="true">'
      ].join('')
    };
  }

  /**
   * Filter translation sets. Left-hand is the filter to run from filterFns, right side are possible filterResult
   * values that will set the left-hand to the "match."
   */
  var filterTrans = {
    dmg: ['arc', 'solar', 'void', 'kinetic'],
    type: ['primary', 'special', 'heavy', 'helmet', 'leg', 'gauntlets', 'chest', 'class', 'classitem', 'artifact', 'ghost', 'horn', 'consumable', 'ship', 'material', 'vehicle', 'emblem', 'bounties', 'quests', 'messages', 'missions', 'emote'],
    tier: ['common', 'uncommon', 'rare', 'legendary', 'exotic', 'white', 'green', 'blue', 'purple', 'yellow'],
    incomplete: ['incomplete'],
    complete: ['complete'],
    xpcomplete: ['xpcomplete'],
    xpincomplete: ['xpincomplete', 'needsxp'],
    upgraded: ['upgraded'],
    classType: ['titan', 'hunter', 'warlock'],
    dupe: ['dupe', 'duplicate'],
    unascended: ['unascended', 'unassended', 'unasscended'],
    ascended: ['ascended', 'assended', 'asscended'],
    reforgeable: ['reforgeable', 'reforge', 'rerollable', 'reroll'],
    tracked: ['tracked'],
    untracked: ['untracked'],
    locked: ['locked'],
    unlocked: ['unlocked'],
    stackable: ['stackable'],
    engram: ['engram'],
    weaponClass: ['pulserifle', 'scoutrifle', 'handcannon', 'autorifle', 'primaryweaponengram', 'sniperrifle', 'shotgun', 'fusionrifle', 'specialweaponengram', 'rocketlauncher', 'machinegun', 'heavyweaponengram', 'sidearm', 'sword'],
    year: ['year1', 'year2'],
    infusable: ['infusable', 'infuse'],
    stattype: ['intellect', 'discipline', 'strength'],
    new: ['new'],
    glimmer: ['glimmeritem', 'glimmerboost', 'glimmersupply']
  };

  var keywords = _.flatten(_.flatten(_.values(filterTrans)).map(function(word) {
    return ["is:" + word, "not:" + word];
  }));

  // Filters that operate on ranges (>, <, >=, <=)
  var ranges = ['light', 'level', 'quality', 'percentage'];
  ranges.forEach(function(range) {
    keywords.push(range + ":<", range + ":>", range + ":<=", range + ":>=");
  });

  function Link(scope, element) {
    element.find('input').textcomplete([
      {
        words: keywords,
        match: /\b((li|le|qu|pe|is:|not:)\w*)$/,
        search: function(term, callback) {
          callback($.map(this.words, function(word) {
            return word.indexOf(term) === 0 ? word : null;
          }));
        },
        index: 1,
        replace: function(word) {
          return (word.indexOf('is:') === 0 && word.indexOf('not:') === 0)
            ? (word + ' ') : word;
        }
      }
    ], {
      zIndex: 1000
    });
  }

  SearchFilterCtrl.$inject = ['$scope', 'dimStoreService', 'dimSearchService'];

  function SearchFilterCtrl($scope, dimStoreService, dimSearchService) {
    var vm = this;
    var filterInputSelector = '#filter-input';
    var _duplicates = null; // Holds a map from item hash to count of occurrances of that hash

    vm.search = dimSearchService;

    $scope.$watch('vm.search.query', function() {
      vm.filter();
    });

    $scope.$on('dim-stores-updated', function() {
      _duplicates = null;
      vm.filter();
    });

    // Something has changed that could invalidate filters
    $scope.$on('dim-filter-invalidate', function() {
      _duplicates = null;
      vm.filter();
    });

    $scope.$on('dim-focus-filter-input', function() {
      vm.focusFilterInput();
    });

    $scope.$on('dim-escape-filter-input', function() {
      vm.blurFilterInputIfEmpty();
      vm.clearFilter();
    });

    $scope.$on('dim-clear-filter-input', function() {
      vm.clearFilter();
    });

    vm.blurFilterInputIfEmpty = function() {
      if (vm.search.query === "") {
        vm.blurFilterInput();
      }
    };

    vm.focusFilterInput = function() {
      $(filterInputSelector).focus();
    };

    vm.blurFilterInput = function() {
      $(filterInputSelector).blur();
    };

    vm.clearFilter = function() {
      vm.search.query = "";
      vm.filter();
    };

    vm.filter = function() {
      var filterValue = (vm.search.query) ? vm.search.query.toLowerCase() : '';
      filterValue = filterValue.replace(/\s+and\s+/, ' ');
      var searchTerms = filterValue.split(/\s+/);
      var filter;
      var predicate = '';
      var filterFn;
      var filters = [];

      function addPredicate(predicate, filter, invert = false) {
        filters.push({ predicate: predicate, value: filter, invert: invert });
      }

      _.each(searchTerms, function(term) {
        if (term.indexOf('is:') >= 0) {
          filter = term.replace('is:', '');
          if (_cachedFilters[filter]) {
            predicate = _cachedFilters[filter];
            addPredicate(predicate, filter);
          } else {
            for (const key in filterTrans) {
              if (filterTrans.hasOwnProperty(key) && filterTrans[key].indexOf(filter) > -1) {
                predicate = key;
                _cachedFilters[filter] = key;
                addPredicate(predicate, filter);
                break;
              }
            }
          }
        } else if (term.indexOf('not:') >= 0) {
          filter = term.replace('not:', '');
          if (_cachedFilters[filter]) {
            predicate = _cachedFilters[filter];
            addPredicate(predicate, filter, true);
          } else {
            for (const key in filterTrans) {
              if (filterTrans.hasOwnProperty(key) && filterTrans[key].indexOf(filter) > -1) {
                predicate = key;
                _cachedFilters[filter] = key;
                addPredicate(predicate, filter, true);
                break;
              }
            }
          }
        } else if (term.indexOf('light:') >= 0 || term.indexOf('level:') >= 0) {
          filter = term.replace('light:', '').replace('level:', '');
          addPredicate("light", filter);
        } else if (term.indexOf('quality:') >= 0 || term.indexOf('percentage:') >= 0) {
          filter = term.replace('quality:', '').replace('percentage:', '');
          addPredicate("quality", filter);
        } else if (!/^\s*$/.test(term)) {
          addPredicate("keyword", term);
        }
      });

      filterFn = function(item) {
        return _.all(filters, function(filter) {
          var result = filterFns[filter.predicate](filter.value, item);
          return filter.invert ? !result : result;
        });
      };

      _.each(dimStoreService.getStores(), function(store) {
        _.each(store.items, function(item) {
          item.visible = (filters.length > 0) ? filterFn(item) : true;
        });

        // Filter vendor items
        _.each(store.vendors, function(vendor) {
          _.each(vendor.items.armor, function(item) {
            item.visible = (filters.length > 0) ? filterFn(item) : true;
          });
          _.each(vendor.items.weapons, function(item) {
            item.visible = (filters.length > 0) ? filterFn(item) : true;
          });
        });
      });
    };

    // Cache for searches against filterTrans. Somewhat noticebly speeds up the lookup on my older Mac, YMMV. Helps
    // make the for(...) loop for filterTrans a little more bearable for the readability tradeoff.
    var _cachedFilters = {};

    /**
     * Filter groups keyed by type check. Key is what the user will search for, e.g.
     * is:complete
     *
     * Value is the checking function
     * @param {String} predicate The predicate - for example, is:arc gets the 'elemental' filter function, with predicate='arc'
     * @param {Object} item The item to test against.
     * @return {Boolean} Returns true for a match, false for a non-match
     */
    var filterFns = {
      dmg: function(predicate, item) {
        return item.dmg === predicate;
      },
      type: function(predicate, item) {
        return item.type.toLowerCase() === predicate;
      },
      tier: function(predicate, item) {
        const tierMap = {
          white: 'common',
          green: 'uncommon',
          blue: 'rare',
          purple: 'legendary',
          yellow: 'exotic'
        };
        return item.tier.toLowerCase() === (tierMap[predicate] || predicate);
      },
      // Incomplete will show items that are not fully leveled.
      incomplete: function(predicate, item) {
        return item.talentGrid &&
          !item.complete;
      },
      // Complete shows items that are fully leveled.
      complete: function(predicate, item) {
        return item.complete;
      },
      // Upgraded will show items that have enough XP to unlock all
      // their nodes and only need the nodes to be purchased.
      upgraded: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.xpComplete &&
          !item.complete;
      },
      xpincomplete: function(predicate, item) {
        return item.talentGrid &&
          !item.talentGrid.xpComplete;
      },
      xpcomplete: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.xpComplete;
      },
      ascended: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.hasAscendNode &&
          item.talentGrid.ascended;
      },
      unascended: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.hasAscendNode &&
          !item.talentGrid.ascended;
      },
      reforgeable: function(predicate, item) {
        return item.talentGrid && _.any(item.talentGrid.nodes, { name: 'Reforge Ready' });
      },
      untracked: function(predicate, item) {
        return item.trackable &&
          !item.tracked;
      },
      tracked: function(predicate, item) {
        return item.trackable &&
          item.tracked;
      },
      unlocked: function(predicate, item) {
        return item.lockable &&
          !item.locked;
      },
      locked: function(predicate, item) {
        return item.lockable &&
          item.locked;
      },
      dupe: function(predicate, item) {
        if (_duplicates === null) {
          _duplicates = _.chain(dimStoreService.getStores())
            .pluck('items')
            .flatten()
            .countBy('hash')
            .value();
        }

        return _duplicates[item.hash] > 1;
      },
      classType: function(predicate, item) {
        var value;

        switch (predicate) {
        case 'titan':
          value = 0;
          break;
        case 'hunter':
          value = 1;
          break;
        case 'warlock':
          value = 2;
          break;
        }

        return (item.classType === value);
      },
      glimmer: function(predicate, item) {
        var boosts = [
          1043138475, // -black-wax-idol
          1772853454, // -blue-polyphage
          3783295803, // -ether-seeds
          3446457162  // -resupply-codes
        ];
        var supplies = [
          269776572, // -house-banners
          3632619276, // -silken-codex
          2904517731, // -axiomatic-beads
          1932910919 // -network-keys
        ];

        switch (predicate) {
        case 'glimmerboost':
          return boosts.includes(item.hash);
        case 'glimmersupply':
          return supplies.includes(item.hash);
        case 'glimmeritem':
          return boosts.includes(item.hash) || supplies.includes(item.hash);
        }
        return false;
      },
      stattype: function(predicate, item) {
        return item.stats && _.any(item.stats, function(s) { return s.name.toLowerCase() === predicate && s.value > 0; });
      },
      stackable: function(predicate, item) {
        return item.maxStackSize > 1;
      },
      engram: function(predicate, item) {
        return item.isEngram();
      },
      infusable: function(predicate, item) {
        return item.talentGrid && item.talentGrid.infusable;
      },
      weaponClass: function(predicate, item) {
        return predicate.toLowerCase().replace(/\s/g, '') === item.weaponClass;
      },
      keyword: function(predicate, item) {
        return item.name.toLowerCase().indexOf(predicate) >= 0 ||
          // Search perks as well
          (item.talentGrid && _.any(item.talentGrid.nodes, function(node) {
            // Fixed #798 by searching on the description too.
            return (node.name + ' ' + node.description).toLowerCase().indexOf(predicate) >= 0;
          }));
      },
      light: function(predicate, item) {
        if (predicate.length === 0 || item.primStat === undefined) {
          return false;
        }

        var operands = ['<=', '>=', '=', '>', '<'];
        var operand = 'none';
        var result = false;

        operands.forEach(function(element) {
          if (predicate.substring(0, element.length) === element) {
            operand = element;
            predicate = predicate.substring(element.length);
            return false;
          } else {
            return true;
          }
        }, this);

        predicate = parseInt(predicate, 10);

        switch (operand) {
        case 'none':
          result = (item.primStat.value === predicate);
          break;
        case '=':
          result = (item.primStat.value === predicate);
          break;
        case '<':
          result = (item.primStat.value < predicate);
          break;
        case '<=':
          result = (item.primStat.value <= predicate);
          break;
        case '>':
          result = (item.primStat.value > predicate);
          break;
        case '>=':
          result = (item.primStat.value >= predicate);
          break;
        }
        return result;
      },
      quality: function(predicate, item) {
        if (predicate.length === 0 || item.quality === undefined || item.quality === null) {
          return false;
        }

        var operands = ['<=', '>=', '=', '>', '<'];
        var operand = 'none';
        var result = false;

        operands.forEach(function(element) {
          if (predicate.substring(0, element.length) === element) {
            operand = element;
            predicate = predicate.substring(element.length);
            return false;
          } else {
            return true;
          }
        }, this);

        predicate = parseInt(predicate, 10);

        switch (operand) {
        case 'none':
          result = (item.quality.min === predicate);
          break;
        case '=':
          result = (item.quality.min === predicate);
          break;
        case '<':
          result = (item.quality.min < predicate);
          break;
        case '<=':
          result = (item.quality.min <= predicate);
          break;
        case '>':
          result = (item.quality.min > predicate);
          break;
        case '>=':
          result = (item.quality.min >= predicate);
          break;
        }
        return result;
      },
      year: function(predicate, item) {
        if (predicate === 'year1') {
          return item.year === 1;
        } else if (predicate === 'year2') {
          return item.year === 2;
        } else {
          return false;
        }
      },
      new: function(predicate, item) {
        return item.isNew;
      }
    };
  }
})();
<commit_msg>Update search filtering function for all item types<commit_after>(function() {
  'use strict';

  angular.module('dimApp')
    .factory('dimSearchService', function() {
      return {
        query: ''
      };
    })
    .directive('dimSearchFilter', SearchFilter);

  SearchFilter.$inject = [];

  function SearchFilter() {
    return {
      controller: SearchFilterCtrl,
      controllerAs: 'vm',
      link: Link,
      bindToController: true,
      restrict: 'A',
      template: [
        '<input id="filter-input" placeholder="Search item/perk or is:arc" type="search" name="filter" ng-model="vm.search.query" ng-model-options="{ debounce: 500 }" ng-trim="true">'
      ].join('')
    };
  }

  /**
   * Filter translation sets. Left-hand is the filter to run from filterFns, right side are possible filterResult
   * values that will set the left-hand to the "match."
   */
  var filterTrans = {
    dmg: ['arc', 'solar', 'void', 'kinetic'],
    type: ['primary', 'special', 'heavy', 'helmet', 'leg', 'gauntlets', 'chest', 'class', 'classitem', 'artifact', 'ghost', 'horn', 'consumable', 'ship', 'material', 'vehicle', 'emblem', 'bounties', 'quests', 'messages', 'missions', 'emote'],
    tier: ['common', 'uncommon', 'rare', 'legendary', 'exotic', 'white', 'green', 'blue', 'purple', 'yellow'],
    incomplete: ['incomplete'],
    complete: ['complete'],
    xpcomplete: ['xpcomplete'],
    xpincomplete: ['xpincomplete', 'needsxp'],
    upgraded: ['upgraded'],
    classType: ['titan', 'hunter', 'warlock'],
    dupe: ['dupe', 'duplicate'],
    unascended: ['unascended', 'unassended', 'unasscended'],
    ascended: ['ascended', 'assended', 'asscended'],
    reforgeable: ['reforgeable', 'reforge', 'rerollable', 'reroll'],
    tracked: ['tracked'],
    untracked: ['untracked'],
    locked: ['locked'],
    unlocked: ['unlocked'],
    stackable: ['stackable'],
    engram: ['engram'],
    weaponClass: ['pulserifle', 'scoutrifle', 'handcannon', 'autorifle', 'primaryweaponengram', 'sniperrifle', 'shotgun', 'fusionrifle', 'specialweaponengram', 'rocketlauncher', 'machinegun', 'heavyweaponengram', 'sidearm', 'sword'],
    year: ['year1', 'year2'],
    infusable: ['infusable', 'infuse'],
    stattype: ['intellect', 'discipline', 'strength'],
    new: ['new'],
    glimmer: ['glimmeritem', 'glimmerboost', 'glimmersupply']
  };

  var keywords = _.flatten(_.flatten(_.values(filterTrans)).map(function(word) {
    return ["is:" + word, "not:" + word];
  }));

  // Filters that operate on ranges (>, <, >=, <=)
  var ranges = ['light', 'level', 'quality', 'percentage'];
  ranges.forEach(function(range) {
    keywords.push(range + ":<", range + ":>", range + ":<=", range + ":>=");
  });

  function Link(scope, element) {
    element.find('input').textcomplete([
      {
        words: keywords,
        match: /\b((li|le|qu|pe|is:|not:)\w*)$/,
        search: function(term, callback) {
          callback($.map(this.words, function(word) {
            return word.indexOf(term) === 0 ? word : null;
          }));
        },
        index: 1,
        replace: function(word) {
          return (word.indexOf('is:') === 0 && word.indexOf('not:') === 0)
            ? (word + ' ') : word;
        }
      }
    ], {
      zIndex: 1000
    });
  }

  SearchFilterCtrl.$inject = ['$scope', 'dimStoreService', 'dimSearchService'];

  function SearchFilterCtrl($scope, dimStoreService, dimSearchService) {
    var vm = this;
    var filterInputSelector = '#filter-input';
    var _duplicates = null; // Holds a map from item hash to count of occurrances of that hash

    vm.search = dimSearchService;

    $scope.$watch('vm.search.query', function() {
      vm.filter();
    });

    $scope.$on('dim-stores-updated', function() {
      _duplicates = null;
      vm.filter();
    });

    // Something has changed that could invalidate filters
    $scope.$on('dim-filter-invalidate', function() {
      _duplicates = null;
      vm.filter();
    });

    $scope.$on('dim-focus-filter-input', function() {
      vm.focusFilterInput();
    });

    $scope.$on('dim-escape-filter-input', function() {
      vm.blurFilterInputIfEmpty();
      vm.clearFilter();
    });

    $scope.$on('dim-clear-filter-input', function() {
      vm.clearFilter();
    });

    vm.blurFilterInputIfEmpty = function() {
      if (vm.search.query === "") {
        vm.blurFilterInput();
      }
    };

    vm.focusFilterInput = function() {
      $(filterInputSelector).focus();
    };

    vm.blurFilterInput = function() {
      $(filterInputSelector).blur();
    };

    vm.clearFilter = function() {
      vm.search.query = "";
      vm.filter();
    };

    vm.filter = function() {
      var filterValue = (vm.search.query) ? vm.search.query.toLowerCase() : '';
      filterValue = filterValue.replace(/\s+and\s+/, ' ');
      var searchTerms = filterValue.split(/\s+/);
      var filter;
      var predicate = '';
      var filterFn;
      var filters = [];

      function addPredicate(predicate, filter, invert = false) {
        filters.push({ predicate: predicate, value: filter, invert: invert });
      }

      _.each(searchTerms, function(term) {
        if (term.indexOf('is:') >= 0) {
          filter = term.replace('is:', '');
          if (_cachedFilters[filter]) {
            predicate = _cachedFilters[filter];
            addPredicate(predicate, filter);
          } else {
            for (const key in filterTrans) {
              if (filterTrans.hasOwnProperty(key) && filterTrans[key].indexOf(filter) > -1) {
                predicate = key;
                _cachedFilters[filter] = key;
                addPredicate(predicate, filter);
                break;
              }
            }
          }
        } else if (term.indexOf('not:') >= 0) {
          filter = term.replace('not:', '');
          if (_cachedFilters[filter]) {
            predicate = _cachedFilters[filter];
            addPredicate(predicate, filter, true);
          } else {
            for (const key in filterTrans) {
              if (filterTrans.hasOwnProperty(key) && filterTrans[key].indexOf(filter) > -1) {
                predicate = key;
                _cachedFilters[filter] = key;
                addPredicate(predicate, filter, true);
                break;
              }
            }
          }
        } else if (term.indexOf('light:') >= 0 || term.indexOf('level:') >= 0) {
          filter = term.replace('light:', '').replace('level:', '');
          addPredicate("light", filter);
        } else if (term.indexOf('quality:') >= 0 || term.indexOf('percentage:') >= 0) {
          filter = term.replace('quality:', '').replace('percentage:', '');
          addPredicate("quality", filter);
        } else if (!/^\s*$/.test(term)) {
          addPredicate("keyword", term);
        }
      });

      filterFn = function(item) {
        return _.all(filters, function(filter) {
          var result = filterFns[filter.predicate](filter.value, item);
          return filter.invert ? !result : result;
        });
      };

      _.each(dimStoreService.getStores(), function(store) {
        _.each(store.items, function(item) {
          item.visible = (filters.length > 0) ? filterFn(item) : true;
        });

        // Filter vendor items
        _.each(store.vendors, function(vendor) {
          _.each(vendor.items, function(items, type) {
            _.each(items, function(item) {
              item.visible = (filters.length > 0) ? filterFn(item) : true;
            });
          });
        });
      });
    };

    // Cache for searches against filterTrans. Somewhat noticebly speeds up the lookup on my older Mac, YMMV. Helps
    // make the for(...) loop for filterTrans a little more bearable for the readability tradeoff.
    var _cachedFilters = {};

    /**
     * Filter groups keyed by type check. Key is what the user will search for, e.g.
     * is:complete
     *
     * Value is the checking function
     * @param {String} predicate The predicate - for example, is:arc gets the 'elemental' filter function, with predicate='arc'
     * @param {Object} item The item to test against.
     * @return {Boolean} Returns true for a match, false for a non-match
     */
    var filterFns = {
      dmg: function(predicate, item) {
        return item.dmg === predicate;
      },
      type: function(predicate, item) {
        return item.type.toLowerCase() === predicate;
      },
      tier: function(predicate, item) {
        const tierMap = {
          white: 'common',
          green: 'uncommon',
          blue: 'rare',
          purple: 'legendary',
          yellow: 'exotic'
        };
        return item.tier.toLowerCase() === (tierMap[predicate] || predicate);
      },
      // Incomplete will show items that are not fully leveled.
      incomplete: function(predicate, item) {
        return item.talentGrid &&
          !item.complete;
      },
      // Complete shows items that are fully leveled.
      complete: function(predicate, item) {
        return item.complete;
      },
      // Upgraded will show items that have enough XP to unlock all
      // their nodes and only need the nodes to be purchased.
      upgraded: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.xpComplete &&
          !item.complete;
      },
      xpincomplete: function(predicate, item) {
        return item.talentGrid &&
          !item.talentGrid.xpComplete;
      },
      xpcomplete: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.xpComplete;
      },
      ascended: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.hasAscendNode &&
          item.talentGrid.ascended;
      },
      unascended: function(predicate, item) {
        return item.talentGrid &&
          item.talentGrid.hasAscendNode &&
          !item.talentGrid.ascended;
      },
      reforgeable: function(predicate, item) {
        return item.talentGrid && _.any(item.talentGrid.nodes, { name: 'Reforge Ready' });
      },
      untracked: function(predicate, item) {
        return item.trackable &&
          !item.tracked;
      },
      tracked: function(predicate, item) {
        return item.trackable &&
          item.tracked;
      },
      unlocked: function(predicate, item) {
        return item.lockable &&
          !item.locked;
      },
      locked: function(predicate, item) {
        return item.lockable &&
          item.locked;
      },
      dupe: function(predicate, item) {
        if (_duplicates === null) {
          _duplicates = _.chain(dimStoreService.getStores())
            .pluck('items')
            .flatten()
            .countBy('hash')
            .value();
        }

        return _duplicates[item.hash] > 1;
      },
      classType: function(predicate, item) {
        var value;

        switch (predicate) {
        case 'titan':
          value = 0;
          break;
        case 'hunter':
          value = 1;
          break;
        case 'warlock':
          value = 2;
          break;
        }

        return (item.classType === value);
      },
      glimmer: function(predicate, item) {
        var boosts = [
          1043138475, // -black-wax-idol
          1772853454, // -blue-polyphage
          3783295803, // -ether-seeds
          3446457162  // -resupply-codes
        ];
        var supplies = [
          269776572, // -house-banners
          3632619276, // -silken-codex
          2904517731, // -axiomatic-beads
          1932910919 // -network-keys
        ];

        switch (predicate) {
        case 'glimmerboost':
          return boosts.includes(item.hash);
        case 'glimmersupply':
          return supplies.includes(item.hash);
        case 'glimmeritem':
          return boosts.includes(item.hash) || supplies.includes(item.hash);
        }
        return false;
      },
      stattype: function(predicate, item) {
        return item.stats && _.any(item.stats, function(s) { return s.name.toLowerCase() === predicate && s.value > 0; });
      },
      stackable: function(predicate, item) {
        return item.maxStackSize > 1;
      },
      engram: function(predicate, item) {
        return item.isEngram();
      },
      infusable: function(predicate, item) {
        return item.talentGrid && item.talentGrid.infusable;
      },
      weaponClass: function(predicate, item) {
        return predicate.toLowerCase().replace(/\s/g, '') === item.weaponClass;
      },
      keyword: function(predicate, item) {
        return item.name.toLowerCase().indexOf(predicate) >= 0 ||
          // Search perks as well
          (item.talentGrid && _.any(item.talentGrid.nodes, function(node) {
            // Fixed #798 by searching on the description too.
            return (node.name + ' ' + node.description).toLowerCase().indexOf(predicate) >= 0;
          }));
      },
      light: function(predicate, item) {
        if (predicate.length === 0 || item.primStat === undefined) {
          return false;
        }

        var operands = ['<=', '>=', '=', '>', '<'];
        var operand = 'none';
        var result = false;

        operands.forEach(function(element) {
          if (predicate.substring(0, element.length) === element) {
            operand = element;
            predicate = predicate.substring(element.length);
            return false;
          } else {
            return true;
          }
        }, this);

        predicate = parseInt(predicate, 10);

        switch (operand) {
        case 'none':
          result = (item.primStat.value === predicate);
          break;
        case '=':
          result = (item.primStat.value === predicate);
          break;
        case '<':
          result = (item.primStat.value < predicate);
          break;
        case '<=':
          result = (item.primStat.value <= predicate);
          break;
        case '>':
          result = (item.primStat.value > predicate);
          break;
        case '>=':
          result = (item.primStat.value >= predicate);
          break;
        }
        return result;
      },
      quality: function(predicate, item) {
        if (predicate.length === 0 || item.quality === undefined || item.quality === null) {
          return false;
        }

        var operands = ['<=', '>=', '=', '>', '<'];
        var operand = 'none';
        var result = false;

        operands.forEach(function(element) {
          if (predicate.substring(0, element.length) === element) {
            operand = element;
            predicate = predicate.substring(element.length);
            return false;
          } else {
            return true;
          }
        }, this);

        predicate = parseInt(predicate, 10);

        switch (operand) {
        case 'none':
          result = (item.quality.min === predicate);
          break;
        case '=':
          result = (item.quality.min === predicate);
          break;
        case '<':
          result = (item.quality.min < predicate);
          break;
        case '<=':
          result = (item.quality.min <= predicate);
          break;
        case '>':
          result = (item.quality.min > predicate);
          break;
        case '>=':
          result = (item.quality.min >= predicate);
          break;
        }
        return result;
      },
      year: function(predicate, item) {
        if (predicate === 'year1') {
          return item.year === 1;
        } else if (predicate === 'year2') {
          return item.year === 2;
        } else {
          return false;
        }
      },
      new: function(predicate, item) {
        return item.isNew;
      }
    };
  }
})();
<|endoftext|>


<commit_before>import loaderIcon from '../images/loaders/default.svg';
import Waypoint from 'react-waypoint';
import getPlaceholderImageURL from '../util/getPlaceholderImageURL';
import EpisodeListItem from './EpisodeListItem';
import Img from 'react-image';
import Popover from 'react-popover';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import fetch from '../util/fetch';
import moment from 'moment';
import { getPinnedEpisodes } from '../util/pins';
import { getFeed } from '../util/feeds';
import Loader from './Loader';

class PodcastEpisodesView extends React.Component {
	constructor(props) {
		super(props);
		this.toggleFollowPodcast = this.toggleFollowPodcast.bind(this);
		this.toggleMenu = this.toggleMenu.bind(this);
		this.state = {
			episodeCursor: 1, // mongoose-api-query starts pages at 1, not 0
			menuIsOpen: false,
			sortBy: 'latest',
			newEpisodesAvailable: false,
		};
	}
	subscribeToStreamFeed(podcastID, streamFeedToken) {
		this.subscription = window.streamClient
			.feed('podcast', podcastID, streamFeedToken)
			.subscribe(data => {
				console.log(data);
			});
	}

	unsubscribeFromStreamFeed() {
		this.subscription.cancel();
	}

	componentDidMount() {
		this.props.getPodcast(this.props.match.params.podcastID);
		this.getEpisodes(this.props.match.params.podcastID);
		getPinnedEpisodes(this.props.dispatch);
		getFeed(this.props.dispatch, 'episode', 0, 20); // this is to populate 'recent' state indicators
		// subscribe to feed updates
		if (this.props.podcast) {
			this.subscribeToStreamFeed(
				this.props.podcast._id,
				this.props.podcast.streamToken,
			);
		}
	}
	componentWillReceiveProps(nextProps) {
		if (nextProps.match.params.podcastID !== this.props.match.params.podcastID) {
			// essentially, if we've just switched views from one podcast to another
			this.setState(
				{
					episodeCursor: 1, // mongoose-api-query starts pages at 1, not 0
				},
				() => {
					this.props.getPodcast(nextProps.match.params.podcastID);
					this.getEpisodes(nextProps.match.params.podcastID);
					getPinnedEpisodes(this.props.dispatch);
					getFeed(this.props.dispatch, 'episode', 0, 20); // this is to populate 'recent' state indicators
				},
			);
			// if we're switching podcasts, unsubscribe from the old podcast feed, and subscribe to the new podcast feed
			this.unsubscribeFromStreamFeed();
			this.subscribeToStreamFeed(
				nextProps.podcast._id,
				nextProps.podcast.streamToken,
			);
		}
		// if we didn't have a podcast before, subscribe to the new podcast feed
		if (!this.props.podcast && nextProps.podcast) {
			this.subscribeToStreamFeed(
				nextProps.podcast._id,
				nextProps.podcast.streamToken,
			);
		}
	}

	componentWillUnmount() {
		this.unsubscribeFromStreamFeed();
	}

	getEpisodes(forPodcastID) {
		fetch(
			'GET',
			'/episodes',
			{},
			{
				page: this.state.episodeCursor,
				per_page: 10,
				podcast: forPodcastID,
				sort_by: 'publicationDate,desc',
			},
		)
			.then(res => {
				if (res.data.length === 0) {
					this.setState({
						reachedEndOfFeed: true,
					});
				}
				this.props.dispatch({
					episodes: res.data,
					type: 'BATCH_UPDATE_EPISODES',
				});
			})
			.catch(err => {
				console.log(err); // eslint-disable-line no-console
			});
	}

	toggleFollowPodcast() {
		if (this.props.isFollowing) {
			this.props.unfollowPodcast();
		} else {
			this.props.followPodcast();
		}
	}

	toggleMenu() {
		this.setState({
			menuIsOpen: !this.state.menuIsOpen,
		});
	}

	render() {
		if (!this.props.podcast) {
			return <Loader />;
		}

		let sortedEpisodes = [...this.props.episodes];
		sortedEpisodes.sort((a, b) => {
			return (
				moment(b.publicationDate).valueOf() - moment(a.publicationDate).valueOf()
			);
		});

		let menuContent = (

 {
							if (this.props.isFollowing) {
								this.props.unfollowPodcast();
							} else {
								this.props.followPodcast();
							}
						}}
					>
						{this.props.isFollowing ? 'Unfollow' : 'Follow'}

		);

		let rightColumn;

		if (sortedEpisodes.length === 0) {
			rightColumn = (

{'We haven\'t found any episodes for this podcast feed yet :('}

						{
							'It might be because the podcast feed doesn\'t have any episodes, or because it just got added and we\'re still parsing them. Come check back in a few minutes.'
						}

						{
							'If you\'re pretty sure there\'s supposed to be some episodes here, and they aren\'t showing up, please file a '
						}

							GitHub Issue
						.

			);
		} else {
			rightColumn = (

					{sortedEpisodes.map((episode, i) => {
						let active = false;
						if (
							this.props.context.contextID ===
								this.props.match.params.podcastID &&
							i === this.props.context.contextPosition
						) {
							active = true;
						}
						return (
							<EpisodeListItem
								active={active}
								key={episode._id}
								pinEpisode={() => {
									this.props.pinEpisode(episode._id);
								}}
								playOrPauseEpisode={() => {
									if (active && this.props.context.playing) {
										this.props.pauseEpisode();
									} else if (active) {
										this.props.resumeEpisode();
									} else {
										this.props.playEpisode(episode._id, i);
									}
								}}
								playable={true}
								playing={this.props.context.playing}
								position={i}
								unpinEpisode={() => {
									this.props.unpinEpisode(episode.pinID, episode._id);
								}}
								{...episode}
							/>
						);
					})}
					{this.state.reachedEndOfFeed ? (

{'That\'s it! No more episodes here.'}

								{
									'What, did you think that once you got all the way around, you\'d just be back at the same place that you started? Sounds like some real round-feed thinking to me.'
								}

					) : (

							<Waypoint
								onEnter={() => {
									this.setState(
										{
											episodeCursor: this.state.episodeCursor + 1,
										},
										() => {
											this.getEpisodes(
												this.props.match.params.podcastID,
											);
										},
									);
								}}
							/>

					)}

			);
		}

		return (
			<React.Fragment>

{this.props.podcast.title}

							<Popover
								body={menuContent}
								isOpen={this.state.menuIsOpen}
								onOuterAction={this.toggleMenu}
								place="below"
								tipSize={0.1}
							>

							</Popover>

{rightColumn}

			</React.Fragment>
		);
	}
}

PodcastEpisodesView.defaultProps = {
	episodes: [],
	isFollowing: false,
};

PodcastEpisodesView.propTypes = {
	context: PropTypes.shape({
		contextID: PropTypes.string,
		contextPosition: PropTypes.number,
		playing: PropTypes.bool,
	}),
	dispatch: PropTypes.func.isRequired,
	episodes: PropTypes.array,
	followPodcast: PropTypes.func.isRequired,
	getPodcast: PropTypes.func.isRequired,
	isFollowing: PropTypes.bool,
	match: PropTypes.shape({
		params: PropTypes.shape({
			podcastID: PropTypes.string,
		}),
	}),
	pauseEpisode: PropTypes.func.isRequired,
	pinEpisode: PropTypes.func.isRequired,
	playEpisode: PropTypes.func.isRequired,
	podcast: PropTypes.shape({
		_id: PropTypes.string,
		images: PropTypes.shape({
			favicon: PropTypes.string,
			featured: PropTypes.string,
			og: PropTypes.string,
		}),
		title: PropTypes.string,
		streamToken: PropTypes.string.isRequired,
	}),
	resumeEpisode: PropTypes.func.isRequired,
	unfollowPodcast: PropTypes.func.isRequired,
	unpinEpisode: PropTypes.func.isRequired,
};

const mapStateToProps = (state, ownProps) => {
	let isFollowing = false;
	let podcastID = ownProps.match.params.podcastID;

	if (
		state.followedPodcasts &&
		state.followedPodcasts[localStorage['authedUser']] &&
		state.followedPodcasts[localStorage['authedUser']][podcastID]
	) {
		isFollowing = true;
	}
	let podcast = null;
	if (state.podcasts && state.podcasts[podcastID]) {
		podcast = { ...state.podcasts[podcastID] };
	}
	let episodes = [];
	if (state.episodes) {
		for (let episode of Object.keys(state.episodes)) {
			if (state.episodes[episode].podcast === podcastID) {
				episodes.push({ ...state.episodes[episode] });
			}
		}
	}
	for (let episode of episodes) {
		// attach pinned state
		if (state.pinnedEpisodes && state.pinnedEpisodes[episode._id]) {
			episode.pinned = true;
			episode.pinID = state.pinnedEpisodes[episode._id]._id;
		} else {
			episode.pinned = false;
		}

		if (state.feeds && state.feeds[`user_episode:${localStorage['authedUser']}`]) {
			if (
				state.feeds[`user_episode:${localStorage['authedUser']}`].indexOf(
					episode._id,
				) < 20 &&
				state.feeds[`user_episode:${localStorage['authedUser']}`].indexOf(
					episode._id,
				) !== -1
			) {
				episode.recent = true;
			} else {
				episode.recent = false;
			}
		}

		// attach podcast
		episode.podcast = { ...podcast };
	}
	let context = { ...state.player };
	return {
		context,
		episodes,
		isFollowing,
		podcast,
	};
};

const mapDispatchToProps = (dispatch, ownProps) => {
	let podcastID = ownProps.match.params.podcastID;
	return {
		dispatch,
		followPodcast: () => {
			// optimistic dispatch
			// dispatch updated follow relationship
			dispatch({
				podcastID,
				type: 'FOLLOW_PODCAST',
				userID: localStorage['authedUser'],
			});
			// already have podcastID
			fetch(
				'POST',
				'/follows',
				{},
				{
					podcast: podcastID,
					type: 'podcast',
				},
			).catch(err => {
				console.log(err); // eslint-disable-line no-console
				dispatch({
					podcastID,
					type: 'UNFOLLOW_PODCAST',
					userID: localStorage['authedUser'],
				});
			});
		},
		getPodcast: podcastID => {
			fetch('GET', `/podcasts/${podcastID}`)
				.then(res => {
					dispatch({
						podcast: res.data,
						type: 'UPDATE_PODCAST_SHOW',
					});
				})
				.catch(err => {
					console.log(err); // eslint-disable-line no-console
				});
		},
		pauseEpisode: () => {
			dispatch({ type: 'PAUSE_EPISODE' });
		},
		pinEpisode: episodeID => {
			fetch('POST', '/pins', {
				episode: episodeID,
			})
				.then(response => {
					dispatch({
						pin: response.data,
						type: 'PIN_EPISODE',
					});
				})
				.catch(err => {
					console.log(err); // eslint-disable-line no-console
				});
		},
		playEpisode: (episodeID, position) => {
			dispatch({
				contextID: podcastID,
				contextPosition: position,
				contextType: 'podcast',
				episodeID: episodeID,
				playing: true,
				type: 'PLAY_EPISODE',
			});
		},
		resumeEpisode: () => {
			dispatch({ type: 'RESUME_EPISODE' });
		},
		unfollowPodcast: () => {
			// optimistic dispatch
			// dispatch updated follow relationship
			dispatch({
				podcastID,
				type: 'UNFOLLOW_PODCAST',
				userID: localStorage['authedUser'],
			});

			fetch('DELETE', '/follows', null, {
				podcast: podcastID,
				type: 'podcast',
			}).catch(err => {
				console.log(err); // eslint-disable-line no-console
				dispatch({
					podcastID,
					type: 'FOLLOW_PODCAST',
					userID: localStorage['authedUser'],
				});
			});
		},
		unpinEpisode: (pinID, episodeID) => {
			fetch('DELETE', `/pins/${pinID}`)
				.then(() => {
					dispatch({
						episodeID,
						type: 'UNPIN_EPISODE',
					});
				})
				.catch(err => {
					console.log(err); // eslint-disable-line no-console
				});
		},
	};
};

export default connect(mapStateToProps, mapDispatchToProps)(PodcastEpisodesView);
<commit_msg>adding 'click to refresh' toast at top of episode list<commit_after>import loaderIcon from '../images/loaders/default.svg';
import Waypoint from 'react-waypoint';
import getPlaceholderImageURL from '../util/getPlaceholderImageURL';
import EpisodeListItem from './EpisodeListItem';
import Img from 'react-image';
import Popover from 'react-popover';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import fetch from '../util/fetch';
import moment from 'moment';
import { getPinnedEpisodes } from '../util/pins';
import { getFeed } from '../util/feeds';
import Loader from './Loader';

class PodcastEpisodesView extends React.Component {
	constructor(props) {
		super(props);
		this.toggleFollowPodcast = this.toggleFollowPodcast.bind(this);
		this.toggleMenu = this.toggleMenu.bind(this);
		this.state = {
			episodeCursor: 1, // mongoose-api-query starts pages at 1, not 0
			menuIsOpen: false,
			sortBy: 'latest',
			newEpisodesAvailable: true,
		};
	}
	subscribeToStreamFeed(podcastID, streamFeedToken) {
		this.subscription = window.streamClient
			.feed('podcast', podcastID, streamFeedToken)
			.subscribe(data => {
				console.log(data);
				this.setState({
					newEpisodesAvailable: true,
				});
			});
	}

	unsubscribeFromStreamFeed() {
		this.subscription.cancel();
	}

	componentDidMount() {
		this.props.getPodcast(this.props.match.params.podcastID);
		this.getEpisodes(this.props.match.params.podcastID);
		getPinnedEpisodes(this.props.dispatch);
		getFeed(this.props.dispatch, 'episode', 0, 20); // this is to populate 'recent' state indicators
		// subscribe to feed updates
		if (this.props.podcast) {
			this.subscribeToStreamFeed(
				this.props.podcast._id,
				this.props.podcast.streamToken,
			);
		}
	}
	componentWillReceiveProps(nextProps) {
		if (nextProps.match.params.podcastID !== this.props.match.params.podcastID) {
			// essentially, if we've just switched views from one podcast to another
			this.setState(
				{
					episodeCursor: 1, // mongoose-api-query starts pages at 1, not 0
				},
				() => {
					this.props.getPodcast(nextProps.match.params.podcastID);
					this.getEpisodes(nextProps.match.params.podcastID);
					getPinnedEpisodes(this.props.dispatch);
					getFeed(this.props.dispatch, 'episode', 0, 20); // this is to populate 'recent' state indicators
				},
			);
			// if we're switching podcasts, unsubscribe from the old podcast feed, and subscribe to the new podcast feed
			this.unsubscribeFromStreamFeed();
			this.subscribeToStreamFeed(
				nextProps.podcast._id,
				nextProps.podcast.streamToken,
			);
		}
		// if we didn't have a podcast before, subscribe to the new podcast feed
		if (!this.props.podcast && nextProps.podcast) {
			this.subscribeToStreamFeed(
				nextProps.podcast._id,
				nextProps.podcast.streamToken,
			);
		}
	}

	componentWillUnmount() {
		this.unsubscribeFromStreamFeed();
	}

	getEpisodes(forPodcastID) {
		fetch(
			'GET',
			'/episodes',
			{},
			{
				page: this.state.episodeCursor,
				per_page: 10,
				podcast: forPodcastID,
				sort_by: 'publicationDate,desc',
			},
		)
			.then(res => {
				if (res.data.length === 0) {
					this.setState({
						reachedEndOfFeed: true,
					});
				}
				this.props.dispatch({
					episodes: res.data,
					type: 'BATCH_UPDATE_EPISODES',
				});
			})
			.catch(err => {
				console.log(err); // eslint-disable-line no-console
			});
	}

	toggleFollowPodcast() {
		if (this.props.isFollowing) {
			this.props.unfollowPodcast();
		} else {
			this.props.followPodcast();
		}
	}

	toggleMenu() {
		this.setState({
			menuIsOpen: !this.state.menuIsOpen,
		});
	}

	render() {
		if (!this.props.podcast) {
			return <Loader />;
		}

		let sortedEpisodes = [...this.props.episodes];
		sortedEpisodes.sort((a, b) => {
			return (
				moment(b.publicationDate).valueOf() - moment(a.publicationDate).valueOf()
			);
		});

		let menuContent = (

 {
							if (this.props.isFollowing) {
								this.props.unfollowPodcast();
							} else {
								this.props.followPodcast();
							}
						}}
					>
						{this.props.isFollowing ? 'Unfollow' : 'Follow'}

		);

		let rightColumn;

		if (sortedEpisodes.length === 0) {
			rightColumn = (

{'We haven\'t found any episodes for this podcast feed yet :('}

						{
							'It might be because the podcast feed doesn\'t have any episodes, or because it just got added and we\'re still parsing them. Come check back in a few minutes.'
						}

						{
							'If you\'re pretty sure there\'s supposed to be some episodes here, and they aren\'t showing up, please file a '
						}

							GitHub Issue
						.

			);
		} else {
			rightColumn = (

					{sortedEpisodes.map((episode, i) => {
						let active = false;
						if (
							this.props.context.contextID ===
								this.props.match.params.podcastID &&
							i === this.props.context.contextPosition
						) {
							active = true;
						}
						return (
							<EpisodeListItem
								active={active}
								key={episode._id}
								pinEpisode={() => {
									this.props.pinEpisode(episode._id);
								}}
								playOrPauseEpisode={() => {
									if (active && this.props.context.playing) {
										this.props.pauseEpisode();
									} else if (active) {
										this.props.resumeEpisode();
									} else {
										this.props.playEpisode(episode._id, i);
									}
								}}
								playable={true}
								playing={this.props.context.playing}
								position={i}
								unpinEpisode={() => {
									this.props.unpinEpisode(episode.pinID, episode._id);
								}}
								{...episode}
							/>
						);
					})}
					{this.state.reachedEndOfFeed ? (

{'That\'s it! No more episodes here.'}

								{
									'What, did you think that once you got all the way around, you\'d just be back at the same place that you started? Sounds like some real round-feed thinking to me.'
								}

					) : (

							<Waypoint
								onEnter={() => {
									this.setState(
										{
											episodeCursor: this.state.episodeCursor + 1,
										},
										() => {
											this.getEpisodes(
												this.props.match.params.podcastID,
											);
										},
									);
								}}
							/>

					)}

			);
		}

		return (
			<React.Fragment>

{this.props.podcast.title}

							<Popover
								body={menuContent}
								isOpen={this.state.menuIsOpen}
								onOuterAction={this.toggleMenu}
								place="below"
								tipSize={0.1}
							>

							</Popover>

					{this.state.newEpisodesAvailable ? (

 {
								this.props.getPodcast(this.props.match.params.podcastID);
								this.getEpisodes(this.props.match.params.podcastID);
								getPinnedEpisodes(this.props.dispatch);
								getFeed(this.props.dispatch, 'episode', 0, 20); // this is to populate 'recent' state indicators
								this.setState({
									newEpisodesAvailable: false,
								});
							}}
						>
							New episodes available - click to refresh

					) : null}
					{rightColumn}

			</React.Fragment>
		);
	}
}

PodcastEpisodesView.defaultProps = {
	episodes: [],
	isFollowing: false,
};

PodcastEpisodesView.propTypes = {
	context: PropTypes.shape({
		contextID: PropTypes.string,
		contextPosition: PropTypes.number,
		playing: PropTypes.bool,
	}),
	dispatch: PropTypes.func.isRequired,
	episodes: PropTypes.array,
	followPodcast: PropTypes.func.isRequired,
	getPodcast: PropTypes.func.isRequired,
	isFollowing: PropTypes.bool,
	match: PropTypes.shape({
		params: PropTypes.shape({
			podcastID: PropTypes.string,
		}),
	}),
	pauseEpisode: PropTypes.func.isRequired,
	pinEpisode: PropTypes.func.isRequired,
	playEpisode: PropTypes.func.isRequired,
	podcast: PropTypes.shape({
		_id: PropTypes.string,
		images: PropTypes.shape({
			favicon: PropTypes.string,
			featured: PropTypes.string,
			og: PropTypes.string,
		}),
		title: PropTypes.string,
		streamToken: PropTypes.string.isRequired,
	}),
	resumeEpisode: PropTypes.func.isRequired,
	unfollowPodcast: PropTypes.func.isRequired,
	unpinEpisode: PropTypes.func.isRequired,
};

const mapStateToProps = (state, ownProps) => {
	let isFollowing = false;
	let podcastID = ownProps.match.params.podcastID;

	if (
		state.followedPodcasts &&
		state.followedPodcasts[localStorage['authedUser']] &&
		state.followedPodcasts[localStorage['authedUser']][podcastID]
	) {
		isFollowing = true;
	}
	let podcast = null;
	if (state.podcasts && state.podcasts[podcastID]) {
		podcast = { ...state.podcasts[podcastID] };
	}
	let episodes = [];
	if (state.episodes) {
		for (let episode of Object.keys(state.episodes)) {
			if (state.episodes[episode].podcast === podcastID) {
				episodes.push({ ...state.episodes[episode] });
			}
		}
	}
	for (let episode of episodes) {
		// attach pinned state
		if (state.pinnedEpisodes && state.pinnedEpisodes[episode._id]) {
			episode.pinned = true;
			episode.pinID = state.pinnedEpisodes[episode._id]._id;
		} else {
			episode.pinned = false;
		}

		if (state.feeds && state.feeds[`user_episode:${localStorage['authedUser']}`]) {
			if (
				state.feeds[`user_episode:${localStorage['authedUser']}`].indexOf(
					episode._id,
				) < 20 &&
				state.feeds[`user_episode:${localStorage['authedUser']}`].indexOf(
					episode._id,
				) !== -1
			) {
				episode.recent = true;
			} else {
				episode.recent = false;
			}
		}

		// attach podcast
		episode.podcast = { ...podcast };
	}
	let context = { ...state.player };
	return {
		context,
		episodes,
		isFollowing,
		podcast,
	};
};

const mapDispatchToProps = (dispatch, ownProps) => {
	let podcastID = ownProps.match.params.podcastID;
	return {
		dispatch,
		followPodcast: () => {
			// optimistic dispatch
			// dispatch updated follow relationship
			dispatch({
				podcastID,
				type: 'FOLLOW_PODCAST',
				userID: localStorage['authedUser'],
			});
			// already have podcastID
			fetch(
				'POST',
				'/follows',
				{},
				{
					podcast: podcastID,
					type: 'podcast',
				},
			).catch(err => {
				console.log(err); // eslint-disable-line no-console
				dispatch({
					podcastID,
					type: 'UNFOLLOW_PODCAST',
					userID: localStorage['authedUser'],
				});
			});
		},
		getPodcast: podcastID => {
			fetch('GET', `/podcasts/${podcastID}`)
				.then(res => {
					dispatch({
						podcast: res.data,
						type: 'UPDATE_PODCAST_SHOW',
					});
				})
				.catch(err => {
					console.log(err); // eslint-disable-line no-console
				});
		},
		pauseEpisode: () => {
			dispatch({ type: 'PAUSE_EPISODE' });
		},
		pinEpisode: episodeID => {
			fetch('POST', '/pins', {
				episode: episodeID,
			})
				.then(response => {
					dispatch({
						pin: response.data,
						type: 'PIN_EPISODE',
					});
				})
				.catch(err => {
					console.log(err); // eslint-disable-line no-console
				});
		},
		playEpisode: (episodeID, position) => {
			dispatch({
				contextID: podcastID,
				contextPosition: position,
				contextType: 'podcast',
				episodeID: episodeID,
				playing: true,
				type: 'PLAY_EPISODE',
			});
		},
		resumeEpisode: () => {
			dispatch({ type: 'RESUME_EPISODE' });
		},
		unfollowPodcast: () => {
			// optimistic dispatch
			// dispatch updated follow relationship
			dispatch({
				podcastID,
				type: 'UNFOLLOW_PODCAST',
				userID: localStorage['authedUser'],
			});

			fetch('DELETE', '/follows', null, {
				podcast: podcastID,
				type: 'podcast',
			}).catch(err => {
				console.log(err); // eslint-disable-line no-console
				dispatch({
					podcastID,
					type: 'FOLLOW_PODCAST',
					userID: localStorage['authedUser'],
				});
			});
		},
		unpinEpisode: (pinID, episodeID) => {
			fetch('DELETE', `/pins/${pinID}`)
				.then(() => {
					dispatch({
						episodeID,
						type: 'UNPIN_EPISODE',
					});
				})
				.catch(err => {
					console.log(err); // eslint-disable-line no-console
				});
		},
	};
};

export default connect(mapStateToProps, mapDispatchToProps)(PodcastEpisodesView);
<|endoftext|>


<commit_before>import external from '../externalModules.js';
import BaseTool from '../base/BaseTool.js';

import textStyle from '../stateManagement/textStyle.js';
import toolColors from '../stateManagement/toolColors.js';
import getRGBPixels from '../util/getRGBPixels.js';
import calculateSUV from '../util/calculateSUV.js';
import { getNewContext, draw, setShadow, drawCircle } from '../drawing/index.js';
import drawTextBox from '../drawing/drawTextBox.js';

/**
 * @export @public @class
 * @name DragProbeTool
 * @classdesc Tool which provides a probe of the image data at the
 * input position on drag.
 * @extends BaseTool
 */
export default class DragProbeTool extends BaseTool {
  constructor (name = 'DragProbe') {
    const strategies = {
      default: defaultStrategy,
      minimal: minimalStrategy
    };

    super({
      name,
      strategies,
      defaultStrategy: 'default',
      supportedInteractionTypes: ['Mouse', 'Touch'],
      configuration: {}
    });

    this.touchDragCallback = this._movingEventCallback.bind(this);
    this.touchEndCallback = this._endMovingEventCallback.bind(this);

    this.mouseDragCallback = this._movingEventCallback.bind(this);
    this.mouseUpCallback = this._endMovingEventCallback.bind(this);

    this.dragEventData = {};
  }

  _movingEventCallback (evt) {
    const eventData = evt.detail;
    const { element } = eventData;

    this.dragEventData = eventData;
    external.cornerstone.updateImage(element);
  }

  _endMovingEventCallback (evt) {
    const eventData = evt.detail;
    const { element } = eventData;

    this.dragEventData = {};
    external.cornerstone.updateImage(element);
  }

  renderToolData (evt) {
    if (evt && evt.detail && Boolean(Object.keys(this.dragEventData.currentPoints).length)) {
      evt.detail.currentPoints = this.dragEventData.currentPoints;
      this.applyActiveStrategy(evt);
    }
  }
}

/**
 * Default strategy will pick the exactly point of mouse/touch interact and display the probe data.
 *
 * @param  {} evt Image rendered event
 * @param  {} config Tool configuration
 */
const defaultStrategy = (evt, config) => {
  const cornerstone = external.cornerstone;
  const eventData = evt.detail;
  const { element, image, currentPoints, canvasContext } = eventData;

  const context = getNewContext(canvasContext.canvas);

  const color = toolColors.getActiveColor();
  const fontHeight = textStyle.getFontSize();

  const x = Math.round(currentPoints.image.x);
  const y = Math.round(currentPoints.image.y);

  if (x < 0 || y < 0 || x >= image.columns || y >= image.rows) {
    return;
  }

  draw(context, (context) => {
    setShadow(context, config);

    const text = `${x}, ${y}`;
    let storedPixels;
    let str;

    if (image.color) {
      storedPixels = getRGBPixels(element, x, y, 1, 1);
      str = `R: ${storedPixels[0]} G: ${storedPixels[1]} B: ${storedPixels[2]} A: ${storedPixels[3]}`;
    } else {
      storedPixels = cornerstone.getStoredPixels(element, x, y, 1, 1);
      const sp = storedPixels[0];
      const mo = sp * image.slope + image.intercept;
      const suv = calculateSUV(image, sp);

      // Draw text
      str = `SP: ${sp} MO: ${parseFloat(mo.toFixed(3))}`;
      if (suv) {
        str += ` SUV: ${parseFloat(suv.toFixed(3))}`;
      }
    }

    // Draw text 5px away from cursor
    const textCoords = {
      x: currentPoints.canvas.x + 5,
      y: currentPoints.canvas.y - 5
    };

    drawTextBox(context, str, textCoords.x, textCoords.y + fontHeight + 5, color);
    drawTextBox(context, text, textCoords.x, textCoords.y, color);
  });
};

/**
 * Minimal strategy will position a circle and use the center of the circle to calculate and display probe data.
 *
 * @param  {} evt Image rendered event
 * @param  {} config Tool configuration
 */
const minimalStrategy = (evt, config) => {
  const cornerstone = external.cornerstone;
  const eventData = evt.detail;
  const { element, image, currentPoints, canvasContext, isTouchEvent } = eventData;

  const context = getNewContext(canvasContext.canvas);
  const color = toolColors.getActiveColor();

  let pageCoordY = currentPoints.page.y - textStyle.getFontSize() / 2;

  if (isTouchEvent) {
    pageCoordY = currentPoints.page.y - textStyle.getFontSize() * 4;
  }
  const toolCoords = cornerstone.pageToPixel(element, currentPoints.page.x, pageCoordY);

  if (toolCoords.x < 0 || toolCoords.y < 0 ||
    toolCoords.x >= image.columns || toolCoords.y >= image.rows) {
    return;
  }

  draw(context, (context) => {
    setShadow(context, config);

    const seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId);
    const modality = seriesModule && seriesModule.modality;

    let storedPixels;
    let text = '';

    if (image.color) {
      storedPixels = getRGBPixels(element, toolCoords.x, toolCoords.y, 1, 1);
      text = `R: ${storedPixels[0]} G: ${storedPixels[1]} B: ${storedPixels[2]}`;
    } else {
      storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 1, 1);
      const sp = storedPixels[0];
      const mo = sp * image.slope + image.intercept;

      const modalityPixelValueText = parseFloat(mo.toFixed(2));

      if (modality === 'CT') {
        text += `HU: ${modalityPixelValueText}`;
      } else if (modality === 'PT') {
        text += modalityPixelValueText;
        const suv = calculateSUV(image, sp);

        if (suv) {
          text += ` SUV: ${parseFloat(suv.toFixed(2))}`;
        }
      } else {
        text += modalityPixelValueText;
      }
    }

    // Prepare text
    const textCoords = cornerstone.pixelToCanvas(element, toolCoords);

    // Translate the x/y away from the cursor
    let translation = {
      x: 12,
      y: -(textStyle.getFontSize() + 10) / 2
    };

    const handleRadius = 6;
    const padding = 5;
    const width = context.measureText(text).width + 2 * padding;

    if (isTouchEvent) {
      translation = {
        x: -width / 2,
        y: -textStyle.getFontSize() - 10 - 2 * handleRadius
      };
    }

    drawCircle(context, element, textCoords, handleRadius, { color }, 'canvas');
    drawTextBox(context, text, textCoords.x + translation.x, textCoords.y + translation.y, color);
  });
};
<commit_msg>updated DragProbeTool<commit_after>import external from '../externalModules.js';
import BaseTool from '../base/BaseTool.js';

import textStyle from '../stateManagement/textStyle.js';
import toolColors from '../stateManagement/toolColors.js';
import getRGBPixels from '../util/getRGBPixels.js';
import calculateSUV from '../util/calculateSUV.js';
import { getNewContext, draw, setShadow, drawCircle } from '../drawing/index.js';
import drawTextBox, {textBoxWidth} from '../drawing/drawTextBox.js';

/**
 * @export @public @class
 * @name DragProbeTool
 * @classdesc Tool which provides a probe of the image data at the
 * input position on drag.
 * @extends BaseTool
 */
export default class DragProbeTool extends BaseTool {
  constructor (name = 'DragProbe') {
    const strategies = {
      default: defaultStrategy,
      minimal: minimalStrategy
    };

    super({
      name,
      strategies,
      defaultStrategy: 'default',
      supportedInteractionTypes: ['Mouse', 'Touch'],
      configuration: {}
    });

    this.touchDragCallback = this._movingEventCallback.bind(this);
    this.touchEndCallback = this._endMovingEventCallback.bind(this);

    this.mouseDragCallback = this._movingEventCallback.bind(this);
    this.mouseUpCallback = this._endMovingEventCallback.bind(this);

    this.dragEventData = {};
  }

  _movingEventCallback (evt) {
    const eventData = evt.detail;
    const { element } = eventData;

    this.dragEventData = eventData;
    external.cornerstone.updateImage(element);
  }

  _endMovingEventCallback (evt) {
    const eventData = evt.detail;
    const { element } = eventData;

    this.dragEventData = {};
    external.cornerstone.updateImage(element);
  }

  renderToolData (evt) {
    if (evt && evt.detail && Boolean(Object.keys(this.dragEventData.currentPoints).length)) {
      evt.detail.currentPoints = this.dragEventData.currentPoints;
      this.applyActiveStrategy(evt);
    }
  }
}

/**
 * Default strategy will pick the exactly point of mouse/touch interact and display the probe data.
 *
 * @param  {} evt Image rendered event
 * @param  {} config Tool configuration
 */
const defaultStrategy = (evt, config) => {
  const cornerstone = external.cornerstone;
  const eventData = evt.detail;
  const { element, image, currentPoints, canvasContext } = eventData;

  const context = getNewContext(canvasContext.canvas);

  const color = toolColors.getActiveColor();
  const fontHeight = textStyle.getFontSize();

  const x = Math.round(currentPoints.image.x);
  const y = Math.round(currentPoints.image.y);

  if (x < 0 || y < 0 || x >= image.columns || y >= image.rows) {
    return;
  }

  draw(context, (context) => {
    setShadow(context, config);

    const text = `${x}, ${y}`;
    let storedPixels;
    let str;

    if (image.color) {
      storedPixels = getRGBPixels(element, x, y, 1, 1);
      str = `R: ${storedPixels[0]} G: ${storedPixels[1]} B: ${storedPixels[2]} A: ${storedPixels[3]}`;
    } else {
      storedPixels = cornerstone.getStoredPixels(element, x, y, 1, 1);
      const sp = storedPixels[0];
      const mo = sp * image.slope + image.intercept;
      const suv = calculateSUV(image, sp);

      // Draw text
      str = `SP: ${sp} MO: ${parseFloat(mo.toFixed(3))}`;
      if (suv) {
        str += ` SUV: ${parseFloat(suv.toFixed(3))}`;
      }
    }

    // Draw text 5px away from cursor
    const textCoords = {
      x: currentPoints.canvas.x + 5,
      y: currentPoints.canvas.y - 5
    };

    drawTextBox(context, str, textCoords.x, textCoords.y + fontHeight + 5, color);
    drawTextBox(context, text, textCoords.x, textCoords.y, color);
  });
};

/**
 * Minimal strategy will position a circle and use the center of the circle to calculate and display probe data.
 *
 * @param  {} evt Image rendered event
 * @param  {} config Tool configuration
 */
const minimalStrategy = (evt, config) => {
  const cornerstone = external.cornerstone;
  const eventData = evt.detail;
  const { element, image, currentPoints, canvasContext, isTouchEvent } = eventData;

  const context = getNewContext(canvasContext.canvas);
  const color = toolColors.getActiveColor();

  let pageCoordY = currentPoints.page.y - textStyle.getFontSize() / 2;

  if (isTouchEvent) {
    pageCoordY = currentPoints.page.y - textStyle.getFontSize() * 4;
  }
  const toolCoords = cornerstone.pageToPixel(element, currentPoints.page.x, pageCoordY);

  if (toolCoords.x < 0 || toolCoords.y < 0 ||
    toolCoords.x >= image.columns || toolCoords.y >= image.rows) {
    return;
  }

  draw(context, (context) => {
    setShadow(context, config);

    const seriesModule = cornerstone.metaData.get('generalSeriesModule', image.imageId);
    const modality = seriesModule && seriesModule.modality;

    let storedPixels;
    let text = '';

    if (image.color) {
      storedPixels = getRGBPixels(element, toolCoords.x, toolCoords.y, 1, 1);
      text = `R: ${storedPixels[0]} G: ${storedPixels[1]} B: ${storedPixels[2]}`;
    } else {
      storedPixels = cornerstone.getStoredPixels(element, toolCoords.x, toolCoords.y, 1, 1);
      const sp = storedPixels[0];
      const mo = sp * image.slope + image.intercept;

      const modalityPixelValueText = parseFloat(mo.toFixed(2));

      if (modality === 'CT') {
        text += `HU: ${modalityPixelValueText}`;
      } else if (modality === 'PT') {
        text += modalityPixelValueText;
        const suv = calculateSUV(image, sp);

        if (suv) {
          text += ` SUV: ${parseFloat(suv.toFixed(2))}`;
        }
      } else {
        text += modalityPixelValueText;
      }
    }

    // Prepare text
    const textCoords = cornerstone.pixelToCanvas(element, toolCoords);

    // Translate the x/y away from the cursor
    let translation = {
      x: 12,
      y: -(textStyle.getFontSize() + 10) / 2
    };

    const handleRadius = 6;
    const padding = 5;
    const width = textBoxWidth(context, text, padding);

    if (isTouchEvent) {
      translation = {
        x: -width / 2,
        y: -textStyle.getFontSize() - 10 - 2 * handleRadius
      };
    }

    drawCircle(context, element, textCoords, handleRadius, { color }, 'canvas');
    drawTextBox(context, text, textCoords.x + translation.x, textCoords.y + translation.y, color);
  });
};
<|endoftext|>