<commit_before>function AI(grid) {
  this.grid = grid;
}

// static evaluation function
AI.prototype.eval = function() {
  // take into account the number of empty cells
  var emptyCells = Math.max(1, this.grid.availableCells().length);
  // take into account the maximum value on the board
  // that would result from this move. The log
  // is computed before the value is returned
  // Also returns the sum, scaled by using log base 10.
  var maxSum = this.grid.maxAndSum();
  // take into account the similarity of the tiles on
  // the board
  var boardSim = Math.max(1, this.grid.boardSimilarityScore());
  // take into account the monotonicity of the
  // board
  var monotoneScore = Math.max(1, this.grid.monotoneBoardScore());

  return emptyCells*maxSum[0]*
    maxSum[1]*boardSim*monotoneScore;
};

AI.prototype.random = function(){}

//
// Expectiminimax algorithm
//
AI.prototype.expectiminimax = function (depth) {
  if (this.grid.playerTurn) {
    // It's the AI's turn to play (max)
    if (depth == 0) {
      // Don't go any deeper, just run the heuristic
      return {
        score: HEURISTIC_FUNCTION(this.grid);
      }
    } else {
      var score = 0;
      var move = -1;
      // Loop through the allowed moves (up, down, left right)
      for (var direction in [0, 1, 2, 3]) {
        // Clone the grid and make the move on it
        var newGrid = this.grid.clone();
        this.grid.playerTurn = false;
        if (newGrid.move(direction).moved) {
          // Move was successful
          // Check if the value of this new board is greater than the cached
          // value stored in score

          // Check the value of the board by makinga new AI with that board and
          // then running the expectiminimax algo on it
          var newAI = new AI(newGrid);
          var newScore = newAI.expectiminimax(depth - 1).score;

          // If the new score is greater than the previous one, update the
          // score and direction required to get that score
          if (newScore > score) {
            score = newScore;
            move = direction;
          }
        }
      }

      return {
        score: score,
        move: dir
      }
    }
  } else {
    // It's the computer's turn, i.e. the random addition of a 2 tile or 4 tile
    // This is the min player
    var score = 0;

    // Get the available (empty) cells in the grid
    var cells = this.grid.availableCells();

    // Tile to be inserted could be a 2 or a 4
    for (var value in [2, 4]) {

      // Loop through the empty cells
      for (var i in cells) {

        // Create and insert a new tile with the given number
        var cell = cells[i];
        var tile = new Tile(cell, value);
        this.grid.insertTile(tile);

        // Calculate the value of this board
        var newScore = this.expectiminimax(depth).score;

        // The value to add to the total score is the probability that the value
        // is chosen (prob of 2 or prob of 4) multiplied by the probability that
        // that cell is chosen (each cell has equal probability) times the score
        // of the new board
        score += probabilityOfNewTile(value) * newScore * (1 / cells.length);

        // Remove the cell so that everything is back to how it started
        this.grid.removeTile(cell);
      }
    }
    this.grid.playerTurn = true;
    // Not sure if the return value is correct
    return {
      score: score
    }
  }
}

// alpha-beta depth first search
AI.prototype.search = function(depth, alpha, beta, positions, cutoffs) {
  var bestScore;
  var bestMove = -1;
  var result;

  // the maxing player
  if (this.grid.playerTurn) {
    bestScore = alpha;
    for (var direction in [0, 1, 2, 3]) {
      var newGrid = this.grid.clone();
      if (newGrid.move(direction).moved) {
        positions++;
        if (newGrid.isWin()) {
          return { move: direction, score: 10000, positions: positions, cutoffs: cutoffs };
        }
        var newAI = new AI(newGrid);

        if (depth == 0) {
          result = { move: direction, score: newAI.eval() };
        } else {
          result = newAI.search(depth-1, bestScore, beta, positions, cutoffs);
          if (result.score > 9900) { // win
            result.score--; // to slightly penalize higher depth from win
          }
          positions = result.positions;
          cutoffs = result.cutoffs;
        }

        if (result.score > bestScore) {
          bestScore = result.score;
          bestMove = direction;
        }
        if (bestScore > beta) {
          cutoffs++
          return { move: bestMove, score: beta, positions: positions, cutoffs: cutoffs };
        }
      }
    }
  }

  else { // computer's turn, we'll do heavy pruning to keep the branching factor low
    bestScore = beta;

    // try a 2 and 4 in each cell and measure how annoying it is
    // with metrics from eval
    var candidates = [];
    var cells = this.grid.availableCells();
    var scores = { 2: [], 4: [] };
    for (var value in scores) {
      for (var i in cells) {
        scores[value].push(null);
        var cell = cells[i];
        var tile = new Tile(cell, parseInt(value, 10));
        this.grid.insertTile(tile);
        scores[value][i] = -this.grid.smoothness() + this.grid.islands();
        this.grid.removeTile(cell);
      }
    }

    // now just pick out the most annoying moves
    var maxScore = Math.max(Math.max.apply(null, scores[2]), Math.max.apply(null, scores[4]));
    for (var value in scores) { // 2 and 4
      for (var i=0; i<scores[value].length; i++) {
        if (scores[value][i] == maxScore) {
          candidates.push( { position: cells[i], value: parseInt(value, 10) } );
        }
      }
    }

    // search on each candidate
    for (var i=0; i<candidates.length; i++) {
      var position = candidates[i].position;
      var value = candidates[i].value;
      var newGrid = this.grid.clone();
      var tile = new Tile(position, value);
      newGrid.insertTile(tile);
      newGrid.playerTurn = true;
      positions++;
      newAI = new AI(newGrid);
      result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
      positions = result.positions;
      cutoffs = result.cutoffs;

      if (result.score < bestScore) {
        bestScore = result.score;
      }
      if (bestScore < alpha) {
        cutoffs++;
        return { move: null, score: alpha, positions: positions, cutoffs: cutoffs };
      }
    }
    //*/

    /*
    for (var samples=0; samples<4; samples++) {
      var newGrid = this.grid.clone();
      newGrid.computerMove();
      newAI = new AI(newGrid);
      result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
      positions = result.positions;
      cutoffs = result.cutoffs;

      if (result.score < bestScore) {
        bestScore = result.score;
      }
      if (bestScore < alpha) {
        //console.log('cutoff')
        cutoffs++;
        return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
      }

    }
    //*/
    /*
    for (var x=0; x<4; x++) {
      for (var y=0; y<4; y++) {
        var position = {x:x, y:y};
        if (this.grid.cellAvailable(position)) {
          for (var value in [2, 4]) {
          //for (var value in [2]) {
            var newGrid = this.grid.clone();
            var tile = new Tile(position, value);
            newGrid.insertTile(tile);
            newGrid.playerTurn = true;
            positions++;
            newAI = new AI(newGrid);
            //console.log('inserted tile, players turn is', newGrid.playerTurn);
            result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
            positions = result.positions;
            cutoffs = result.cutoffs;

            if (result.score < bestScore) {
              bestScore = result.score;
            }
            if (bestScore < alpha) {
              //console.log('cutoff')
              cutoffs++;
              return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
            }
          }
        }
      }
    }
    //*/
  }

  return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
}

// performs a search and returns the best move
AI.prototype.getBest = function() {
  return this.iterativeDeep();
}

// performs iterative deepening over the alpha-beta search
AI.prototype.iterativeDeep = function() {
  var start = (new Date()).getTime();
  var depth = 0;
  var best;
  do {
    var newBest = this.search(depth, -10000, 10000, 0 ,0);
    if (newBest.move == -1) {
      //console.log('BREAKING EARLY');
      break;
    } else {
      best = newBest;
    }
    depth++;
  } while ( (new Date()).getTime() - start < minSearchTime);
  //console.log('depth', --depth);
  //console.log(this.translate(best.move));
  //console.log(best);
  return best
}

AI.prototype.translate = function(move) {
 return {
    0: 'up',
    1: 'right',
    2: 'down',
    3: 'left'
  }[move];
}

<commit_msg>Switch playerTurn immediately<commit_after>function AI(grid) {
  this.grid = grid;
}

// static evaluation function
AI.prototype.eval = function() {
  // take into account the number of empty cells
  var emptyCells = Math.max(1, this.grid.availableCells().length);
  // take into account the maximum value on the board
  // that would result from this move. The log
  // is computed before the value is returned
  // Also returns the sum, scaled by using log base 10.
  var maxSum = this.grid.maxAndSum();
  // take into account the similarity of the tiles on
  // the board
  var boardSim = Math.max(1, this.grid.boardSimilarityScore());
  // take into account the monotonicity of the
  // board
  var monotoneScore = Math.max(1, this.grid.monotoneBoardScore());

  return emptyCells*maxSum[0]*
    maxSum[1]*boardSim*monotoneScore;
};

AI.prototype.random = function(){}

//
// Expectiminimax algorithm
//
AI.prototype.expectiminimax = function (depth) {
  if (this.grid.playerTurn) {
    this.grid.playerTurn = false;

    // It's the AI's turn to play (max)
    if (depth == 0) {
      // Don't go any deeper, just run the heuristic
      return {
        score: HEURISTIC_FUNCTION(this.grid);
      }
    } else {
      var score = 0;
      var move = -1;
      // Loop through the allowed moves (up, down, left right)
      for (var direction in [0, 1, 2, 3]) {
        // Clone the grid and make the move on it
        var newGrid = this.grid.clone();
        if (newGrid.move(direction).moved) {
          // Move was successful
          // Check if the value of this new board is greater than the cached
          // value stored in score

          // Check the value of the board by makinga new AI with that board and
          // then running the expectiminimax algo on it
          var newAI = new AI(newGrid);
          var newScore = newAI.expectiminimax(depth - 1).score;

          // If the new score is greater than the previous one, update the
          // score and direction required to get that score
          if (newScore > score) {
            score = newScore;
            move = direction;
          }
        }
      }

      return {
        score: score,
        move: dir
      }
    }
  } else {
    this.grid.playerTurn = true;

    // It's the computer's turn, i.e. the random addition of a 2 tile or 4 tile
    // This is the min player
    var score = 0;

    // Get the available (empty) cells in the grid
    var cells = this.grid.availableCells();

    // Tile to be inserted could be a 2 or a 4
    for (var value in [2, 4]) {

      // Loop through the empty cells
      for (var i in cells) {

        // Create and insert a new tile with the given number
        var cell = cells[i];
        var tile = new Tile(cell, value);
        this.grid.insertTile(tile);

        // Calculate the value of this board
        var newScore = this.expectiminimax(depth).score;

        // The value to add to the total score is the probability that the value
        // is chosen (prob of 2 or prob of 4) multiplied by the probability that
        // that cell is chosen (each cell has equal probability) times the score
        // of the new board
        score += probabilityOfNewTile(value) * newScore * (1 / cells.length);

        // Remove the cell so that everything is back to how it started
        this.grid.removeTile(cell);
      }
    }
    // Not sure if the return value is correct
    return {
      score: score
    }
  }
}

