<?xml version="1.0" encoding="UTF-8" ?>
<Module>
  <ModulePrefs title="Flickr" height="341"
               title_url="http://flickr.com/photos/me/"
               description="Check out your Flickr activity, see your Contacts' recent photos, oggle the latest 'Interesting' photos or search across Flickr's entire gallery. From the hardcore Flickr addict to the non-Flickr user looking to beautify their homepage, this gadget can accomodate."
               screenshot="http://warmbrain.com/gadgets/flickr_v2.jpg"
               thumbnail="http://warmbrain.com/gadgets/flickr_thumb_v2.jpg"
               author_email="dylan.parker+flickr@gmail.com"
               author="Dylan Parker"
               author_location="Victoria BC, Canada"
  >
    <Require feature="dynamic-height" />
    <Require feature="setprefs" />
    <Require feature="tabs" />
    <Require feature="analytics" />
  </ModulePrefs> 
  <UserPref name="num_photo_rows" display_name="# Photo Rows" datatype="enum" default_value="4">
    <EnumValue value="2" display_value="Two" />
    <EnumValue value="3" display_value="Three" />
    <EnumValue value="4" display_value="Four" />
    <EnumValue value="5" display_value="Five" />
  </UserPref>
  <UserPref name="username" datatype="hidden" />
  <UserPref name="NSID" default_value="" datatype="hidden" />
  <UserPref name="search" default_value="refraction" datatype="hidden" />
  <UserPref name="lastSelectedTab" datatype="hidden" default_value="2" />
  
  <!-- V2 -->
  <UserPref name="auth_token" datatype="hidden" />
  <UserPref name="version" default_value="0" datatype="hidden" />
  
  <Content type="html">
     <![CDATA[ 
<style>

.PhotoGrid {
  position:relative;
  overflow:hidden;
  text-align:center;
}

IMG.photoph,IMG.photo {
  background-color:#eaeaea;
  border:0px;
  margin:1px;
}
IMG.photoph {
  width:1px;
  height:1px;
  border-right:74px solid #eaeaea;
  border-bottom:74px solid #eaeaea;
}
IMG.photo {
  width:75px;
  height:75px;
}

.photo_zoom_container,.photo_zoom_dimmer {
  position:absolute;
  width:100%;
  height:100%;
  left:0;
  top:0;
}
.photo_zoom_container {
  z-index:1001;
  font-family:Helvetica,Verdana,sans-serif;
  font-size:80%;
  color: #FF0084;
  font-weight:bold;
}
.photo_zoom_container A {
  color:#0063DC;
}
.photo_zoom_dimmer {
  display:none;
  z-index:1000;
  background-color:white;
  filter:alpha(opacity=85);
  opacity:0.85;
}

.tablib_main_container {
  margin-top:2px;
}
.tablib_unselected, .tablib_spacerTab, .tablib_emptyTab {
  border: 0px;
  border-bottom:1px solid #89b8f2;
}
.tablib_selected, .tablib_unselected {
  font-family:Helvetica,Verdana,sans-serif;
  background-color:#fff;
  font-size:110%;
  padding:4px 0px;
  font-weight:bold;
}
.tablib_selected {
  border-color: #89b8f2;
}
.tablib_unselected {
  padding-bottom:3px;
  padding-top:5px;
  color:#85b4ee;
}
.tablib_unselected .pink {
  color:#ff85c4;
}
.tablib_selected {
  color:#0063DC;
}
.tablib_selected .pink {
  color:#FF0084;
}

#search_button {
  background-color:#0063DC;
  color:white;
  font-weight:bold;
  font-size:80%;
}
#search_box{
  margin-bottom:10px;
}

#ActivityTab {
  font-size:87%;
}

#InterestingTab, #ContactsTab {
  margin-top:12px;
}

 .activity_item {
   margin-top:10px;
   padding-top:12px;
   border-top:1px dotted #ccc;
 }
 .activity_item:first-child {
   margin-top:0px;
   border-top:0px solid #fff;
 }
 
 .activity_item_thumb {
   float:left;
   width:48px;
   height:48px;
   margin-right:12px;
 }
 .activity_item_head {
   font-size:11px;
   padding-top:14px;
 }
 .activity_item_stats {
   color:#999;
 }
 .activity_item_stats A {
   text-decoration:none;
   color:#999;
 }
 .activity_item_stats A:hover {
   color:#1057AE;
   text-decoration:underline;
 }
 .activity_item_title {
   color: #1057AE;
   font-size:15px;
   margin-bottom:4px;
   font-family:Arial,Helvetica,sans-serif;
 }
 .activity_item_title A {
   color:#1057AE;
   text-decoration:none;
 }
 .activity_event {
   font-size:13px;
   clear:both;
   margin-top:10px;
 }
 .mycomment {
   color:#999;
   font-style:oblique;
 }
 .activity_event_thumb {
   float:left;
   margin-top:2px;
 }
 .activity_event_timestamp {
   margin-top:2px;
   font-size:11px;
   float:right;
   color:#999;
 }
 .activity_event_more {
   color:#ff0084;
   text-align:right;
 }
 .activity_event_more A {
   color:#1057Ae;
 }
 .activity_event_body {
   margin-left:25px;
   line-height:18px;
 }
 .activity_event_body IMG {
   border:0;
 }
 .activity_event_body A {
   color:#1057AE 
 }
 .tabdiv {
   display:none;
 }

#auth1, #auth2, #auth3 {
  font-size:87%;
  margin:12px;
  margin-top:20px;
}

#auth_link {
  background-color:#0063DC;
  color:white;
  font-weight:bold;
  text-decoration:none;
  padding:3px 4px 2px 5px;
  font-size:12px;
  border:2px solid #fff;
  border-right-color:#999;
  border-bottom-color:#999;
}


