| Index: lib/icon.js | 
| diff --git a/lib/icon.js b/lib/icon.js | 
| index 2011d1e23701aada50df48f3f2722514d3b3b24e..75b459490757126c574ca765f4fe315edc465b26 100644 | 
| --- a/lib/icon.js | 
| +++ b/lib/icon.js | 
| @@ -17,73 +17,142 @@ | 
|  | 
| /** @module icon */ | 
|  | 
| -const numberOfFrames = 10; | 
| +"use strict"; | 
|  | 
| -let whitelistedState = new ext.PageMap(); | 
| -let notificationType = null; | 
| +const frameOpacities =  [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, | 
| +                         1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, | 
| +                         0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]; | 
| +const numberOfFrames = frameOpacities.length; | 
| +const safariPlatform = require("info").platform == "safari"; | 
| + | 
| +let frameInterval = null; | 
| let animationInterval = null; | 
| -let animationStep = 0; | 
| +let whitelistedState = new ext.PageMap(); | 
|  | 
| -function getIconPath(whitelisted) | 
| +function loadImage(url) | 
| { | 
| -  let filename = "icons/abp-$size"; | 
| +  return new Promise((resolve, reject) => | 
| +  { | 
| +    let image = new Image(); | 
| +    image.src = url; | 
| +    image.addEventListener("load", () => | 
| +    { | 
| +      resolve(image); | 
| +    }); | 
| +    image.addEventListener("error", () => | 
| +    { | 
| +      reject("Failed to load image " + url); | 
| +    }); | 
| +  }); | 
| +}; | 
|  | 
| -  // If the current page is whitelisted, pick an icon that indicates that | 
| -  // Adblock Plus is disabled, however not when the notification icon has | 
| -  // full opacity, or on Safari where icons are genrally grayscale-only. | 
| -  if (whitelisted && animationStep < numberOfFrames && require("info").platform != "safari") | 
| -    filename += "-whitelisted"; | 
| +function setIcon(page, notificationType, opacity, frames) | 
| +{ | 
| +  opacity = opacity || 0; | 
| +  let whitelisted = !!whitelistedState.get(page) && !safariPlatform; | 
|  | 
| -  // If the icon is currently animating to indicate a pending notification, | 
| -  // pick the icon for the corresponing notification type and animation frame. | 
| -  if (notificationType && animationStep > 0) | 
| +  if (!notificationType || !frames) | 
| { | 
| -    filename += "-notification-" + notificationType; | 
| - | 
| -    if (animationStep < numberOfFrames) | 
| -      filename += "-" + animationStep; | 
| +    if (opacity > 0.5) | 
| +      page.browserAction.setIcon("icons/abp-$size-notification-" | 
| +                                 + notificationType + ".png"); | 
| +    else | 
| +      page.browserAction.setIcon("icons/abp-$size" + | 
| +                                 (whitelisted ? "-whitelisted" : "") + ".png"); | 
| +  } | 
| +  else | 
| +  { | 
| +    chrome.browserAction.setIcon({ | 
| +      tabId: page._id, | 
| +      imageData: frames["" + opacity + whitelisted] | 
| +    }); | 
| } | 
| - | 
| -  return filename + ".png"; | 
| } | 
|  | 
| -function setIcon(page) | 
| +function renderFrames(notificationType) | 
| { | 
| -  page.browserAction.setIcon(getIconPath(whitelistedState.get(page))); | 
| +  if (safariPlatform) | 
| +    return Promise.resolve(null); | 
| + | 
| +  return Promise.all([ | 
| +    loadImage("icons/abp-19.png"), | 
| +    loadImage("icons/abp-19-whitelisted.png"), | 
| +    loadImage("icons/abp-19-notification-" + notificationType + ".png"), | 
| +    loadImage("icons/abp-38.png"), | 
| +    loadImage("icons/abp-38-whitelisted.png"), | 
| +    loadImage("icons/abp-38-notification-" + notificationType + ".png"), | 
| +  ]).then(images => | 
| +  { | 
| +    let images = { | 
| +      19: { base: [images[0], images[1]], overlay: images[2] }, | 
| +      38: { base: [images[3], images[4]], overlay: images[5] } | 
| +    }; | 
| + | 
| +    let frames = {}; | 
| +    let canvas = document.createElement("canvas"); | 
| +    let context = canvas.getContext("2d"); | 
| + | 
| +    for (let whitelisted of [false, true]) | 
| +    { | 
| +      for (let i = 0, opacity = 0; i <= 10; opacity = ++i / 10) | 
| +      { | 
| +        let imageData = {}; | 
| +        for (let size of [19, 38]) | 
| +        { | 
| +          canvas.width = size; | 
| +          canvas.height = size; | 
| +          context.globalAlpha = 1; | 
| +          context.drawImage(images[size]["base"][whitelisted | 0], 0, 0); | 
| +          context.globalAlpha = opacity; | 
| +          context.drawImage(images[size]["overlay"], 0, 0); | 
| +          imageData[size] = context.getImageData(0, 0, size, size); | 
| +        } | 
| +        frames["" + opacity + whitelisted] = imageData; | 
| +      } | 
| +    } | 
| + | 
| +    return frames; | 
| +  }); | 
| } | 
|  | 
| -function runAnimation() | 
| +function runAnimation(notificationType) | 
| { | 
| -  return setInterval(function() | 
| +  function playAnimation(frames) | 
| { | 
| -    ext.pages.query({active: true}, function(pages) | 
| +    let animationStep = 0; | 
| +    ext.pages.query({active: true}, pages => | 
| { | 
| -      let fadeInInterval = setInterval(function() | 
| +      function appendActivePage(page) | 
| { | 
| -        animationStep++; | 
| -        pages.forEach(setIcon); | 
| - | 
| -        if (animationStep < numberOfFrames) | 
| -          return; | 
| +        pages.push(page); | 
| +      } | 
| +      ext.pages.onActivated.addListener(appendActivePage); | 
|  | 
| -        setTimeout(function() | 
| +      frameInterval = setInterval(() => | 
| +      { | 
| +        let opacity = frameOpacities[animationStep++]; | 
| +        for (let page of pages) | 
| { | 
| -          let fadeOutInterval = setInterval(function() | 
| -          { | 
| -            animationStep--; | 
| -            pages.forEach(setIcon); | 
| - | 
| -            if (animationStep > 0) | 
| -              return; | 
| - | 
| -            clearInterval(fadeOutInterval); | 
| -          }, 100); | 
| -        },1000); | 
| +          if (whitelistedState.has(page)) | 
| +            setIcon(page, notificationType, opacity, frames); | 
| +        }; | 
|  | 
| -        clearInterval(fadeInInterval); | 
| +        if (animationStep > numberOfFrames) | 
| +        { | 
| +          animationStep = 0; | 
| +          clearInterval(frameInterval); | 
| +          frameInterval = null; | 
| +          ext.pages.onActivated.removeListener(appendActivePage); | 
| +        } | 
| }, 100); | 
| }); | 
| -  }, 10000); | 
| +  } | 
| + | 
| +  renderFrames(notificationType).then(frames => | 
| +  { | 
| +    playAnimation(frames); | 
| +    animationInterval = setInterval(() => { playAnimation(frames); }, 10000); | 
| +  }); | 
| } | 
|  | 
| /** | 
| @@ -95,33 +164,31 @@ function runAnimation() | 
| */ | 
| exports.updateIcon = function(page, whitelisted) | 
| { | 
| -  page.browserAction.setIcon(getIconPath(whitelisted)); | 
| whitelistedState.set(page, whitelisted); | 
| +  if (frameInterval == null) | 
| +    setIcon(page); | 
| }; | 
|  | 
| -/** | 
| - * Starts to animate the browser action icon to indicate a pending notifcation. | 
| - * | 
| - * @param {string} type  The notification type (i.e: "information" or "critical") | 
| - */ | 
| -exports.startIconAnimation = function(type) | 
| -{ | 
| -  notificationType = type; | 
| - | 
| -  if (animationInterval == null) | 
| -    animationInterval = runAnimation(); | 
| -}; | 
| - | 
| +let stopIconAnimation = | 
| /** | 
| * Stops to animate the browser action icon. | 
| */ | 
| exports.stopIconAnimation = function() | 
| { | 
| +  if (frameInterval != null) | 
| +    clearInterval(frameInterval); | 
| if (animationInterval != null) | 
| -  { | 
| clearInterval(animationInterval); | 
| -    animationInterval = null; | 
| -  } | 
| +  frameInterval = animationInterval = null; | 
| +}; | 
|  | 
| -  notificationType  = null; | 
| +/** | 
| + * Starts to animate the browser action icon to indicate a pending notifcation. | 
| + * | 
| + * @param {string} type  The notification type (i.e: "information" or "critical") | 
| + */ | 
| +exports.startIconAnimation = function(type) | 
| +{ | 
| +  stopIconAnimation(); | 
| +  runAnimation(type); | 
| }; | 
|  |