// alpha-beta depth first search
AI.prototype.search = function(depth, alpha, beta, positions, cutoffs) {
  var bestScore;
  var bestMove = -1;
  var result;

  // the maxing player
  if (this.grid.playerTurn) {
    bestScore = alpha;
    for (var direction in [0, 1, 2, 3]) {
      var newGrid = this.grid.clone();
      if (newGrid.move(direction).moved) {
        positions++;
        if (newGrid.isWin()) {
          return { move: direction, score: 10000, positions: positions, cutoffs: cutoffs };
        }
        var newAI = new AI(newGrid);

        if (depth == 0) {
          result = { move: direction, score: newAI.eval() };
        } else {
          result = newAI.search(depth-1, bestScore, beta, positions, cutoffs);
          if (result.score > 9900) { // win
            result.score--; // to slightly penalize higher depth from win
          }
          positions = result.positions;
          cutoffs = result.cutoffs;
        }

        if (result.score > bestScore) {
          bestScore = result.score;
          bestMove = direction;
        }
        if (bestScore > beta) {
          cutoffs++
          return { move: bestMove, score: beta, positions: positions, cutoffs: cutoffs };
        }
      }
    }
  }

  else { // computer's turn, we'll do heavy pruning to keep the branching factor low
    bestScore = beta;

    // try a 2 and 4 in each cell and measure how annoying it is
    // with metrics from eval
    var candidates = [];
    var cells = this.grid.availableCells();
    var scores = { 2: [], 4: [] };
    for (var value in scores) {
      for (var i in cells) {
        scores[value].push(null);
        var cell = cells[i];
        var tile = new Tile(cell, parseInt(value, 10));
        this.grid.insertTile(tile);
        scores[value][i] = -this.grid.smoothness() + this.grid.islands();
        this.grid.removeTile(cell);
      }
    }

    // now just pick out the most annoying moves
    var maxScore = Math.max(Math.max.apply(null, scores[2]), Math.max.apply(null, scores[4]));
    for (var value in scores) { // 2 and 4
      for (var i=0; i<scores[value].length; i++) {
        if (scores[value][i] == maxScore) {
          candidates.push( { position: cells[i], value: parseInt(value, 10) } );
        }
      }
    }

    // search on each candidate
    for (var i=0; i<candidates.length; i++) {
      var position = candidates[i].position;
      var value = candidates[i].value;
      var newGrid = this.grid.clone();
      var tile = new Tile(position, value);
      newGrid.insertTile(tile);
      newGrid.playerTurn = true;
      positions++;
      newAI = new AI(newGrid);
      result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
      positions = result.positions;
      cutoffs = result.cutoffs;

      if (result.score < bestScore) {
        bestScore = result.score;
      }
      if (bestScore < alpha) {
        cutoffs++;
        return { move: null, score: alpha, positions: positions, cutoffs: cutoffs };
      }
    }
    //*/

    /*
    for (var samples=0; samples<4; samples++) {
      var newGrid = this.grid.clone();
      newGrid.computerMove();
      newAI = new AI(newGrid);
      result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
      positions = result.positions;
      cutoffs = result.cutoffs;

      if (result.score < bestScore) {
        bestScore = result.score;
      }
      if (bestScore < alpha) {
        //console.log('cutoff')
        cutoffs++;
        return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
      }

    }
    //*/
    /*
    for (var x=0; x<4; x++) {
      for (var y=0; y<4; y++) {
        var position = {x:x, y:y};
        if (this.grid.cellAvailable(position)) {
          for (var value in [2, 4]) {
          //for (var value in [2]) {
            var newGrid = this.grid.clone();
            var tile = new Tile(position, value);
            newGrid.insertTile(tile);
            newGrid.playerTurn = true;
            positions++;
            newAI = new AI(newGrid);
            //console.log('inserted tile, players turn is', newGrid.playerTurn);
            result = newAI.search(depth, alpha, bestScore, positions, cutoffs);
            positions = result.positions;
            cutoffs = result.cutoffs;

            if (result.score < bestScore) {
              bestScore = result.score;
            }
            if (bestScore < alpha) {
              //console.log('cutoff')
              cutoffs++;
              return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
            }
          }
        }
      }
    }
    //*/
  }

  return { move: bestMove, score: bestScore, positions: positions, cutoffs: cutoffs };
}

// performs a search and returns the best move
AI.prototype.getBest = function() {
  return this.iterativeDeep();
}

// performs iterative deepening over the alpha-beta search
AI.prototype.iterativeDeep = function() {
  var start = (new Date()).getTime();
  var depth = 0;
  var best;
  do {
    var newBest = this.search(depth, -10000, 10000, 0 ,0);
    if (newBest.move == -1) {
      //console.log('BREAKING EARLY');
      break;
    } else {
      best = newBest;
    }
    depth++;
  } while ( (new Date()).getTime() - start < minSearchTime);
  //console.log('depth', --depth);
  //console.log(this.translate(best.move));
  //console.log(best);
  return best
}

AI.prototype.translate = function(move) {
 return {
    0: 'up',
    1: 'right',
    2: 'down',
    3: 'left'
  }[move];
}

<|endoftext|>


<commit_before>import { Vessels } from '/imports/api/vessels/vessels.coffee'

Meteor.publish('vessel.details', function(vesselId, projectId) {

    if (typeof(vesselId) != 'string' || vesselId == '') {
        return [];
    }

    if (!Roles.userIsInRole(this.userId, Permissions.member, projectId)) {
        return [];
    }

    const project = Projects.findOne(projectId, {
        fields: {
            vesselModule: 1
        }
    });

    if (project == null || !project.vesselModule) {
        return [];
    }

    const cursor = Vessels.find({
        _id: vesselId
    });

    const handle = cursor.observe({
        added: (vessel) => {
            this.added('vessels', vessel._id, getExtendedVessel(vessel));
        },
        changed: (newVessel, oldVessel) => {
            this.changed('vessels', oldVessel._id, getExtendedVessel(newVessel));
        }
    });

    this.ready();

    this.onStop(() => {
        handle.stop();
    });
});

function getExtendedVessel(vesselId) {
    let vessel = Vessels.findOne(vesselId);

    if (vessel != undefined) {
        if ('visits' in vessel) {
            if (vessel.visits.length > 1) {
                vessel.visits.sort((a, b) => {
                    a.date - b.date
                });
                vessel.visits = [vessel.visits.pop()];
            }

            if (vessel.visits.length > 0) {
                if (vessel.visits[0].isUserVisible) {
                    const author = Meteor.users.findOne(vessel.visits[0].createdBy, {
                        fields: {
                            'profile.firstname': 1,
                            'profile.lastname': 1,
                            'profile.telefon': 1,
                            'profile.email': 1
                        }
                    });

                    vessel.visits[0].person = author.profile.firstname + ' ' + author.profile.lastname;
                    vessel.visits[0].email = author.profile.email;
                    vessel.visits[0].phone = author.profile.telefon;
                } else {
                    vessel.visits[0].person = 'Not visible';
                    vessel.visits[0].email = '';
                    vessel.visits[0].phone = '';
                }

                const project = Projects.findOne(vessel.visits[0].projectId, {
                    fields: {
                        country: 1,
                        harborGroup: 1,
                        harbors: 1
                    }
                });

                vessel.visits[0].country = project.country;
                vessel.visits[0].harborGroup = project.harborGroup;

                vessel.visits[0].harbor = project.harbors.filter((harbor) => {
                    return harbor._id == vessel.visits[0].harborId;
                })[0].name;
            }
        } else {
            vessel.visits = [];
        }
}

    return vessel;
}
<commit_msg>fix visit sorting<commit_after>import { Vessels } from '/imports/api/vessels/vessels.coffee'

Meteor.publish('vessel.details', function(vesselId, projectId) {

    if (typeof(vesselId) != 'string' || vesselId == '') {
        return [];
    }

    if (!Roles.userIsInRole(this.userId, Permissions.member, projectId)) {
        return [];
    }

    const project = Projects.findOne(projectId, {
        fields: {
            vesselModule: 1
        }
    });

    if (project == null || !project.vesselModule) {
        return [];
    }

    const cursor = Vessels.find({
        _id: vesselId
    });

    const handle = cursor.observe({
        added: (vessel) => {
            this.added('vessels', vessel._id, getExtendedVessel(vessel));
        },
        changed: (newVessel, oldVessel) => {
            this.changed('vessels', oldVessel._id, getExtendedVessel(newVessel));
        }
    });

    this.ready();

    this.onStop(() => {
        handle.stop();
    });
});

function getExtendedVessel(vesselId) {
    let vessel = Vessels.findOne(vesselId);

    if (vessel != undefined) {
        if ('visits' in vessel) {
            if (vessel.visits.length > 1) {
                vessel.visits.sort((a, b) => {
                    return a.date - b.date;
                });
                vessel.visits = [vessel.visits.pop()];
            }

            if (vessel.visits.length > 0) {
                if (vessel.visits[0].isUserVisible) {
                    const author = Meteor.users.findOne(vessel.visits[0].createdBy, {
                        fields: {
                            'profile.firstname': 1,
                            'profile.lastname': 1,
                            'profile.telefon': 1,
                            'profile.email': 1
                        }
                    });

                    vessel.visits[0].person = author.profile.firstname + ' ' + author.profile.lastname;
                    vessel.visits[0].email = author.profile.email;
                    vessel.visits[0].phone = author.profile.telefon;
                } else {
                    vessel.visits[0].person = 'Not visible';
                    vessel.visits[0].email = '';
                    vessel.visits[0].phone = '';
                }

                const project = Projects.findOne(vessel.visits[0].projectId, {
                    fields: {
                        country: 1,
                        harborGroup: 1,
                        harbors: 1
                    }
                });

                vessel.visits[0].country = project.country;
                vessel.visits[0].harborGroup = project.harborGroup;

                vessel.visits[0].harbor = project.harbors.filter((harbor) => {
                    return harbor._id == vessel.visits[0].harborId;
                })[0].name;
            }
        } else {
            vessel.visits = [];
        }
}

    return vessel;
}
<|endoftext|>