</style>
<div id="status_div"></div>


<div id="ActivityTab" class="tabdiv">
  <div id="activity"></div>
</div>

<div id="ContactsTab" class="tabdiv">
  <div id="ContactsPhotoGrid"></div>
</div>

<div id="InterestingTab" class="tabdiv">
  <div id="InterestingPhotoGrid"></div>
</div>

<div id="SearchTab" style="padding-top:6px" class="tabdiv">
 <div id="search_box">
  <table width="100%"><tr>
   <form style="display:inline" onsubmit="SearchTab.onsearch();return false;">
    <td width="100%"><input type="text" style="width:100%" /></td>
    <td><input id="search_button" type="submit" value="SEARCH" /></td>
   </form>
  </tr></table>
  <div id="SearchTabStatus" style="display:none"></div>
 </div>
 <div id="SearchPhotoGrid"></div>
</div>


<!-- AUTH Steps -->
<div id="auth1" style="display:none">
  <div><b>Authorization Needed</b></div>
  <p></p>
  <div id="firstauth">This gadget needs your <b>authorization</b> to display this information.</div>
  <div id="reauth">It seems this gadget no longer has your <b>authorization</b> to display this information.</div>
  <p></p>
  If you want to use this tab just click the button below to open a new Flickr.com window and start the process.
  <p></p>
  <b><a target="_blank" id="auth_link" href="" onclick="return Gadget.startAuth(this)">BEGIN AUTHORIZATION</a></b>
  <p></p>
  Come on back here once you are done.
</div>
<div id="auth2" style="display:none">
  <div><b>Waiting for you to return...</b></div>
  <p></p>
  Once you have authorized this gadget on Flickr you can return here and click the button below.
  <p></p>
  <button id="search_button" onclick="Gadget.finishAuth(false)">COMPLETE AUTHORIZATION</button>
</div>
<div id="auth3" style="display:none">
  <div style="color:#FF0084"><b>Success!</b></div>
  <p></p>
  You have finished authorization.
  <p></p>
  <button id="search_button" onclick="Gadget.finishAuth(false)">COMPLETE AUTHORIZATION</button>
</div>



<script>
//_IG_Analytics("UA-571684-2", "/flickr_gadget_v2");

// Our Flickr API Object that only impls the methods we need
var Flickr = (function(){
  var api_url = "http://api.flickr.com/services/rest/?";
  var api_key = "8aa99c6da16162fa00d7a5992f28d426";
  var base_url = api_url;
  var SEVEN_DAYS = 604800;
  var ONE_HOUR = 3600;
  var FIFTEEN_MINUTES = 900;

  function get_sig(params) {
    // An MD5 hash of the ordered params and values
    var keys = [];
    for (var key in params) { keys.push(key) }
    keys.sort();
    var param_str = "17641795ce77796d";
    for (var i = 0; i < keys.length; i++) {
      param_str += (keys[i] + params[keys[i]]);
    }
    return param_str.md5();
  }

  // Common method to query Flickr REST interface
  function fetch(method, params, callback, cache_secs, opt_sign) {
    params["api_key"] = api_key;
    params["format"] = "json";
    params["nojsoncallback"] = "1";
    params["method"] = method;
    if (cache_secs == 0) {
      params["rand"] = (Math.random()+"").substring(2,6);
    }
    // Sometimes a bad value gets stored in the /ig/proxy cache.
    // Setting Flickr.nocache allows us to avoid the bad value and create a new cached value.
    if (Flickr.nocache) {
      params["nocache"] = Flickr.nocache;
    }
    
    var url = base_url;
    
    for (var i in params) {
      url += (url[url.length-1] == "?" ? "" : "&");
      url += _esc(i) + "=" + _esc(params[i]);
    }
    if (opt_sign) {
      url += "&api_sig=" + get_sig(params);
    }
    _IG_FetchContent(url, callback, { refreshInterval: cache_secs });
    // Tracking gadget API usage
    _IG_Analytics("UA-571684-2", "/flickr_gadget_v2/api/" + method);
  }

  return {
    MAX_PHOTOS : 100,

    activity : {
      userPhotos : function(timeframe, per_page, page, auth_token, callback) {
        fetch("flickr.activity.userPhotos", {
          "timeframe" : timeframe,
          "per_page" : per_page,
          "page" : page,
          "auth_token" : auth_token
        }, callback, ONE_HOUR, true);
      }
    },
    
    auth : {
      getFrob : function(callback) {
        // Frob request has the cache disabled
        fetch("flickr.auth.getFrob", {}, callback, 0, true);
      },

      getLoginURL : function(frob) {
        var params = {api_key: api_key, perms: "read", frob: frob};
        var url = "http://flickr.com/services/auth/?";
        url += "api_key=" + api_key + "&perms=read&frob=" + frob;
        url += "&api_sig=" + get_sig(params);
        return url;
      },

      getToken : function(frob, callback) {
        fetch("flickr.auth.getToken", {frob: frob}, callback, 0, true);
      }
    },

    people : {
      findByUsername : function(username, callback) {
        fetch("flickr.people.findByUsername",
        { "username" : username }, callback, SEVEN_DAYS);
      }
    },

    photos : {
      getContactsPublicPhotos : function(user_id, count, just_friends, callback) {
        fetch("flickr.photos.getContactsPublicPhotos", { 
          "user_id" : user_id, 
          "count" : Math.min(count, Flickr.MAX_PHOTOS),
          "just_friends" : just_friends
        }, callback, ONE_HOUR, false);
      },
    
      getContactsPhotos : function(count, just_friends, auth_token,  callback) {
        fetch("flickr.photos.getContactsPhotos", { 
          "count" : Math.min(count, Flickr.MAX_PHOTOS),
          "just_friends" : just_friends,
          "auth_token" : auth_token
        }, callback, ONE_HOUR, true);
      },

      search : function(text, user_id, per_page, page, sort, callback) {
        var params = {
          "user_id" : user_id,
          "per_page" : Math.min(per_page, Flickr.MAX_PHOTOS),
          "page" : page,
          "sort" : sort,
          "extras" : "owner_name,views"
        };
        if (text) {
          params.text = text;
        }
        fetch("flickr.photos.search", params, callback, ONE_HOUR, false);
      }
    },

    interestingness : {
      getList : function(per_page, page, callback) {
        fetch("flickr.interestingness.getList", { 
          "per_page" : Math.min(per_page, Flickr.MAX_PHOTOS),
          "page" : page,
          "extras" : "owner_name"
        }, callback, FIFTEEN_MINUTES);
      }
    },
    
    photo_url : function(o, size_ext) {
      return "http://farm" + o.farm + ".static.flickr.com" + "/" + o.server +
                "/" + o.id + "_" + o.secret + size_ext + ".jpg";
    }
  };
})();

// Handles rendering of the 'Contacts' tab
var ContactsTab = (function(){
  var photogrid = null;

  return {
    onclick : function(div_id) {
      var NSID = Gadget.prefs().getString('NSID');
      var auth_token = Gadget.prefs().getString("auth_token");
      if (!auth_token && !NSID) {
        Gadget.initAuth(true);
        return;
      }
      photogrid = photogrid ? photogrid : new PhotoGrid("ContactsPhotoGrid",
                                                        Gadget.prefs().getInt("num_photo_rows"));
      if (photogrid.has_photos()) {
        _IG_AdjustIFrameHeight();
        return;
      }
      if (auth_token) {
        Flickr.photos.getContactsPhotos(50, 0, auth_token,
                                        function(data) {
          if (!data) return Gadget.flushCacheAndReloadTab();
          var obj = eval("(" + data + ")");
          if (!obj || obj.stat == "fail") {
            if (obj.code == "98") {  // invalid auth token
              Gadget.initAuth(false);
              return;
            }
            return Gadget.error("Unable to retrieve photos.<br>" + obj.message);
          }
         
          photos = obj.photos.photo;
          if (photos.length == 0) {
            return Gadget.error("Can't find any photos posted by your contacts.");
          }
  
          photogrid.set_photos(photos, false);
        });
      } else {  // non-auth NSID-driven lookup
        // This is kept around from v1 for people upgrading. Without this block they would
        // no longer see their Contacts photos without having to do the auth process.
        // Trying to minimize the jolt for upgrading users.
        Flickr.photos.getContactsPublicPhotos(NSID, 50, 0, function(data) {
          if (!data) return Gadget.flushCacheAndReloadTab();
          var obj = eval("(" + data + ")");
          if (!obj || obj.stat == "fail") {
            if (obj.code == "1") {  // user not found. Fallback to asking for auth.
              Gadget.initAuth(true);
              return;
            }
            return Gadget.error("Unable to retrieve photos.<br>" + obj.message);
          }
         
          photos = obj.photos.photo;
          if (photos.length == 0) {
            return Gadget.error("Can't find any photos posted by your contacts.");
          }
  
          photogrid.set_photos(photos, false);
        });
      }
    }
  };
})();

var SearchTab = (function() {
  var photogrid = null;
  var search_field = null;
  var search = null;
  var inited = false;
  var statusDiv = _gel("SearchTabStatus");

  function setStatus(msg) {
    statusDiv.style.display = "block";
    statusDiv.innerHTML = "<br><center><i>" + msg + "</i></center><br>";
    _IG_AdjustIFrameHeight();
  }
  
  function hideStatus() {
    statusDiv.style.display = "none";
  }

  function render() {
    hideStatus();
    if (search_field.value == "") return;
    Flickr.photos.search(search_field.value, "", 150, 1,
                         "interestingness-desc",
                         function(data) {
      if (!data) return Gadget.flushCacheAndReloadTab();
      var obj = eval("(" + data + ")");
      if (!obj || obj.stat == "fail") {
        return setStatus("Unable to retrieve photos.<br>" + obj.message);
      }

      photos = obj.photos.photo;
      if (photos.length == 0) {
        return setStatus("No photos found for: <b>" + _hesc(search_field.value) + "</b>");    
      }

      photogrid.set_photos(photos, true);
    });  
  }

  return {
    onsearch : function() {
      if (search_field.value != Gadget.prefs().getString("search")) {
        Gadget.prefs().set("search", search_field.value);  // Save the pref
        photogrid.set_photos(null);
        render();
      }
    },

    onclick : function() {
      if (!inited) {
        search_field = _gel("SearchTab").getElementsByTagName("INPUT")[0];
        search_field.value = Gadget.prefs().getString("search");
        photogrid = photogrid ? photogrid : new PhotoGrid("SearchPhotoGrid",
                                                          Gadget.prefs().getInt("num_photo_rows"));
        inited = true;
      }
      if (!photogrid.has_photos()) {
        render();
      }
    }
  };
})();

var InterestingTab = (function() {
  var photogrid = null;

  return {
    onclick : function(div_id) {
      photogrid = photogrid ? photogrid : new PhotoGrid("InterestingPhotoGrid",
                                                        Gadget.prefs().getInt("num_photo_rows"));
      if (photogrid.has_photos()) {
        _IG_AdjustIFrameHeight();
        return;
      }
      Flickr.interestingness.getList(150, 1,
                                     function(data) {                     
        if (!data) return Gadget.flushCacheAndReloadTab();
        var obj = eval("(" + data + ")");
        if (!obj || obj.stat == "fail") {
          return Gadget.error("Unable to retrieve photos.<br>" + obj.message);
        }
        photogrid.set_photos(obj.photos.photo, true);
      });
    }
  };
})();