<commit_before>colors = {
  backgroundDark: '#2C3E50',
  backgroundMedium: '#2980B9',
  backgroundMediumLight: '#3498DB',
  backgroundLight: '#ECF0F1',
  red: '#E74C3C',
}

var tables = []
var s = Snap('#diagram')

class Table {
  constructor(name, x = 10, y = 10) {
    let graphics = Snap()

    this.metrics = {
      x,
      y,
      width: 300,
      height: 40
    }

    this.name = this.convertTableName(name)

    this.properties = {}

    this.table = {
      canvas: graphics,
      container: graphics
          .rect(1, 1, this.metrics.width - 2, this.metrics.height - 2),
      separator: graphics
          .rect(0, 38, this.metrics.width, '100%'),
      label: graphics
          .text(this.metrics.width / 2, this.metrics.height / 2 + 4, this.name),

      buttonAdd: graphics.append((() => {
            let g = Snap()
            g.attr({
              x: 10,
              y: 10,
            })
            g.addClass('button')

            g.rect()
                .addClass('background')

            // Plus
            g.line(10, 5, 10, 15)
                .addClass('line')
            g.line(5, 10, 15, 10)
                .addClass('line')

            g.click(() => this.addProperty(new Property(`Property ${Object.keys(this.properties).length + 1}`)))

            return g
          })()
      ),
      buttonDelete: graphics.append((() => {
            let g = Snap()
            g.attr({
              x: this.metrics.width - 10 - 20,
              y: 10,
            })
            g.addClass('button')

            g.rect()
                .addClass('background')

            // Cross
            g.line(6, 6, 14, 14)
                .addClass('line')
            g.line(6, 14, 14, 6)
                .addClass('line')

            return g
          })()
      ),
    }

    // Add classes and config
    this.table.canvas.attr({
      id: this.name,
      class: 'table',
      x: this.metrics.x,
      y: this.metrics.y,
      width: this.metrics.width,
      height: this.metrics.height,
    })
    this.table.container.addClass('background')
    this.table.separator.addClass('separator')
    this.table.label.addClass('tableName')
    this.table.label.click(() => {
      let name = window.prompt('Enter new table name', this.name)

      if (name !== null)
        this.changeTableName(name)
    })
  }

  changeTableName(name) {
    this.name = this.convertTableName(name)
    this.table.label.node.innerHTML = this.name
    this.table.canvas.attr({
      id: this.name,
    })
  }

  addProperty(property) {
    this.properties[property.name] = property.property.canvas

    this.metrics.height = 40 + (40 * Object.keys(this.properties).length)
    this.table.canvas.attr({height: this.metrics.height})
    this.table.container.attr({height: this.metrics.height - 2})

    this.properties[property.name].attr({y: 40 * (Object.keys(this.properties).length)})

    this.table.canvas.append(this.properties[property.name])

    // TODO: Sorting the properties
    // Should be weighted based on primary key (and maybe foreign key) and then sorted by alpha desc
    // this.properties = Object.keys(this.properties).sort((a, b) => {
    //   if (a.hasClass('primaryKey') && b.hasClass('primaryKey')) {
    //     return 1
    //   }
    // })
  }

  convertTableName(name) {
    // Make all none-word chars into "_"
    name = name.replace(/[^\w\d]/g, '_')

    // Make the first letter lowercase
    name = name.substring(0, 1).toLowerCase() + name.substring(1, name.length)

    // Converts camelcase into underscore
    name = name.replace(/([A-Z])/g, '_$1')

    // Converts everything to lowercase, as some SQL systems does not support casing
    name = name.toLowerCase()

    return name
  }
}

class Property {
  constructor(name) {
    let graphics = Snap()

    this.metrics = {
      x: 2,
      y: 2,
      width: 296,
      height: 38
    }

    this.name = this.convertPropertyName(name)

    this.property = {
      canvas: graphics,
      container: graphics
          .rect(0, 0, '100%', '100%'),
      label: graphics
          .text(10 + 20 + 10, this.metrics.height / 2 + 4, this.name),

      buttonPrimaryKey: graphics.append((() => {
            let g = Snap(20, 20)
            g.attr({
              x: 10,
              y: 10
            })
            g.addClass('button primaryKey')

            g.rect()
                .addClass('background')

            // Plus
            g.text(10, 10 + 4, "PK")
                .addClass('label')

            g.click(() => {
               g
                  .parent()
                  .parent()
                  .selectAll('.property .button.primaryKey')
                  .items.forEach(item =>
                      item.removeClass('active')
                  )

              g.toggleClass('active')
            })

            return g
          })()
      ),
      buttonDelete: graphics.append((() => {
            let g = Snap()
            g.attr({
              x: this.metrics.width - 10 - 20,
              y: 10,
            })
            g.addClass('button')

            g.rect()
                .addClass('background')

            // Cross
            g.line(6, 6, 14, 14)
                .addClass('line')
            g.line(6, 14, 14, 6)
                .addClass('line')

            return g
          })()
      ),
    }

    // Add classes and config
    this.property.canvas.attr({
      id: this.name,
      class: 'property',
      x: this.metrics.x,
      y: this.metrics.y,
      width: this.metrics.width,
      height: this.metrics.height,
    })
    this.property.container.addClass('background')
    this.property.label.addClass('label')
    this.property.label.click(() => {
      let name = window.prompt('Enter new property name', this.name)

      if (name !== null)
        this.changePropertyName(name)
    })
  }

  changePropertyName(name) {
    this.name = this.convertPropertyName(name)
    this.property.label.node.innerHTML = this.name
    this.property.canvas.attr({
      id: this.name,
    })
  }

  convertPropertyName(name) {
    // Make all none-word chars into "_"
    name = name.replace(/\W/g, '_')

    // Make the first letter lowercase
    name = name.substring(0, 1).toLowerCase() + name.substring(1, name.length)

    // Converts camelcase into underscore
    name = name.replace(/([A-Z])/g, '_$1')

    // Converts everything to lowercase, as some SQL systems does not support casing
    name = name.toLowerCase()

    return name
  }
}

function addTable(e = null) {
  if (e === null) {
    document.querySelector('img#buttonNewTable').className = document.querySelector('div#diagramOverlay').className = 'active'
    document.querySelector('div#diagramOverlay').innerHTML = 'Click anywhere here to place new table'
    s.click(addTable)
  } else {
    let name = window.prompt('Please enter the table name', '')

    if (name !== null) {
      tables.push(new Table(name, e.offsetX, e.offsetY))
      s.append(tables[tables.length - 1].table.canvas)
    }

    document.querySelector('img#buttonNewTable').className = document.querySelector('div#diagramOverlay').className = ''
    document.querySelector('div#diagramOverlay').innerHTML = ''
    s.unclick(addTable)
  }
}<commit_msg>Adds initial property of 'id' to a new table<commit_after>colors = {
  backgroundDark: '#2C3E50',
  backgroundMedium: '#2980B9',
  backgroundMediumLight: '#3498DB',
  backgroundLight: '#ECF0F1',
  red: '#E74C3C',
}

var tables = []
var s = Snap('#diagram')

class Table {
  constructor(name, x = 10, y = 10) {
    let graphics = Snap()

    this.metrics = {
      x,
      y,
      width: 300,
      height: 40
    }

    this.name = this.convertTableName(name)

    this.properties = {}

    this.table = {
      canvas: graphics,
      container: graphics
          .rect(1, 1, this.metrics.width - 2, this.metrics.height - 2),
      separator: graphics
          .rect(0, 38, this.metrics.width, '100%'),
      label: graphics
          .text(this.metrics.width / 2, this.metrics.height / 2 + 4, this.name),

      buttonAdd: graphics.append((() => {
            let g = Snap()
            g.attr({
              x: 10,
              y: 10,
            })
            g.addClass('button')

            g.rect()
                .addClass('background')

            // Plus
            g.line(10, 5, 10, 15)
                .addClass('line')
            g.line(5, 10, 15, 10)
                .addClass('line')

            g.click(() => this.addProperty(new Property(`Property ${Object.keys(this.properties).length + 1}`)))

            return g
          })()
      ),
      buttonDelete: graphics.append((() => {
            let g = Snap()
            g.attr({
              x: this.metrics.width - 10 - 20,
              y: 10,
            })
            g.addClass('button')

            g.rect()
                .addClass('background')

            // Cross
            g.line(6, 6, 14, 14)
                .addClass('line')
            g.line(6, 14, 14, 6)
                .addClass('line')

            return g
          })()
      ),
    }

    // Add classes and config
    this.table.canvas.attr({
      id: this.name,
      class: 'table',
      x: this.metrics.x,
      y: this.metrics.y,
      width: this.metrics.width,
      height: this.metrics.height,
    })
    this.table.container.addClass('background')
    this.table.separator.addClass('separator')
    this.table.label.addClass('tableName')
    this.table.label.click(() => {
      let name = window.prompt('Enter new table name', this.name)

      if (name !== null)
        this.changeTableName(name)
    })

    this.addProperty(new Property('id'))
  }

  changeTableName(name) {
    this.name = this.convertTableName(name)
    this.table.label.node.innerHTML = this.name
    this.table.canvas.attr({
      id: this.name,
    })
  }

  addProperty(property) {
    this.properties[property.name] = property.property.canvas

    this.metrics.height = 40 + (40 * Object.keys(this.properties).length)
    this.table.canvas.attr({height: this.metrics.height})
    this.table.container.attr({height: this.metrics.height - 2})

    this.properties[property.name].attr({y: 40 * (Object.keys(this.properties).length)})

    this.table.canvas.append(this.properties[property.name])

    // TODO: Sorting the properties
    // Should be weighted based on primary key (and maybe foreign key) and then sorted by alpha desc
    // this.properties = Object.keys(this.properties).sort((a, b) => {
    //   if (a.hasClass('primaryKey') && b.hasClass('primaryKey')) {
    //     return 1
    //   }
    // })
  }