var ActivityTab = (function() {
  var MAX_EVENTS = 5;
  var frob = 0;
  var my_photos_grid = null;
  var IMG_comment_icon_src = _IG_GetCachedUrl("http://l.yimg.com/g/images/icon_comment.gif");
  var IMG_fave_icon_src = _IG_GetCachedUrl("http://l.yimg.com/g/images/icon_fave.gif");
  var IMG_tag_icon_src = _IG_GetCachedUrl("http://l.yimg.com/g/images/icon_tag.gif");
  var IMG_note_icon_src = _IG_GetCachedUrl("http://l.yimg.com/g/images/icon_note.gif");

  
  function relativeTimeString(then) {
    var value = 0;
    var unit = "";
    var delta = ((new Date()).getTime() / 1000) - then;
    if (delta < 60) {
      value = delta;
      unit = "second";
    } else if (delta < 3600) {
      value = Math.floor(delta / 60);
      unit = "minute";
    } else if (delta < 86400) {
      value = Math.floor(delta / 3600);
      unit = "hour";
    } else if (delta < 2592000) {
      value = Math.floor(delta / 86400);
      unit = "day";
    } else {
      value = Math.floor(delta / 2592000);
      unit = "month";
    }
    if (value > 1) unit += "s";
    return value + " " + unit + " ago";
  }
  

  return {
    onclick : function(div_id) {
      var auth_token = Gadget.prefs().getString("auth_token");
      if (!auth_token) {
        Gadget.initAuth(true);
        return;
      }
      _gel("activity").style.display = "block";
      if (_gel("activity").innerHTML != "") return;
      var NSID = Gadget.prefs().getString('NSID');
      Flickr.activity.userPhotos("7d", "5", "1", auth_token, function(data) {
        if (!data) return Gadget.flushCacheAndReloadTab();
        var obj = eval("(" + data + ")");
        if (!obj || obj.stat == "fail") {
          if (obj.code == "98") {
            Gadget.initAuth(false);
            return;
          }
          return Gadget.error("Unable to retrieve photos.<br>" + obj.message);
        }
        
        var items = obj.items.item;
        if (items.length == 0) {
          return Gadget.error('No comments, faves, notes or tags found for your Flickr account in the last 7 days.<br /><br />' +
                              'Visit Flickr for <a target="_top" href="http://flickr.com/activity/">all activity</a>.');
        }
        
        
        var s = [];
        for (var i = 0; i < items.length; i++) {
          // TODO: USE A FRIKKIN' TEMPLATE FRAMEWORK!
          var item = items[i];
          s.push('<div class="activity_item">');
            s.push('<img class="activity_item_thumb" src="' + Flickr.photo_url(item, "_s") + '" />');
            s.push('<div class="activity_item_head">');
              s.push('<div class="activity_item_title">');
                s.push('<a target="_top" href="http://flickr.com/photos/' + _esc(NSID) + '/' + _esc(item.id) + '/">');
                  s.push(item.title._content || 'Untitled');
                s.push('</a>');
              s.push('</div>');
              s.push('<div class="activity_item_stats">');
                var stats_url = "http://flickr.com/photos/" + _esc(NSID) + "/" + _esc(item.id) + "/stats/";
                var num_views = parseInt(item.views);
                var plural = (num_views > 1 ? "s" : ""); 
                s.push(num_views + ' <a target="_top" href="' + stats_url + '">view' + plural + '</a>');
                var num_comments = parseInt(item.commentsold) + parseInt(item.commentsnew);
                if (num_comments) {
                  var url = 'http://flickr.com/photos/' + _esc(NSID) + '/' + _esc(item.id) + '/';
                  var plural = (num_comments > 1 ? "s" : ""); 
                  s.push(' &nbsp;|&nbsp; ' + num_comments + ' <a target="_top" href="' + url + '">comment' + plural + '</a>');
                }
                var num_faves = parseInt(item.faves);
                if (num_faves) {
                  var url = "http://flickr.com/photos/" + _esc(NSID) + "/" + _esc(item.id) + "/favorites/";
                  var plural = (num_faves > 1 ? "s" : ""); 
                  s.push(' &nbsp;|&nbsp; ' + num_faves + ' <a target="_top" href="' + url + '">fave' + plural + '</a>');
                }
              s.push('</div>');
            s.push('</div>');
            for (var j = 0; j < Math.min(MAX_EVENTS, item.activity.event.length); j++) {
              var event = item.activity.event[j];
              var clazz = "activity_event";
              if (event.user == NSID) clazz += " mycomment";
              s.push('<div class="' + clazz + '">');
              var icon = null;
              var body = null;
              switch (event.type) {
                case "comment":
                  icon = IMG_comment_icon_src;
                  if (event.user == NSID) { // It's you
                    body = '<b>You said:</b><br />' + event._content;
                  } else {
                    body = '<a target="_top" href="http://flickr.com/photos/' + event.user + '">';
                    body += _hesc(event.username) + '</a> said:<br />' + event._content;
                  }
                  break;
                case "fave":
                  icon = IMG_fave_icon_src;
                  body = '<a target="_top" href="http://flickr.com/photos/' + event.user + '">';
                  body += _hesc(event.username) + '</a> added this as a ';
                  body += '<a target="_top" title="View ' + _hesc(event.username) + '\'s favorites" href="http://flickr.com/photos/' + event.user + '/favorites">favorite</a>.';
                  break;
                case "tag":
                  icon = IMG_tag_icon_src;
                  body = '<a target="_top" href="http://flickr.com/photos/' + event.user + '">';
                  body += _hesc(event.username) + '</a> added a tag: <a target="_top" href="http://flickr.com/photos/tags/' + _esc(event._content) + '/">' + _hesc(event._content) + '</a>';
                  break;
                case "note":
                  icon = IMG_note_icon_src;
                  body = '<a target="_top" href="http://flickr.com/photos/' + event.user + '">';
                  body += _hesc(event.username) + '</a> added a note:<br />' + event._content;
                  break;
                default: // ignore
                  continue;
              }
              s.push('<img class="activity_event_thumb" src="' + icon + '" />');
              s.push('<div class="activity_event_timestamp">' + relativeTimeString(event.dateadded) + '</div>');
              s.push('<div class="activity_event_body">' + body + '</div>');
              s.push('</div>');
            }
            if (item.activity.event.length > MAX_EVENTS) {
              // A *more* link shown when more than MAX_EVENTS returned for an item
              s.push('<div class="activity_event_more">&raquo; <a target="_top" href="http://flickr.com/photos/' + _esc(NSID) + '/' + _esc(item.id) + '#reply">more</a></div>');
            }
          s.push('</div>');
        }
        
        _gel("activity").innerHTML = s.join('');
        _IG_AdjustIFrameHeight();
      });
    }

  };
})();


function PhotoGrid(div_id, rows) {
  var id_ = div_id;
  var div_ = _gel(div_id);
  div_.className = "PhotoGrid";
  var photos_ = null;
  var rows_ = rows;
  var placeholder_html_ = null;
  div_.style.height = (77 * rows_) + "px";
  div_.photogrid = this;
  var interval_id = null;
  var last_num_photos_visible_ = 0;
  
  _IG_AddDOMEventHandler(window, "unload", function() {
    // Remove circular reference on unload
    div_.photogrid = null;
    div_.innerHTML = "";
  });
  
  function num_photos_visible() {
    return parseInt(_gel("status_div").offsetWidth/77.0) * rows_;
  }
  this.num_photos_visible = num_photos_visible;
  function reset_placeholders() {
    var s = [];
    var npv = num_photos_visible();
    for (var i = 0; i < npv; i++) {
      s.push("<a target='_top'><img class=photoph></a>");
    }
    // Now the hidden HTML that will contain a 'zoomed' image
    s.push("<div id=photo_zoom_dimmer" + id_ + " class='photo_zoom_dimmer'>&nbsp;</div>");
    s.push("<table cellspacing=0 cellpadding=0 border=0");
    s.push(" onclick='this.parentNode.photogrid.hide_zoom()'");
    s.push(" id=photo_zoom_container" + id_ + " class='photo_zoom_container' style='display:none'>");
    s.push("<tr><td align=center valign=middle>");
    s.push("</td></tr></table>");
    div_.innerHTML = s.join("");
  }
  reset_placeholders();

  function render_photos() {
    if (photos_) {
      // If number visible has changed, re-render the placeholders
      var npv = num_photos_visible();
      if (last_num_photos_visible_ != npv) {
        last_num_photos_visible_ = npv;
	reset_placeholders();
      }
      // Figure out how many to render...
      var num_photos = Math.min(npv, photos_.length);
      var phs = div_.getElementsByTagName("A");
      for (var i = 0; i < num_photos; i++) {
        var a = phs[i];
        var img = phs[i].firstChild;
        var src = "http://farm" + photos_[i].farm + ".static.flickr.com" +
                  "/" + photos_[i].server +
                  "/" + photos_[i].id +
                  "_" + photos_[i].secret + "_s.jpg";
        if (img.src != src) {
          img.src = src;
          img.className = "photo";
          a.href = "http://flickr.com/photos/" + _esc(photos_[i].owner) + "/" + photos_[i].id + "/";
          a.setAttribute("onclick", "this.parentNode.photogrid.show_zoom(" + i + ");return false;");
          a.onclick = photo_click_func(i);  // For MSIE
          a.title = (photos_[i].username || photos_[i].ownername || "Unknown") + " :: " + (photos_[i].title || "Untitled");
        } 
      }
    }
    _IG_AdjustIFrameHeight();
  }

  function photo_click_func(index) {
    return function() {
      this.parentNode.photogrid.show_zoom(index);
      return false;
    }
  }

  this.hide_zoom = function() {
    _gel("photo_zoom_container" + id_).style.display = "none";
    _gel("photo_zoom_dimmer" + id_).style.display = "none";
  }

  this.show_zoom = function(i) {
    var SCALE = 0.8;
    var photo = photos_[i];
    if (!photo) return;
    _gel("photo_zoom_container" + id_).style.display = "";
    _gel("photo_zoom_dimmer" + id_).style.display = "block";
    
    set_zoomed_image(PhotoGrid.loading_img_src, 16, 16, null, null);
    var img = new Image();
    img.onload = function() {
      var w = null;
      var h = null;
      var divw = div_.offsetWidth;
      var divh = div_.offsetHeight;
      if (((divw*SCALE)/img.width)*img.height < divh-35) {
        w = parseInt(divw * SCALE);
      } else {
        h = parseInt(divh * SCALE);
      }
      var FLICKR_ROOT_URL = "http://flickr.com/photos/" + _esc(photo.owner) + "/";
      var caption = [
        "<a href='" + FLICKR_ROOT_URL + photo.id + "/' target=_top>",
        _hesc(photo.title || "Untitled"),
        "</a> <nobr>&nbsp;&bull;&nbsp; ",
        "<a href='" + FLICKR_ROOT_URL + "' target=_top>",
        _hesc(photo.username || photo.ownername || "Unknown"),
        "</a></nobr>"
      ];
      set_zoomed_image(img.src, w, h, caption.join(""), "border:5px solid white;margin-bottom:2px");
      img.onload = null;
    }
    img.src = "http://farm" + photo.farm + ".static.flickr.com" +
              "/" + photo.server +
              "/" + photo.id +
              "_" + photo.secret + ".jpg";  
  }
  
  function set_zoomed_image(src, width, height, caption, style) {
    var html = [ "<img id=photo_zoom_image" + id_ + " src='" + src + "'" ];
    if (width) html.push(" width=" + width);
    if (height) html.push(" height=" + height);
    if (style) html.push(" style='" + style + "'");
    html.push(">");
    if (caption) html.push("<br>" + caption);
    _gel("photo_zoom_container" + id_).getElementsByTagName("TD")[0].innerHTML = html.join("");
  }
  
  _IG_AddDOMEventHandler(window, "resize", function() {
    render_photos();
  });
    
  this.set_photos = function(photos, shuffle) {
    photos_ = photos;
    if (shuffle) {
      for (var i = photos_.length-1; i >= 0; i--) {
        var rnd = Math.floor(Math.random() * (photos_.length-1));
        var tmp = photos_[i];
        photos_[i] = photos_[rnd];
        photos_[rnd] = tmp;
      }
    }
    reset_placeholders();
    render_photos();
    // If/when there are photos visible, we swap them around every few seconds
  }
  
  this.has_photos = function() {
    return !!photos_;
  }
  
  interval_id = setInterval(function() {
    if (!photos_ || photos_.length == 0) return;
    var num_photos = Math.min(num_photos_visible(), photos_.length);
    if (num_photos == photos_.length) return;
    var ix = Math.floor(Math.random() * (num_photos));
    var iy = Math.floor(Math.random() * (photos_.length-num_photos-1)) + num_photos;
    var tmp = photos_[ix];
    photos_[ix] = photos_[iy];
    photos_[iy] = tmp;
    render_photos();
  }, 3000);  
}