  convertTableName(name) {
    // Make all none-word chars into "_"
    name = name.replace(/[^\w\d]/g, '_')

    // Make the first letter lowercase
    name = name.substring(0, 1).toLowerCase() + name.substring(1, name.length)

    // Converts camelcase into underscore
    name = name.replace(/([A-Z])/g, '_$1')

    // Converts everything to lowercase, as some SQL systems does not support casing
    name = name.toLowerCase()

    return name
  }
}

class Property {
  constructor(name) {
    let graphics = Snap()

    this.metrics = {
      x: 2,
      y: 2,
      width: 296,
      height: 38
    }

    this.name = this.convertPropertyName(name)

    this.property = {
      canvas: graphics,
      container: graphics
          .rect(0, 0, '100%', '100%'),
      label: graphics
          .text(10 + 20 + 10, this.metrics.height / 2 + 4, this.name),

      buttonPrimaryKey: graphics.append((() => {
            let g = Snap(20, 20)
            g.attr({
              x: 10,
              y: 10
            })
            g.addClass('button primaryKey')

            g.rect()
                .addClass('background')

            // Plus
            g.text(10, 10 + 4, "PK")
                .addClass('label')

            g.click(() => {
               g
                  .parent()
                  .parent()
                  .selectAll('.property .button.primaryKey')
                  .items.forEach(item =>
                      item.removeClass('active')
                  )

              g.toggleClass('active')
            })

            return g
          })()
      ),
      buttonDelete: graphics.append((() => {
            let g = Snap()
            g.attr({
              x: this.metrics.width - 10 - 20,
              y: 10,
            })
            g.addClass('button')

            g.rect()
                .addClass('background')

            // Cross
            g.line(6, 6, 14, 14)
                .addClass('line')
            g.line(6, 14, 14, 6)
                .addClass('line')

            return g
          })()
      ),
    }

    // Add classes and config
    this.property.canvas.attr({
      id: this.name,
      class: 'property',
      x: this.metrics.x,
      y: this.metrics.y,
      width: this.metrics.width,
      height: this.metrics.height,
    })
    this.property.container.addClass('background')
    this.property.label.addClass('label')
    this.property.label.click(() => {
      let name = window.prompt('Enter new property name', this.name)

      if (name !== null)
        this.changePropertyName(name)
    })
  }

  changePropertyName(name) {
    this.name = this.convertPropertyName(name)
    this.property.label.node.innerHTML = this.name
    this.property.canvas.attr({
      id: this.name,
    })
  }

  convertPropertyName(name) {
    // Make all none-word chars into "_"
    name = name.replace(/\W/g, '_')

    // Make the first letter lowercase
    name = name.substring(0, 1).toLowerCase() + name.substring(1, name.length)

    // Converts camelcase into underscore
    name = name.replace(/([A-Z])/g, '_$1')

    // Converts everything to lowercase, as some SQL systems does not support casing
    name = name.toLowerCase()

    return name
  }
}

function addTable(e = null) {
  if (e === null) {
    document.querySelector('img#buttonNewTable').className = document.querySelector('div#diagramOverlay').className = 'active'
    document.querySelector('div#diagramOverlay').innerHTML = 'Click anywhere here to place new table'
    s.click(addTable)
  } else {
    let name = window.prompt('Please enter the table name', '')

    if (name !== null) {
      tables.push(new Table(name, e.offsetX, e.offsetY))
      s.append(tables[tables.length - 1].table.canvas)
    }

    document.querySelector('img#buttonNewTable').className = document.querySelector('div#diagramOverlay').className = ''
    document.querySelector('div#diagramOverlay').innerHTML = ''
    s.unclick(addTable)
  }
}<|endoftext|>


<commit_before>/*! This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true,
strict:true, undef:true, unused:true, curly:true, browser:true, white:true,
moz:true, esnext:false, indent:2, maxerr:50, devel:true, node:true, boss:true,
globalstrict:true, nomen:false, newcap:false */

/*global d3:false, $:false */

'use strict';

var TRANSITION_DURATION = 500;

var getColour = function (bug) {
  switch (bug.status) {
    case 'submitted':
    case 'posted':
      return 'rgb(127,255,127)';
    case 'unknown':
      return 'rgb(255,255,255)';
    default:
      return 'rgb(255,127,127)';
  }
};

var draw = function (bugs) {
  console.log(d3.select('.bugs').selectAll('.bug')[0].length)
  var bugRow = d3.select('.bugs').selectAll('.bug')
    .data(bugs);
  console.log(d3.select('.bugs').selectAll('.bug')[0].length)

  bugRow.enter().append('div').classed('bug', true)
    .style({'opacity': '0', 'background-color': bug => getColour(bug)})
    .text(bug => bug.bugId + ' (' + bug.status + ')')
    .transition().duration(TRANSITION_DURATION)
    .delay((d, i) => i*20)
    .style({'opacity': '1'});
  console.log(d3.select('.bugs').selectAll('.bug')[0].length)

  bugRow.exit()
    .transition().duration(TRANSITION_DURATION)
    .style({'height': '0px', 'opacity': '0'})
    .remove();
  console.log(d3.select('.bugs').selectAll('.bug')[0].length)
};

$.when(d3.csvPromise('data/qx.csv'))
  .then(function (bug_data) {
    $.each(bug_data, (i, bug) => {
      // console.log(bug.bugId,bug.status)
    });
    draw(bug_data);
  }).fail(function (error) {
    console.log('Fail', error);
  });
<commit_msg>Load the actual list of bugs from bugzilla!<commit_after>/*! This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true,
strict:true, undef:true, unused:true, curly:true, browser:true, white:true,
moz:true, esnext:false, indent:2, maxerr:50, devel:true, node:true, boss:true,
globalstrict:true, nomen:false, newcap:false */

/*global d3:false, $:false */

'use strict';

(function () {
var TRANSITION_DURATION = 500;
var BUGZILLA_URL = 'https://api-dev.bugzilla.mozilla.org/latest/bug?' +
    'include_fields=id,assigned_to,summary,last_change_time,whiteboard,status,resolution&' +
    'status=ALL&' +
    'whiteboard=\[qx\]';

var getColour = function (status) {
  switch (status) {
    case 'unknown':
      return 'rgb(255,255,255)';
    case 'submitted':
    case 'posted':
      return 'rgb(127,255,127)';
    case 'assigned':
    case 'fixed':
      return 'rgb(192,214,255)';
    default:
      return 'rgb(255,127,127)';
  }
};

var getAssignee = function (bug) {
  if (bug.assigned_to && bug.assigned_to.name) {
    return bug.assigned_to.name;
  }
  return 'nobody';
}

var draw = function (all_bugs, bug_statuses) {
  var bugRow = d3.select('.bugs').selectAll('.bug')
    .data(all_bugs);

  bugRow.enter().append('div').classed('bug', true)
    .style({'opacity': '0', 'background-color': bug => getColour(bug_statuses[bug.id])})
    .text(bug => bug.id + ' - ' + bug.summary + ' (' + getAssignee(bug) + ')')
    .attr('title', bug => bug_statuses[bug.id])
    .transition().duration(TRANSITION_DURATION)
    .delay((d, i) => i*20)
    .style({'opacity': '1'});

  bugRow.exit()
    .transition().duration(TRANSITION_DURATION)
    .style({'height': '0px', 'opacity': '0'})
    .remove();
};

$.when(d3.csvPromise('data/qx.csv'), d3.jsonPromise(BUGZILLA_URL))
  .then(function (bug_data, more_bug_data) {
    var all_bugs = more_bug_data.bugs;
    var bug_statuses = {};
    $.each(bug_data, (i, bug) => {
      bug_statuses[+bug.bugId] = bug.status;
    });
    draw(all_bugs, bug_statuses);
  }).fail(function (error) {
    console.log('Fail', error);
  });
})();<|endoftext|>


<commit_before>/* This file is part of Indico.
 * Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
 *
 * Indico is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * Indico is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indico; if not, see <http://www.gnu.org/licenses/>.
 */

var ndDirectives = angular.module("ndDirectives", []);

ndDirectives.directive("ndDialog", function($http, $compile, $timeout) {
    return {
        restrict: 'E',
        scope: {
            show: '=',
            heading: '@',
            okButton: '@',
            okCallback: '&',
            okOnly: '=',
            cancelButton: '@',
            cancelCallback: '&',
            validate: "=",
            api: '=',
            data: '=',
            config: "="
        },

        controller: function($scope) {
            $scope.actions = {
                init: function() {},
                cleanup: function() {},
                close: function() {
                    $scope.$apply($scope.validationStarted = false);
                    $scope.actions.cleanup();
                    $scope.show = false;
                    $scope.$apply($scope.show);
                },
                cancel: function() {
                    $scope.cancelCallback({dialogScope: $scope});
                    $scope.actions.close();
                },
                ok: function() {
                    $scope.$apply($scope.validationStarted = true);
                    var resultOkCallback = $scope.okCallback({dialogScope: $scope});
                    if(($scope.validate === true && resultOkCallback === true) || $scope.validate !== true) {
                        $scope.actions.close();
                    }
                }
            };
        },

        link: function(scope, element) {
            var dialog;

            var initDialog = function() {
                dialog = new ExclusivePopupWithButtons(
                    scope.heading,
                    scope.actions.cancel,
                    false,
                    false,
                    true
                );

                dialog._onClose = function() {};

                dialog._getButtons = function() {
                    var buttons = [];
                    var ok = scope.okButton || $T('Ok');
                    var cancel = scope.cancelButton || $T('Cancel');

                    buttons.push([ok, function() {
                        scope.actions.ok();
                    }]);

                    if (!scope.okOnly) {
                        buttons.push([cancel, function() {
                            scope.actions.cancel();
                        }]);
                    }

                    return buttons;
                };

                dialog.draw = function() {
                    return this.ExclusivePopupWithButtons.prototype.draw.call(this, element);
                };

                dialog.postDraw = function() {
                    this.canvas.dialog('option', 'draggable', false);
                    this.canvas.dialog('option', 'position', 'center');
                };
            };

            var openDialog = function() {
                if (dialog) {
                    showDialog();
                } else {
                    $http.get(
                        scope.templateUrl, {}
                    ).success(function(response, status, header, config) {
                        element = angular.element(response);
                        $compile(element)(scope);
                        $timeout(function(){
                            initDialog();
                            showDialog();
                        }, 0);
                    }). error(function(data, status, header, config) {
                        scope.show = false;
                        var msg = $T('The content of this dialog is currently unavailable.');
                        new AlertPopup(scope.heading, msg).open();
                    });
                }
            };

            var showDialog = function() {
                scope.actions.init();
                dialog.open();
            };

            scope.setSelectedTab = function(tab_id) {
                scope.tabSelected = tab_id;
            };

            scope.isTabSelected = function(tab_id) {
                return scope.tabSelected === tab_id;
            };

            scope.$watch("show", function(val) {
                if (scope.show === true) {
                    openDialog();
                } else if (dialog) {
                    dialog.close();
                }
            });
        }
    };
});