PhotoGrid.loading_img_src = _IG_GetCachedUrl("http://warmbrain.com/gadgets/loading.gif");



// Methods general and accessible to all tabs
// Also handles the rendering of the photo grid
var Gadget = (function() {
  var prefs = new _IG_Prefs(__MODULE_ID__);
  var frob = null;  // Used during auth
  var tabData = [
    ["Activity", "My Recent Activity", ActivityTab.onclick, "ActivityTab"],
    ["Contacts", "Most recent photos posted by " + prefs.getString("username") + "'s contacts", ContactsTab.onclick, "ContactsTab"],
    ["Interesting", "Latest \'Interesting\' photos as defined by Flickr", InterestingTab.onclick, "InterestingTab"],
    ["Search", "Find photos by keyword", SearchTab.onclick, "SearchTab"]
  ];
  var tabs = new _IG_Tabs(__MODULE_ID__, tabData[prefs.getInt("lastSelectedTab")][0]);

  function pinkify_tabs() {
    var list = tabs.getTabs();
    for (var i = 0; i < list.length; i++) {
      var name = list[i].getName();
      if (name.indexOf("pink") == -1) {
        var char = name.substr(name.length-1, 1);
        list[i].getNameContainer().innerHTML =
          name.substr(0, name.length-1) + "<span class=pink>" + char + "</span>";  
      }
    }
  }

  function getTabClickCallback(func) {
    return function() {
      Gadget.hideError();
      Gadget.hideAuth();
      func();
      prefs.set("lastSelectedTab", tabs.getSelectedTab().getIndex());
    };
  }

  
  function finishAuthOnFocus() {
    Gadget.finishAuth(true);
  }
  
  return {
    init : function() {
      for (var i = 0; i < tabData.length; i++) {
        tabs.addTab(tabData[i][0], {
          tooltip: tabData[i][1],
          callback: getTabClickCallback(tabData[i][2]),
          contentContainer : _gel(tabData[i][3])
        });
      }
      pinkify_tabs();

      // Versioning should come in useful in the future during migrations
      // Wish it was in place for the V1 -> V2 migration :(
      var version = Gadget.prefs().getInt("version");
      switch (version) {
        case 0:  // The default
          Gadget.prefs().set("version", 2);
          break;
      }
    },

    hideError: function() {
      // Not actually hidden, just empty. 
      _gel("status_div").innerHTML = "";
    },
    
    error : function(msg) {
      _gel("status_div").innerHTML = "<br><center><i>" + msg + "</i></center><br>";
      tabs.getSelectedTab().getContentContainer().style.display = "none";
      _gel("auth1").style.display = "none";
      _gel("auth2").style.display = "none";
      _IG_AdjustIFrameHeight();
    },

    prefs : function() {
      return prefs;
    },
    
    flushCacheAndReloadTab : function() {
      Flickr.nocache = (Math.random()+"").substring(2,6) + "";
      var cb = tabs.getSelectedTab().getCallback();
      cb();
    },
    
    hideAuth : function() {
      _gel("auth1").style.display = "none";
      _gel("auth2").style.display = "none";
    },

    initAuth : function(firstauth) {
      // Hide the current tab contents
      tabs.getSelectedTab().getContentContainer().style.display = "none";
      _gel('auth1').style.display = "block";
      if (firstauth) {
        _gel('firstauth').style.display = "block";
        _gel('reauth').style.display = "none";
      } else {
        _gel('firstauth').style.display = "none";
        _gel('reauth').style.display = "block";
      }
      _IG_AdjustIFrameHeight();
      Flickr.auth.getFrob(function(data) {
        var obj = eval("(" + data + ")");
        if (!obj || obj.stat == "fail") {
          return Gadget.error("Unable to start authorization.<br>" + obj.message);
        }
        frob = obj.frob._content;
        _gel("auth_link").href = Flickr.auth.getLoginURL(frob);
      });
    },

    startAuth : function(el) {
      // If the HREF isn't set yet, ignore the click.
      if (!el.href) return false;
      _gel('auth1').style.display = "none";
      _gel('auth2').style.display = "block";
      _IG_AddDOMEventHandler(window, "focus", finishAuthOnFocus);
    },
    
    finishAuth : function(auto) {
      Flickr.auth.getToken(frob, function(data) {
        var obj = eval("(" + data + ")");
        if (!obj || obj.stat == "fail") {
          // We tried to auto-complete auth. Ignore and let the user click.
          if (auto) return;
          if (obj.code == "108") {
            // invalid frob. Better messaging?
          }
          return Gadget.error("Unable to complete authorization.<br>" + obj.message + "<br>Please try again.");
        }
        // Success! Save the tokens!
        Gadget.prefs().set("auth_token", obj.auth.token._content);
        Gadget.prefs().set("NSID", obj.auth.user.nsid);
        Gadget.prefs().set("username", obj.auth.user.username);
        _IG_RemoveDOMEventHandler(window, "focus", finishAuthOnFocus);
        // TODO: Enable the new contacts tab?
        // Now re-draw the tab.
        tabs.getSelectedTab().getContentContainer().style.display = "block";
        _gel('auth2').style.display = "none";
        var cb = tabs.getSelectedTab().getCallback();
        cb();
      });    
    }
  };
})();