ndDirectives.directive("contenteditable", function() {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, elem, attrs, ctrl) {
            scope.edition = false;
            scope.multiline = scope.$eval(attrs.ndMultiline) || false;

            var sanitizeHtml = function() {
                var sanitized = elem.html().replace(/<div[^<]*?>/g, '
').replace(/<\/div[^<]*?>/g, '');
                sanitized = sanitized.replace(/
(
)+/g, '
');
                sanitized = sanitized.replace(/^
/, '');
                sanitized = sanitized.replace(/
$/, '');
                elem.html(sanitized);
            };

            var updateHtml = function() {
                if (ctrl.$viewValue === '') {
                    elem.html(attrs.placeholder);
                    elem.addClass('empty');
                } else {
                    elem.html(ctrl.$viewValue);
                }
            };

            var getOneLineHeight = function() {
                var html = elem.html();
                var height = elem.html('').css('height');
                elem.html(html);
                return height;
            };

            var actions = {
                init: function() {
                    elem.addClass('focus');
                    elem.actioninput('setIconsVisibility', 'visible');
                    elem.actioninput('setEmptyValue', elem.html());
                    elem.actioninput('initSize', elem.css('font-size'), getOneLineHeight());
                    elem.removeClass('empty');

                    if (ctrl.$viewValue === '' && !scope.edition) {
                        elem.html('');
                    }

                    scope.edition = true;
                },
                close: function() {
                    elem.removeClass('focus');
                    elem.actioninput('setIconsVisibility', 'hidden');
                    scope.edition = false;
                    updateHtml();
                }
            };

            // model -> view
            ctrl.$render = function() {
                updateHtml();
            };

            // creation
            elem.actioninput({
                focusAfter: false,
                enterKeyEnabled: !scope.multiline,

                // view -> model
                callback: function() {
                    actions.close();
                },

                // view -> model
                actionCallback: function() {
                    sanitizeHtml();
                    scope.$apply(function() {
                        ctrl.$setViewValue(elem.html());
                    });
                    actions.close();
                },

                // angular -> jquery
                onchange: function() {
                    elem.val(elem.html());
                }
            });

            // input init
            elem.on('focus', function() {
                actions.init();
            });

            elem.actioninput('setIconsVisibility', 'hidden');
        }
    };
});

ndDirectives.directive('ndDatepicker', function($compile) {
    return {
        restrict: 'E',
        replace: 'true',
        template: '<ng-form name="dateForm"></ng-form>',
        scope: {
            dateFormat: '@',
            hiddenInputs: '@',
            required: '@',
            showTime: '=',
            validation: '=',
            value: '='
        },

        link: function(scope, element) {
            var hiddenInputs =
                scope.$eval(scope.hiddenInputs) ||
                ['day', 'month', 'year', 'hour', 'min'];

            var mapMomentFormat = function(format) {
                format = format.replace('%Y', 'YYYY');
                format = format.replace('%m', 'MM');
                format = format.replace('%d', 'DD');
                format = format.replace('%H', 'HH');
                format = format.replace('%M', 'mm');
                return format;
            };

            var updateModel = function() {
                _.each(element.find('input'), function(e) {
                    $(e).trigger('input');
                });
            };

            var getAttributes = function() {
                var attributes = {};

                attributes['ng-class'] = "{hasError: validation && dateForm.$invalid}";
                if (scope.required) {
                    attributes.required = "required";
                }

                return attributes;
            };

            var getDatetime = function() {
                if (scope.value) {
                    return moment('{0} {1}'.format(scope.value.date, scope.value.time), 'YYYY-MM-DD HH:mm:ss');
                } else {
                    return null;
                }
            };

            var createDatePicker = function() {
                return IndicoUI.Widgets.Generic.dateField(
                    scope.showTime,
                    getAttributes(),
                    hiddenInputs,
                    null,
                    scope.dateFormat,
                    updateModel
                );
            };

            var getInputHtml = function(inputId) {
                var required = '';
                var value = null;

                if (inputId.match('Year') !== null) {
                    required = 'ng-required="{{required}}"';
                }

                if (scope.value) {
                    if (inputId.match('Year') !== null) {
                        value = getDatetime().year();
                    } else if (inputId.match('Month') !== null) {
                        value = getDatetime().month() + 1;
                    } else if (inputId.match('Day') !== null) {
                        value = getDatetime().date();
                    } else if (inputId.match('Hour') !== null) {
                        value = getDatetime().hour();
                    } else if (inputId.match('Min') !== null) {
                        value = getDatetime().minute();
                    }
                }

                return '<input type="text" name="{0}" id="{0}" ng-model="{0}" ng-init="{0}={1}" {2} style="display: none"/>'.format(inputId, value, required);
            };

            var init = function() {
                var datepicker = createDatePicker();

                element.html(datepicker.dom);
                _.each(hiddenInputs, function(inputId) {
                    element.append(getInputHtml(inputId));
                });

                if (scope.value) {
                    datepicker.set(getDatetime().format(mapMomentFormat(scope.dateFormat)));
                }

                $compile(element)(scope);
            };

            scope.$watch('dateFormat', function(newVal, oldVal) {
                init();
            });

            init();
        }
    };
});

ndDirectives.directive('ndConfirmationPopup', function() {
    return {
        restrict: 'E',
        scope: {
            heading: '@',
            content: '@',
            callback: '=',
            show: '='
        },

        link: function(scope) {
            var open = function() {
                var popup = new ConfirmPopup(scope.heading, scope.content, scope.callback);
                popup.open();
            };

            scope.$watch('show', function() {
                if (scope.show === true) {
                    scope.show = false;
                    open();
                }
            });
        }
    };
});

ndDirectives.directive('ndValidFile', function() {
    return {
        require: 'ngModel',
        link: function (scope, el, attrs, ngModel) {
            ngModel.$render = function () {
                ngModel.$setViewValue(el.val());
            };

            el.bind('change', function() {
                scope.$apply(function() {
                    ngModel.$render();
                });
            });
        }
    };
});

ndDirectives.directive('ndRadioExtend', function($rootScope) {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, element) {
            element.bind('click', function() {
                $rootScope.$$phase || $rootScope.$apply();
            });
        }
    };
});

ndDirectives.directive('ndBlacklist', function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
            var blacklist = attrs.ndBlacklist.split(',');

            // DOM -> Model validation
            ngModel.$parsers.unshift(function(value) {
                var valid = blacklist.indexOf(value) === -1;
                ngModel.$setValidity('blacklist', valid);
                return valid ? value : undefined;
            });

            // model -> DOM validation
            ngModel.$formatters.unshift(function(value) {
                ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
                return value;
            });
        }
    };
});

ndDirectives.directive('ndInitFocus', function() {
    var timer;

    return function(scope, element, attrs) {
        if (timer) clearTimeout(timer);

        timer = setTimeout(function() {
            element.focus();
        }, 0);
    };
});
<commit_msg>[IMP] contenteditable doesn't trim newlines<commit_after>/* This file is part of Indico.
 * Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
 *
 * Indico is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * Indico is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indico; if not, see <http://www.gnu.org/licenses/>.
 */

var ndDirectives = angular.module("ndDirectives", []);

ndDirectives.directive("ndDialog", function($http, $compile, $timeout) {
    return {
        restrict: 'E',
        scope: {
            show: '=',
            heading: '@',
            okButton: '@',
            okCallback: '&',
            okOnly: '=',
            cancelButton: '@',
            cancelCallback: '&',
            validate: "=",
            api: '=',
            data: '=',
            config: "="
        },

        controller: function($scope) {
            $scope.actions = {
                init: function() {},
                cleanup: function() {},
                close: function() {
                    $scope.$apply($scope.validationStarted = false);
                    $scope.actions.cleanup();
                    $scope.show = false;
                    $scope.$apply($scope.show);
                },
                cancel: function() {
                    $scope.cancelCallback({dialogScope: $scope});
                    $scope.actions.close();
                },
                ok: function() {
                    $scope.$apply($scope.validationStarted = true);
                    var resultOkCallback = $scope.okCallback({dialogScope: $scope});
                    if(($scope.validate === true && resultOkCallback === true) || $scope.validate !== true) {
                        $scope.actions.close();
                    }
                }
            };
        },

        link: function(scope, element) {
            var dialog;

            var initDialog = function() {
                dialog = new ExclusivePopupWithButtons(
                    scope.heading,
                    scope.actions.cancel,
                    false,
                    false,
                    true
                );

                dialog._onClose = function() {};

                dialog._getButtons = function() {
                    var buttons = [];
                    var ok = scope.okButton || $T('Ok');
                    var cancel = scope.cancelButton || $T('Cancel');

                    buttons.push([ok, function() {
                        scope.actions.ok();
                    }]);

                    if (!scope.okOnly) {
                        buttons.push([cancel, function() {
                            scope.actions.cancel();
                        }]);
                    }

                    return buttons;
                };

                dialog.draw = function() {
                    return this.ExclusivePopupWithButtons.prototype.draw.call(this, element);
                };

                dialog.postDraw = function() {
                    this.canvas.dialog('option', 'draggable', false);
                    this.canvas.dialog('option', 'position', 'center');
                };
            };

            var openDialog = function() {
                if (dialog) {
                    showDialog();
                } else {
                    $http.get(
                        scope.templateUrl, {}
                    ).success(function(response, status, header, config) {
                        element = angular.element(response);
                        $compile(element)(scope);
                        $timeout(function(){
                            initDialog();
                            showDialog();
                        }, 0);
                    }). error(function(data, status, header, config) {
                        scope.show = false;
                        var msg = $T('The content of this dialog is currently unavailable.');
                        new AlertPopup(scope.heading, msg).open();
                    });
                }
            };

            var showDialog = function() {
                scope.actions.init();
                dialog.open();
            };

            scope.setSelectedTab = function(tab_id) {
                scope.tabSelected = tab_id;
            };

            scope.isTabSelected = function(tab_id) {
                return scope.tabSelected === tab_id;
            };

            scope.$watch("show", function(val) {
                if (scope.show === true) {
                    openDialog();
                } else if (dialog) {
                    dialog.close();
                }
            });
        }
    };
});

ndDirectives.directive("contenteditable", function() {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, elem, attrs, ctrl) {
            scope.edition = false;
            scope.multiline = scope.$eval(attrs.ndMultiline) || false;

            var sanitizeHtml = function() {
                var sanitized = elem.html().replace(/<div[^<]*?>/g, '
').replace(/<\/div[^<]*?>/g, '');
                sanitized = sanitized.replace(/^(
)*/, '');
                sanitized = sanitized.replace(/(
)*$/, '');
                elem.html(sanitized);
            };

            var updateHtml = function() {
                if (ctrl.$viewValue === '') {
                    elem.html(attrs.placeholder);
                    elem.addClass('empty');
                } else {
                    elem.html(ctrl.$viewValue);
                }
            };

            var getOneLineHeight = function() {
                var html = elem.html();
                var height = elem.html('').css('height');
                elem.html(html);
                return height;
            };

            var actions = {
                init: function() {
                    elem.addClass('focus');
                    elem.actioninput('setIconsVisibility', 'visible');
                    elem.actioninput('setEmptyValue', elem.html());
                    elem.actioninput('initSize', elem.css('font-size'), getOneLineHeight());
                    elem.removeClass('empty');

                    if (ctrl.$viewValue === '' && !scope.edition) {
                        elem.html('');
                    }

                    scope.edition = true;
                },
                close: function() {
                    elem.removeClass('focus');
                    elem.actioninput('setIconsVisibility', 'hidden');
                    scope.edition = false;
                    updateHtml();
                }
            };

            // model -> view
            ctrl.$render = function() {
                updateHtml();
            };

            // creation
            elem.actioninput({
                focusAfter: false,
                enterKeyEnabled: !scope.multiline,

                // view -> model
                callback: function() {
                    actions.close();
                },

                // view -> model
                actionCallback: function() {
                    sanitizeHtml();
                    scope.$apply(function() {
                        ctrl.$setViewValue(elem.html());
                    });
                    actions.close();
                },

                // angular -> jquery
                onchange: function() {
                    elem.val(elem.html());
                }
            });

            // input init
            elem.on('focus', function() {
                actions.init();
            });

            elem.actioninput('setIconsVisibility', 'hidden');
        }
    };
});

ndDirectives.directive('ndDatepicker', function($compile) {
    return {
        restrict: 'E',
        replace: 'true',
        template: '<ng-form name="dateForm"></ng-form>',
        scope: {
            dateFormat: '@',
            hiddenInputs: '@',
            required: '@',
            showTime: '=',
            validation: '=',
            value: '='
        },

        link: function(scope, element) {
            var hiddenInputs =
                scope.$eval(scope.hiddenInputs) ||
                ['day', 'month', 'year', 'hour', 'min'];

            var mapMomentFormat = function(format) {
                format = format.replace('%Y', 'YYYY');
                format = format.replace('%m', 'MM');
                format = format.replace('%d', 'DD');
                format = format.replace('%H', 'HH');
                format = format.replace('%M', 'mm');
                return format;
            };

            var updateModel = function() {
                _.each(element.find('input'), function(e) {
                    $(e).trigger('input');
                });
            };

            var getAttributes = function() {
                var attributes = {};

                attributes['ng-class'] = "{hasError: validation && dateForm.$invalid}";
                if (scope.required) {
                    attributes.required = "required";
                }

                return attributes;
            };

            var getDatetime = function() {
                if (scope.value) {
                    return moment('{0} {1}'.format(scope.value.date, scope.value.time), 'YYYY-MM-DD HH:mm:ss');
                } else {
                    return null;
                }
            };

            var createDatePicker = function() {
                return IndicoUI.Widgets.Generic.dateField(
                    scope.showTime,
                    getAttributes(),
                    hiddenInputs,
                    null,
                    scope.dateFormat,
                    updateModel
                );
            };

            var getInputHtml = function(inputId) {
                var required = '';
                var value = null;

                if (inputId.match('Year') !== null) {
                    required = 'ng-required="{{required}}"';
                }

                if (scope.value) {
                    if (inputId.match('Year') !== null) {
                        value = getDatetime().year();
                    } else if (inputId.match('Month') !== null) {
                        value = getDatetime().month() + 1;
                    } else if (inputId.match('Day') !== null) {
                        value = getDatetime().date();
                    } else if (inputId.match('Hour') !== null) {
                        value = getDatetime().hour();
                    } else if (inputId.match('Min') !== null) {
                        value = getDatetime().minute();
                    }
                }

                return '<input type="text" name="{0}" id="{0}" ng-model="{0}" ng-init="{0}={1}" {2} style="display: none"/>'.format(inputId, value, required);
            };

            var init = function() {
                var datepicker = createDatePicker();

                element.html(datepicker.dom);
                _.each(hiddenInputs, function(inputId) {
                    element.append(getInputHtml(inputId));
                });

                if (scope.value) {
                    datepicker.set(getDatetime().format(mapMomentFormat(scope.dateFormat)));
                }

                $compile(element)(scope);
            };

            scope.$watch('dateFormat', function(newVal, oldVal) {
                init();
            });

            init();
        }
    };
});

ndDirectives.directive('ndConfirmationPopup', function() {
    return {
        restrict: 'E',
        scope: {
            heading: '@',
            content: '@',
            callback: '=',
            show: '='
        },

        link: function(scope) {
            var open = function() {
                var popup = new ConfirmPopup(scope.heading, scope.content, scope.callback);
                popup.open();
            };

            scope.$watch('show', function() {
                if (scope.show === true) {
                    scope.show = false;
                    open();
                }
            });
        }
    };
});

ndDirectives.directive('ndValidFile', function() {
    return {
        require: 'ngModel',
        link: function (scope, el, attrs, ngModel) {
            ngModel.$render = function () {
                ngModel.$setViewValue(el.val());
            };

            el.bind('change', function() {
                scope.$apply(function() {
                    ngModel.$render();
                });
            });
        }
    };
});

ndDirectives.directive('ndRadioExtend', function($rootScope) {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, element) {
            element.bind('click', function() {
                $rootScope.$$phase || $rootScope.$apply();
            });
        }
    };
});

ndDirectives.directive('ndBlacklist', function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
            var blacklist = attrs.ndBlacklist.split(',');

            // DOM -> Model validation
            ngModel.$parsers.unshift(function(value) {
                var valid = blacklist.indexOf(value) === -1;
                ngModel.$setValidity('blacklist', valid);
                return valid ? value : undefined;
            });

            // model -> DOM validation
            ngModel.$formatters.unshift(function(value) {
                ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
                return value;
            });
        }
    };
});

ndDirectives.directive('ndInitFocus', function() {
    var timer;

    return function(scope, element, attrs) {
        if (timer) clearTimeout(timer);

        timer = setTimeout(function() {
            element.focus();
        }, 0);
    };
});
<|endoftext|>


<commit_before>var ce = chrome.extension;
var ct = chrome.tabs;
var root_url = ce.getURL('');
var popup_url = ce.getURL('popup.html');

chrome.runtime.onMessage.addListener(function(request, sender) {
	if (request.act === 'draw_attention') {
		if (! sender || ! sender.tab || ! sender.tab.windowId) return;
		chrome.windows.update(sender.tab.windowId, {
			drawAttention: true
		});
	} else if (request.act === 'stop_drawing_attention') {
		if (! sender || ! sender.tab || ! sender.tab.windowId) return;
		chrome.windows.update(sender.tab.windowId, {
			drawAttention: false
		});
	} else {
		chrome.tabs.query({
			url: chrome.extension.getURL('/popup.html?new_window=true')
		}, function(tabs) {
			tabs.forEach(function(tab) {
				if (! sender.tab) {
					chrome.tabs.remove(tab.id);
				}
			});
		});
	}
});

function createTab(url) {
	ct.create({
		url: url,
		selected: true
	});
}

function updateDetails(flag) {
	var user = Ripple(PREFiX.accessToken);
	var verify = user.verify().next(function(details) {
		lscache.set('account_details', details);
		PREFiX.account = details;
	});
	if (flag) {
		// 延时重试
		verify.
		error(function() {
			setTimeout(function() {
				updateDetails(flag);
			}, 60000);
		});
	}
	return verify;
}

function closeTab(id) {
	ct.remove(id);
}

function closeWindow(id) {
	chrome.windows.remove(id);
}

function update() {
	clearInterval(PREFiX.interval);
	PREFiX.interval = setInterval(update, 30000);

	var tl = PREFiX.homeTimeline;
	var statuses = fixStatusList(tl.statuses.concat(tl.buffered));
	var newest_status = statuses[0];
	var deferred_new = Deferred.next();

	chrome.browserAction.setBadgeText({
		text: '...'
	});
	chrome.browserAction.setBadgeBackgroundColor({
		color: [ 255, 255, 255, 200 ]
	});
	chrome.browserAction.setTitle({
		title: 'PREFiX - 正在刷新'
	});

	if (newest_status) {
		deferred_new = PREFiX.user.getHomeTimeline({
			since_id: newest_status.id
		}).setupAjax({
			lock: update
		}).next(function(statuses) {
			unshift(tl.buffered, statuses);
		});
	}
	var deferred_notification = PREFiX.user.getNotification().next(function(data) {
		PREFiX.previous_count = PREFiX.count;
		PREFiX.count = data;
	});
	Deferred.parallel(deferred_new, deferred_notification).next(function() {
		var need_notify = false;
		var title = [ 'PREFiX' ];

		var new_statuses = tl.buffered.filter(function(status) {
			return ! status.is_self;
		});
		if (new_statuses.length) {
			title.push(new_statuses.length + ' 条新消息');
		}
		if (PREFiX.count.mentions) {
			switchTo('mentions_model');
			title.push('你被 @ 了 ' + PREFiX.count.mentions + ' 次');
			chrome.browserAction.setBadgeBackgroundColor({
				color: [ 113, 202, 224, 204 ]
			});
			if (PREFiX.count.mentions > PREFiX.previous_count.mentions)
				need_notify = true;
		}
		if (PREFiX.count.direct_messages) {
			switchTo('privatemsgs_model');
			title.push('你有 ' + PREFiX.count.direct_messages + ' 条未读私信');
			chrome.browserAction.setBadgeBackgroundColor({
				color: [ 211, 0, 4, 204 ]
			});
			if (PREFiX.count.direct_messages > PREFiX.previous_count.direct_messages)
				need_notify = true;
		}
		chrome.browserAction.setBadgeText({
			text: (PREFiX.count.direct_messages || PREFiX.count.mentions || '') + ''
		});
		chrome.browserAction.setTitle({
			title: title.join('\n')
		});
		if (need_notify) playSound();
	}).error(function(e) {
		chrome.browserAction.setBadgeText({
			text: ' '
		});
		chrome.browserAction.setBadgeBackgroundColor({
			color: [ 255, 0, 0, 200 ]
		});
		chrome.browserAction.setTitle({
			title: 'PREFiX - 网络连接断开或内部错误'
		});
	});
}