// The large but necessary MD5 code
// With thanks to Harald Hanek
// http://code.delacap.com/p/js-methods/docs/md5.html
// TODO: defer execution? Move to AppEngine?
(function(){var md5={hexcase:0,b64pad:"",chrsz:8,'hex_md5':function(s){return this.binl2hex(this.core_md5(this.str2binl(s),s.length*this.chrsz))},'b64_md5':function(s){return this.binl2b64(this.core_md5(this.str2binl(s),s.length*this.chrsz))},'str_md5':function(s){return this.binl2str(this.core_md5(this.str2binl(s),s.length*this.chrsz))},'hex_hmac_md5':function(key,data){return this.binl2hex(this.core_hmac_md5(key,data))},'b64_hmac_md5':function(key,data){return this.binl2b64(this.core_hmac_md5(key,data))},'str_hmac_md5':function(key,data){return this.binl2str(this.core_hmac_md5(key,data))},'core_md5':function(x,len){x[len>>5]|=0x80<<((len)%32);x[(((len+64)>>>9)<<4)+14]=len;var a=1732584193;var b=-271733879;var c=-1732584194;var d=271733878;for(var i=0;i<x.length;i+=16){var olda=a;var oldb=b;var oldc=c;var oldd=d;a=this.md5_ff(a,b,c,d,x[i+0],7,-680876936);d=this.md5_ff(d,a,b,c,x[i+1],12,-389564586);c=this.md5_ff(c,d,a,b,x[i+2],17,606105819);b=this.md5_ff(b,c,d,a,x[i+3],22,-1044525330);a=this.md5_ff(a,b,c,d,x[i+4],7,-176418897);d=this.md5_ff(d,a,b,c,x[i+5],12,1200080426);c=this.md5_ff(c,d,a,b,x[i+6],17,-1473231341);b=this.md5_ff(b,c,d,a,x[i+7],22,-45705983);a=this.md5_ff(a,b,c,d,x[i+8],7,1770035416);d=this.md5_ff(d,a,b,c,x[i+9],12,-1958414417);c=this.md5_ff(c,d,a,b,x[i+10],17,-42063);b=this.md5_ff(b,c,d,a,x[i+11],22,-1990404162);a=this.md5_ff(a,b,c,d,x[i+12],7,1804603682);d=this.md5_ff(d,a,b,c,x[i+13],12,-40341101);c=this.md5_ff(c,d,a,b,x[i+14],17,-1502002290);b=this.md5_ff(b,c,d,a,x[i+15],22,1236535329);a=this.md5_gg(a,b,c,d,x[i+1],5,-165796510);d=this.md5_gg(d,a,b,c,x[i+6],9,-1069501632);c=this.md5_gg(c,d,a,b,x[i+11],14,643717713);b=this.md5_gg(b,c,d,a,x[i+0],20,-373897302);a=this.md5_gg(a,b,c,d,x[i+5],5,-701558691);d=this.md5_gg(d,a,b,c,x[i+10],9,38016083);c=this.md5_gg(c,d,a,b,x[i+15],14,-660478335);b=this.md5_gg(b,c,d,a,x[i+4],20,-405537848);a=this.md5_gg(a,b,c,d,x[i+9],5,568446438);d=this.md5_gg(d,a,b,c,x[i+14],9,-1019803690);c=this.md5_gg(c,d,a,b,x[i+3],14,-187363961);b=this.md5_gg(b,c,d,a,x[i+8],20,1163531501);a=this.md5_gg(a,b,c,d,x[i+13],5,-1444681467);d=this.md5_gg(d,a,b,c,x[i+2],9,-51403784);c=this.md5_gg(c,d,a,b,x[i+7],14,1735328473);b=this.md5_gg(b,c,d,a,x[i+12],20,-1926607734);a=this.md5_hh(a,b,c,d,x[i+5],4,-378558);d=this.md5_hh(d,a,b,c,x[i+8],11,-2022574463);c=this.md5_hh(c,d,a,b,x[i+11],16,1839030562);b=this.md5_hh(b,c,d,a,x[i+14],23,-35309556);a=this.md5_hh(a,b,c,d,x[i+1],4,-1530992060);d=this.md5_hh(d,a,b,c,x[i+4],11,1272893353);c=this.md5_hh(c,d,a,b,x[i+7],16,-155497632);b=this.md5_hh(b,c,d,a,x[i+10],23,-1094730640);a=this.md5_hh(a,b,c,d,x[i+13],4,681279174);d=this.md5_hh(d,a,b,c,x[i+0],11,-358537222);c=this.md5_hh(c,d,a,b,x[i+3],16,-722521979);b=this.md5_hh(b,c,d,a,x[i+6],23,76029189);a=this.md5_hh(a,b,c,d,x[i+9],4,-640364487);d=this.md5_hh(d,a,b,c,x[i+12],11,-421815835);c=this.md5_hh(c,d,a,b,x[i+15],16,530742520);b=this.md5_hh(b,c,d,a,x[i+2],23,-995338651);a=this.md5_ii(a,b,c,d,x[i+0],6,-198630844);d=this.md5_ii(d,a,b,c,x[i+7],10,1126891415);c=this.md5_ii(c,d,a,b,x[i+14],15,-1416354905);b=this.md5_ii(b,c,d,a,x[i+5],21,-57434055);a=this.md5_ii(a,b,c,d,x[i+12],6,1700485571);d=this.md5_ii(d,a,b,c,x[i+3],10,-1894986606);c=this.md5_ii(c,d,a,b,x[i+10],15,-1051523);b=this.md5_ii(b,c,d,a,x[i+1],21,-2054922799);a=this.md5_ii(a,b,c,d,x[i+8],6,1873313359);d=this.md5_ii(d,a,b,c,x[i+15],10,-30611744);c=this.md5_ii(c,d,a,b,x[i+6],15,-1560198380);b=this.md5_ii(b,c,d,a,x[i+13],21,1309151649);a=this.md5_ii(a,b,c,d,x[i+4],6,-145523070);d=this.md5_ii(d,a,b,c,x[i+11],10,-1120210379);c=this.md5_ii(c,d,a,b,x[i+2],15,718787259);b=this.md5_ii(b,c,d,a,x[i+9],21,-343485551);a=this.safe_add(a,olda);b=this.safe_add(b,oldb);c=this.safe_add(c,oldc);d=this.safe_add(d,oldd)}return Array(a,b,c,d)},'md5_cmn':function(q,a,b,x,s,t){return this.safe_add(this.bit_rol(this.safe_add(this.safe_add(a,q),this.safe_add(x,t)),s),b)},'md5_ff':function(a,b,c,d,x,s,t){return this.md5_cmn((b&c)|((~b)&d),a,b,x,s,t)},'md5_gg':function(a,b,c,d,x,s,t){return this.md5_cmn((b&d)|(c&(~d)),a,b,x,s,t)},'md5_hh':function(a,b,c,d,x,s,t){return this.md5_cmn(b^c^d,a,b,x,s,t)},'md5_ii':function(a,b,c,d,x,s,t){return this.md5_cmn(c^(b|(~d)),a,b,x,s,t)},'core_hmac_md5':function(key,data){var bkey=this.str2binl(key);if(bkey.length>16)bkey=this.core_md5(bkey,key.length*this.chrsz);var ipad=Array(16),opad=Array(16);for(var i=0;i<16;i++){ipad[i]=bkey[i]^0x36363636;opad[i]=bkey[i]^0x5C5C5C5C}var hash=this.core_md5(ipad.concat(this.str2binl(data)),512+data.length*this.chrsz);return this.core_md5(opad.concat(hash),512+128)},'safe_add':function(x,y){var lsw=(x&0xFFFF)+(y&0xFFFF);var msw=(x>>16)+(y>>16)+(lsw>>16);return(msw<<16)|(lsw&0xFFFF)},'bit_rol':function(num,cnt){return(num<<cnt)|(num>>>(32-cnt))},'str2binl':function(str){var bin=Array();var mask=(1<<this.chrsz)-1;for(var i=0;i<str.length*this.chrsz;i+=this.chrsz)bin[i>>5]|=(str.charCodeAt(i/this.chrsz)&mask)<<(i%32);return bin},'binl2str':function(bin){var str="";var mask=(1<<this.chrsz)-1;for(var i=0;i<bin.length*32;i+=this.chrsz)str+=String.fromCharCode((bin[i>>5]>>>(i%32))&mask);return str},'binl2hex':function(binarray){var hex_tab=this.hexcase?"0123456789ABCDEF":"0123456789abcdef";var str="";for(var i=0;i<binarray.length*4;i++){str+=hex_tab.charAt((binarray[i>>2]>>((i%4)*8+4))&0xF)+hex_tab.charAt((binarray[i>>2]>>((i%4)*8))&0xF)}return str},'binl2b64':function(binarray){var tab="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var str="";for(var i=0;i<binarray.length*4;i+=3){var triplet=(((binarray[i>>2]>>8*(i%4))&0xFF)<<16)|(((binarray[i+1>>2]>>8*((i+1)%4))&0xFF)<<8)|((binarray[i+2>>2]>>8*((i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>binarray.length*32)str+=this.b64pad;else str+=tab.charAt((triplet>>6*(3-j))&0x3F)}}return str}};if(!String.prototype.md5)String.prototype.md5=function(){return md5.hex_md5(this)}})();

Gadget.init();

</script>
     ]]>

  </Content> 
</Module>