function loadFriends() {
	var friends = {};
	[ 'Friends', 'Followers' ].forEach(function(type) {
		(function get(page) {
			PREFiX.user['getLatestLogged' + type]({
				page: page,
				count: 100
			}).next(function(data) {
				if (data.length) {
					data = data.map(function(user) {
						return {
							name: user.name,
							string: user.id + ' ' + user.name
						};
					}).filter(function(user) {
						if (friends[user.name]) return false;
						friends[user.name] = true;
						return true;
					});
					PREFiX.friends.push.apply(PREFiX.friends, data);
					get(page + 1);
				}
			});
		})(1);
	});
}

var init_interval;

function load() {
	if (PREFiX.loaded) return;
	PREFiX.loaded = true;
	PREFiX.count = {
		mentions: 0,
		direct_messages: 0
	};
	PREFiX.friends = [],
	PREFiX.user = Ripple(PREFiX.accessToken);
	var init_data = function() {
		PREFiX.user.getHomeTimeline().next(function(statuses) {
			if (! PREFiX.homeTimeline.statuses.length) {
				PREFiX.homeTimeline.statuses = fixStatusList(statuses);
			}
			clearInterval(init_interval);
		});
	};
	init_interval = setInterval(init_data, 15 * 1000);
	init_data();
	update();
	loadFriends();
}

function unload() {
	if (! PREFiX.loaded) return;
	clearInterval(PREFiX.interval);
	clearInterval(init_interval);
	PREFiX.loaded = false;
	PREFiX.user = null;
	PREFiX.current = 'tl_model';
	PREFiX.compose = {
		text: '',
		type: '',
		id: '',
		user: ''
	};
	PREFiX.count = {
		mentions: 0,
		direct_messages: 0
	};
	PREFiX.homeTimeline = {
		statuses: [],
		buffered: [],
		scrollTop: 0
	};
	PREFiX.mentions = {
		statuses: [],
		scrollTop: 0
	};
	PREFiX.privatemsgs = {
		messages: [],
		scrollTop: 0
	};
	PREFiX.friends = [];
	chrome.browserAction.setBadgeText({
		text: ''
	});
	chrome.browserAction.setTitle({
		title: 'PREFiX'
	});
	chrome.browserAction.setIcon({
		path: '/icons/19.png'
	});
}

function initialize() {
	settings.load();

	if (PREFiX.accessToken) {
		// 更新账户信息
		updateDetails().
		next(function() {
			// 成功
			load();
		}).
		error(function(event) {
			if (event.status) {
				if (event.status === 401) {
					// access token 无效
					reset();
				} else {
					// 可能 API Hits 用光了, 延时重试
					setTimeout(initialize, 60000);
				}
			} else {
				// 网络错误
				if (PREFiX.account) {
					// 如果本地存在缓存的账户信息,
					// 则先使用缓存, 等一会再重试
					load();
					setTimeout(function() {
						updateDetails(true);
					}, 60000);
				} else {
					// 如果不存在, 则稍后再重试
					setTimeout(initialize, 60000);
				}
			}
		});

		return;
	}

	var tab_id, tab_port;
	Ripple.authorize.withPINCode(function(auth_url) {
		var options = {
			url: auth_url,
			selected: true
		};
		var deferred = Deferred();

		// 打开验证页面
		ct.create(options, function(tab) {

			ct.onUpdated.addListener(function onUpdated(id, info) {
				// 等待用户点击 '授权' 后跳转至 PIN Code 页面
				if (id !== tab.id) return;
				tab_id = id;

				// 继续验证操作
				ct.executeScript(id, {
					file: 'js/authorize.js'
				}, function() {
					// 等待页面传送 PIN Code
					var port = ct.connect(id);
					port.onMessage.addListener(function listenForPINCode(msg) {
						var pin_code = msg.pinCode;
						tab_port = port;
						// 如果页面端没有拿到 PIN Code, 会传送 'rejected' 消息过来
						deferred[pin_code == 'rejected' ? 'fail' : 'call'](pin_code);

						ct.onUpdated.removeListener(onUpdated);
						tab_port.onMessage.removeListener(listenForPINCode);
					});
				});

				ct.insertCSS(id, {
					code: '#retry { text-decoration: underline; }' +
								'#retry:hover { cursor: pointer; }'
				});
			});

		});

		// 返回 Deferred, 当拿到 PIN Code 后会继续后面的操作
		return deferred;
	}).
	next(function(token) {
		// 成功拿到 access token
		tab_port.postMessage({
			type: 'authorize',
			msg: 'success'
		});

		// 把 access token 缓存下来并重启程序
		lscache.set('access_token', token);
		PREFiX.accessToken = token;
		initialize();

		setTimeout(function() {
			closeTab(tab_id);
		}, 5000);
	}).
	error(function(error) {
		if (Ripple.getConfig('dumpLevel') > 0) {
			console.log(error);
		}
		if (tab_port) {
			// 打开了验证页面, 却没有完成验证
			tab_port.postMessage('failure');
			tab_port.onMessage.addListener(function(msg) {
				// 等待用户点击 '重试'
				if (msg.type === 'authorize' && msg.msg === 'retry') {
					closeTab(tab_id);
					initialize();
				}
			});
		} else {
			// 可能由于网络错误, 导致验证地址没有成功获取
			setTimeout(initialize, 60000);
		}
	});

}

// 清理所有与当前用户有关的数据, 恢复到未加载状态
function reset() {
	PREFiX.unload();
	PREFiX.accessToken = PREFiX.account = PREFiX.user = null;
	lscache.remove('access_token');
	lscache.remove('account_details');
	initialize();
}

function switchTo(model) {
	if (! PREFiX.popupActive) {
		PREFiX.current = model;
	}
}

var playSound = (function() {
	var audio = new Audio;
	audio.src = 'dongdong.mp3';
	var timeout;
	var last_played = new Date;
	last_played.setFullYear(1970);
	return function() {
		clearTimeout(timeout);
		timeout = setTimeout(function() {
			if (audio.networkState !== 1)
				return playSound();
			var now = new Date;
			if (now - last_played < 15 * 1000)
				return;
			last_played = now;
			audio.play();
		}, 50);
	}
})();

var settings = {
	current: { },
	default: {
		playSound: true
	},
	load: function() {
		var local_settings = lscache.get('settings') || { };
		var current = settings.current;
		for (var key in settings.default) {
			current[key] = local_settings[key] === undefined ?
				settings.default[key] : local_settings[key];
		}
	},
	save: function() {
		lscache.set('settings', settings.current);
	}
};

var PREFiX = this.PREFiX = {
	version: chrome.app.getDetails().version,
	load: load,
	unload: unload,
	initialize: initialize,
	reset: reset,
	update: update,
	loaded: false,
	showExtHomePage: function() {
		//createTab('https://chrome.google.com/webstore/detail/fkabhbjhcdoccohpojphgofmlljekcgg/reviews');
	},
	interval: null,
	current: 'tl_model',
	compose: {
		text: '',
		type: '',
		id: '',
		user: ''
	},
	count: {
		mentions: 0,
		direct_messages: 0
	},
	previous_count: {
		mentions: 0,
		direct_messages: 0
	},
	homeTimeline: {
		statuses: [],
		buffered: [],
		scrollTop: 0
	},
	mentions: {
		statuses: [],
		scrollTop: 0
	},
	privatemsgs: {
		messages: [],
		scrollTop: 0
	},
	friends: [],
	settings: settings,
	account: lscache.get('account_details'), // 当前账号的数据, 如昵称头像等
	accessToken: lscache.get('access_token'), // 缓存的 access token, 与饭否服务器联络的凭证
	user: null // 一个 Ripple 实例, 提供所有 API 接口
};

initialize();<commit_msg>修正提示音不能关闭的 bug<commit_after>var ce = chrome.extension;
var ct = chrome.tabs;
var root_url = ce.getURL('');
var popup_url = ce.getURL('popup.html');

chrome.runtime.onMessage.addListener(function(request, sender) {
	if (request.act === 'draw_attention') {
		if (! sender || ! sender.tab || ! sender.tab.windowId) return;
		chrome.windows.update(sender.tab.windowId, {
			drawAttention: true
		});
	} else if (request.act === 'stop_drawing_attention') {
		if (! sender || ! sender.tab || ! sender.tab.windowId) return;
		chrome.windows.update(sender.tab.windowId, {
			drawAttention: false
		});
	} else {
		chrome.tabs.query({
			url: chrome.extension.getURL('/popup.html?new_window=true')
		}, function(tabs) {
			tabs.forEach(function(tab) {
				if (! sender.tab) {
					chrome.tabs.remove(tab.id);
				}
			});
		});
	}
});

function createTab(url) {
	ct.create({
		url: url,
		selected: true
	});
}

function updateDetails(flag) {
	var user = Ripple(PREFiX.accessToken);
	var verify = user.verify().next(function(details) {
		lscache.set('account_details', details);
		PREFiX.account = details;
	});
	if (flag) {
		// 延时重试
		verify.
		error(function() {
			setTimeout(function() {
				updateDetails(flag);
			}, 60000);
		});
	}
	return verify;
}

function closeTab(id) {
	ct.remove(id);
}

function closeWindow(id) {
	chrome.windows.remove(id);
}

function update() {
	clearInterval(PREFiX.interval);
	PREFiX.interval = setInterval(update, 30000);

	var tl = PREFiX.homeTimeline;
	var statuses = fixStatusList(tl.statuses.concat(tl.buffered));
	var newest_status = statuses[0];
	var deferred_new = Deferred.next();

	chrome.browserAction.setBadgeText({
		text: '...'
	});
	chrome.browserAction.setBadgeBackgroundColor({
		color: [ 255, 255, 255, 200 ]
	});
	chrome.browserAction.setTitle({
		title: 'PREFiX - 正在刷新'
	});

	if (newest_status) {
		deferred_new = PREFiX.user.getHomeTimeline({
			since_id: newest_status.id
		}).setupAjax({
			lock: update
		}).next(function(statuses) {
			unshift(tl.buffered, statuses);
		});
	}
	var deferred_notification = PREFiX.user.getNotification().next(function(data) {
		PREFiX.previous_count = PREFiX.count;
		PREFiX.count = data;
	});
	Deferred.parallel(deferred_new, deferred_notification).next(function() {
		var need_notify = false;
		var title = [ 'PREFiX' ];

		var new_statuses = tl.buffered.filter(function(status) {
			return ! status.is_self;
		});
		if (new_statuses.length) {
			title.push(new_statuses.length + ' 条新消息');
		}
		if (PREFiX.count.mentions) {
			switchTo('mentions_model');
			title.push('你被 @ 了 ' + PREFiX.count.mentions + ' 次');
			chrome.browserAction.setBadgeBackgroundColor({
				color: [ 113, 202, 224, 204 ]
			});
			if (PREFiX.count.mentions > PREFiX.previous_count.mentions)
				need_notify = true;
		}
		if (PREFiX.count.direct_messages) {
			switchTo('privatemsgs_model');
			title.push('你有 ' + PREFiX.count.direct_messages + ' 条未读私信');
			chrome.browserAction.setBadgeBackgroundColor({
				color: [ 211, 0, 4, 204 ]
			});
			if (PREFiX.count.direct_messages > PREFiX.previous_count.direct_messages)
				need_notify = true;
		}
		chrome.browserAction.setBadgeText({
			text: (PREFiX.count.direct_messages || PREFiX.count.mentions || '') + ''
		});
		chrome.browserAction.setTitle({
			title: title.join('\n')
		});
		if (need_notify) playSound();
	}).error(function(e) {
		chrome.browserAction.setBadgeText({
			text: ' '
		});
		chrome.browserAction.setBadgeBackgroundColor({
			color: [ 255, 0, 0, 200 ]
		});
		chrome.browserAction.setTitle({
			title: 'PREFiX - 网络连接断开或内部错误'
		});
	});
}

function loadFriends() {
	var friends = {};
	[ 'Friends', 'Followers' ].forEach(function(type) {
		(function get(page) {
			PREFiX.user['getLatestLogged' + type]({
				page: page,
				count: 100
			}).next(function(data) {
				if (data.length) {
					data = data.map(function(user) {
						return {
							name: user.name,
							string: user.id + ' ' + user.name
						};
					}).filter(function(user) {
						if (friends[user.name]) return false;
						friends[user.name] = true;
						return true;
					});
					PREFiX.friends.push.apply(PREFiX.friends, data);
					get(page + 1);
				}
			});
		})(1);
	});
}

var init_interval;

function load() {
	if (PREFiX.loaded) return;
	PREFiX.loaded = true;
	PREFiX.count = {
		mentions: 0,
		direct_messages: 0
	};
	PREFiX.friends = [],
	PREFiX.user = Ripple(PREFiX.accessToken);
	var init_data = function() {
		PREFiX.user.getHomeTimeline().next(function(statuses) {
			if (! PREFiX.homeTimeline.statuses.length) {
				PREFiX.homeTimeline.statuses = fixStatusList(statuses);
			}
			clearInterval(init_interval);
		});
	};
	init_interval = setInterval(init_data, 15 * 1000);
	init_data();
	update();
	loadFriends();
}

function unload() {
	if (! PREFiX.loaded) return;
	clearInterval(PREFiX.interval);
	clearInterval(init_interval);
	PREFiX.loaded = false;
	PREFiX.user = null;
	PREFiX.current = 'tl_model';
	PREFiX.compose = {
		text: '',
		type: '',
		id: '',
		user: ''
	};
	PREFiX.count = {
		mentions: 0,
		direct_messages: 0
	};
	PREFiX.homeTimeline = {
		statuses: [],
		buffered: [],
		scrollTop: 0
	};
	PREFiX.mentions = {
		statuses: [],
		scrollTop: 0
	};
	PREFiX.privatemsgs = {
		messages: [],
		scrollTop: 0
	};
	PREFiX.friends = [];
	chrome.browserAction.setBadgeText({
		text: ''
	});
	chrome.browserAction.setTitle({
		title: 'PREFiX'
	});
	chrome.browserAction.setIcon({
		path: '/icons/19.png'
	});
}

function initialize() {
	settings.load();

	if (PREFiX.accessToken) {
		// 更新账户信息
		updateDetails().
		next(function() {
			// 成功
			load();
		}).
		error(function(event) {
			if (event.status) {
				if (event.status === 401) {
					// access token 无效
					reset();
				} else {
					// 可能 API Hits 用光了, 延时重试
					setTimeout(initialize, 60000);
				}
			} else {
				// 网络错误
				if (PREFiX.account) {
					// 如果本地存在缓存的账户信息,
					// 则先使用缓存, 等一会再重试
					load();
					setTimeout(function() {
						updateDetails(true);
					}, 60000);
				} else {
					// 如果不存在, 则稍后再重试
					setTimeout(initialize, 60000);
				}
			}
		});

		return;
	}

	var tab_id, tab_port;
	Ripple.authorize.withPINCode(function(auth_url) {
		var options = {
			url: auth_url,
			selected: true
		};
		var deferred = Deferred();

		// 打开验证页面
		ct.create(options, function(tab) {

			ct.onUpdated.addListener(function onUpdated(id, info) {
				// 等待用户点击 '授权' 后跳转至 PIN Code 页面
				if (id !== tab.id) return;
				tab_id = id;

				// 继续验证操作
				ct.executeScript(id, {
					file: 'js/authorize.js'
				}, function() {
					// 等待页面传送 PIN Code
					var port = ct.connect(id);
					port.onMessage.addListener(function listenForPINCode(msg) {
						var pin_code = msg.pinCode;
						tab_port = port;
						// 如果页面端没有拿到 PIN Code, 会传送 'rejected' 消息过来
						deferred[pin_code == 'rejected' ? 'fail' : 'call'](pin_code);

						ct.onUpdated.removeListener(onUpdated);
						tab_port.onMessage.removeListener(listenForPINCode);
					});
				});

				ct.insertCSS(id, {
					code: '#retry { text-decoration: underline; }' +
								'#retry:hover { cursor: pointer; }'
				});
			});

		});

		// 返回 Deferred, 当拿到 PIN Code 后会继续后面的操作
		return deferred;
	}).
	next(function(token) {
		// 成功拿到 access token
		tab_port.postMessage({
			type: 'authorize',
			msg: 'success'
		});

		// 把 access token 缓存下来并重启程序
		lscache.set('access_token', token);
		PREFiX.accessToken = token;
		initialize();

		setTimeout(function() {
			closeTab(tab_id);
		}, 5000);
	}).
	error(function(error) {
		if (Ripple.getConfig('dumpLevel') > 0) {
			console.log(error);
		}
		if (tab_port) {
			// 打开了验证页面, 却没有完成验证
			tab_port.postMessage('failure');
			tab_port.onMessage.addListener(function(msg) {
				// 等待用户点击 '重试'
				if (msg.type === 'authorize' && msg.msg === 'retry') {
					closeTab(tab_id);
					initialize();
				}
			});
		} else {
			// 可能由于网络错误, 导致验证地址没有成功获取
			setTimeout(initialize, 60000);
		}
	});

}

// 清理所有与当前用户有关的数据, 恢复到未加载状态
function reset() {
	PREFiX.unload();
	PREFiX.accessToken = PREFiX.account = PREFiX.user = null;
	lscache.remove('access_token');
	lscache.remove('account_details');
	initialize();
}

function switchTo(model) {
	if (! PREFiX.popupActive) {
		PREFiX.current = model;
	}
}

var playSound = (function() {
	var audio = new Audio;
	audio.src = 'dongdong.mp3';
	var timeout;
	var last_played = new Date;
	last_played.setFullYear(1970);
	return function() {
		clearTimeout(timeout);
		if (! settings.current.playSound)
			return;
		timeout = setTimeout(function() {
			if (audio.networkState !== 1)
				return playSound();
			var now = new Date;
			if (now - last_played < 15 * 1000)
				return;
			last_played = now;
			audio.play();
		}, 50);
	}
})();

var settings = {
	current: { },
	default: {
		playSound: true
	},
	load: function() {
		var local_settings = lscache.get('settings') || { };
		var current = settings.current;
		for (var key in settings.default) {
			current[key] = local_settings[key] === undefined ?
				settings.default[key] : local_settings[key];
		}
	},
	save: function() {
		lscache.set('settings', settings.current);
	}
};

var PREFiX = this.PREFiX = {
	version: chrome.app.getDetails().version,
	load: load,
	unload: unload,
	initialize: initialize,
	reset: reset,
	update: update,
	loaded: false,
	showExtHomePage: function() {
		//createTab('https://chrome.google.com/webstore/detail/fkabhbjhcdoccohpojphgofmlljekcgg/reviews');
	},
	interval: null,
	current: 'tl_model',
	compose: {
		text: '',
		type: '',
		id: '',
		user: ''
	},
	count: {
		mentions: 0,
		direct_messages: 0
	},
	previous_count: {
		mentions: 0,
		direct_messages: 0
	},
	homeTimeline: {
		statuses: [],
		buffered: [],
		scrollTop: 0
	},
	mentions: {
		statuses: [],
		scrollTop: 0
	},
	privatemsgs: {
		messages: [],
		scrollTop: 0
	},
	friends: [],
	settings: settings,
	account: lscache.get('account_details'), // 当前账号的数据, 如昵称头像等
	accessToken: lscache.get('access_token'), // 缓存的 access token, 与饭否服务器联络的凭证
	user: null // 一个 Ripple 实例, 提供所有 API 接口
};

initialize();<|endoftext|>