decembre

GM - Flickr Functional Suite - NEW-2025.03 - (by C. Tilford) - Tweak reuse GM Flickr River

Jun 2nd, 2014 (edited)
938
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name          Flickr Functional Suite - NEW(2025.03) - Tweak for Photo page
  3. // @namespace     http://www.tilford.net/
  4. // @description   Adds some additional functionality to page - NEW(2025.03) - Tweak for Photo page By reuse FlickrRiver GM
  5. // @icon          https://external-content.duckduckgo.com/ip3/blog.flickr.net.ico
  6. // @include       http://*.flickr.com/*
  7. // @include       http://flickr.com/*
  8. // @include       https://*.flickr.com/*
  9. // @include       https://flickr.com/*
  10. // @grant         GM_getValue
  11. // @grant         GM_setValue
  12. // @grant         GM_log
  13. // @grant         GM_xmlhttpRequest
  14.  
  15. //
  16. // ==/UserScript==
  17.  
  18. /*
  19.  
  20. This is a Grease Monkey script. It *requires* FireFox and the Grease
  21. Monkey extension to work:
  22.  
  23. http://www.mozilla.com/firefox/
  24. http://greasemonkey.mozdev.org/
  25.  
  26. Following the instructions on the above web pages
  27. 1. Install FireFox
  28. 2. Install GreaseMonkey
  29. 3. Install this script
  30.  
  31. Author: Charles Tilford  http://www.flickr.com/photos/charlestilford/
  32.   (I am not hosting tilford.net yet)
  33.  
  34. This code is released under the CreativeCommons 'By' license. That is,
  35. you can do anything you want with it, just leave my name as one of the
  36. authors. http://creativecommons.org/licenses/by/2.5/
  37.  
  38. h2. Summary
  39.  
  40. Provides an interface to let you assign colors to users you meet - that way, if you find a user's link on a page in the future, it will automatically be highlighted with thes color you previously gave them.
  41.  
  42. In group pool pages, you can highlight user photos where more than one image is visible from the same user. You can also overlay all comments from a user under the corresponding photos on that page.
  43.  
  44. h2. Features:
  45.  
  46. # Configuration
  47. ** All features can be toggled on / off
  48. ** Access to configuration either through [?] links or main "Organize" menu
  49. ** Colors can be customized.
  50. # User coloration
  51. ** All '/photos/USERNAME/' links will be altered:
  52. *** Clicking on them brings up a mini-menu
  53. *** You can assign a color to any user, which will highlight the user links
  54. *** Colors will be remembered (only on your computer)
  55. *** It is now possible to assign multiple colors to a user
  56. *** Links to their photos, profile and pool images (if in pool page) shown in menu
  57. # Group pool analysis
  58. ** When viewing pool pages, all images on the page are tallied
  59. *** Users posting more than one photo will be noted
  60. *** Images from those users will get a colored background
  61. *** Background colors are unique to each user (but otherwise essentially random)
  62. *** Summary of users posting multiple images shown at top of page
  63. ** Comments for all photos on a pool page are pre-fetched
  64. *** For any poster on a pool page, all their comments can be shown for other photos on the same page
  65. ** NOTE: these functions are off by default, turn them on in the options page ('Organize' Flickr menu)
  66.  
  67. !http://static.flickr.com/69/206732191_399d74d02a.jpg!:http://www.flickr.com/photos/charlestilford/206732191/
  68.  
  69. !http://static.flickr.com/73/219349265_b6b41a139b.jpg!:http://www.flickr.com/photos/charlestilford/219349265/
  70.  
  71. !http://static.flickr.com/90/219349268_a4dd3fa439.jpg!:http://www.flickr.com/photos/charlestilford/219349268/
  72.  
  73.  
  74. Handy Color name reference:
  75. http://www.w3.org/TR/2002/WD-css3-color-20020219/#x11-color
  76.  
  77. !! Caution !!
  78.  
  79. Persistent data, such as colors assigned to users, are stored within
  80. the Grease Monkey / FireFox system itself. If you invest a lot of time in
  81. color-coding users, you run the risk of losing all your assignments if
  82. FireFox's prefs.js is over-written. If you would like to back up your
  83. assignments, you should back up that file - on Windows it will be
  84. somewhere like:
  85.  
  86. C:\Documents and Settings\YourUserName\Application Data\
  87.    Mozilla\Firefox\Profiles\8aabdex06.default\prefs.js
  88.  
  89. and have entries like:
  90.  
  91. user_pref("greasemonkey.scriptvals.http://www.tilford.net//Flickr Functional Suite.UserColorcharlestilford", "Lime");
  92.  
  93. !! About User Identifications
  94.  
  95. Flickr uses a confusing mixture of identifiers for their customers.
  96.  
  97. Flickr     Used Here Example
  98. ---------- --------- -----------------
  99. NSID:      nsid      51159953@N00
  100. username:  uname     listentoreason
  101. photourl:  purl      charlestilford
  102. displayid: dispid    Truncated Username...
  103.  
  104. The NSID is the most useful for getting information, but the least
  105. human friendly. The displayid is not technically an official
  106. identifer, but is encountered when parsing Flickr web pages. It is
  107. used when a user name is too long - the username is truncated,
  108. apparently on a whitespace character, and shown with elipses ie "...".
  109.  
  110. # History
  111. * 25 Aug 2006
  112. ** John Carney suggests a Plays-Nice-With-Others alteration that lets pop-up functionality work fine with other GM scripts
  113. ** Added additional links in user menu, tried to tighten up real estate usage
  114. * 24 Aug 2006
  115. ** Bug fix by John Carney (http://schmickr.innercurmudgeon.com/)
  116. * 19 Aug 2006
  117. ** Implemented interface to Flickr API
  118. ** Use API to fetch comments for all photos on pool page
  119. *** For any pool contributor, show all of their comments on that page
  120. * 15 Aug 2006
  121. ** Allow multiple colors to be assigned to one user
  122. * 6 Aug 2006
  123. ** User settings added
  124. *** Can toggle basic behaviors on and off
  125. *** Can now customize color list, and assign description to each color
  126. ** Altered 'All Pool Photos' to only display when the low-level ID is used as the UID
  127. * 4 August 2006
  128. ** Nice interface to set user color
  129. ** User colors persistently stored with GM_setValue()
  130. ** Multiple posting working well
  131. ** The 'All Pool Photos' usually will not work - I need to implement a method to ajax get the low-level user ID
  132.  
  133.  !! Disclaimer !!
  134.  
  135. Feel free to mine the code for ideas, but be warned that because this
  136. is a Grease Monkey script, I have made no effort at all to make the code
  137. anywhere close to cross-browser compatible - parts of the code will be
  138. specific to FireFox and will not function in other browsers.
  139.  
  140. Boy it is *nice* to develop JS for *ONLY* FireFox!
  141.  
  142. */
  143.  
  144. /* (2025) TWEAK TO HAVE FFS in PHOTO PAGE - work but i don't understant why it work (add a F.F.S button near User Name */
  145. var userRegex = new RegExp(".*?flickr.com\/photos\/([^\/]*)");
  146. var matched = userRegex.exec(window.location);
  147. if(matched){
  148.     var riverurl = "http://www.flickriver.com/photos/"+ matched[1] +"/";
  149.     document.getElementsByClassName("attribution-info")[0].innerHTML += '  |  <span ><a href="'+ riverurl+'">   F. Functional S.   </a>  </span>   ';
  150. }
  151.  
  152. var CatFlickrSuite = {
  153.     // Internal data structures
  154.     privateData: new Object(), // Safe (?) storage of metadata for DOM elements
  155.     env:         new Object(), // Global environmental values
  156.     user:        new Object(), // user-defined values
  157.     doneTask:    new Object(), // Prevent re-fetching some data
  158.     gmMsg: "",                 // Debugging message buffer
  159.     apikey: "b1c88b6a99ffabbf76ace9eab025996f",
  160.     seckey: "79c1cc27f6c7cff9",
  161.     objects: {  }, // Pre-calculated/scanned
  162.     // Sigh. Flickr is changing their object IDs. Their perogative, of course
  163.     // Centralize the IDs here to make code updates easier
  164.     fids: {
  165.         main: 'main',
  166.         navYou: 'flickr_nav_menu_you',
  167.         //        navOrg: 'flickr_nav_menu_organize',
  168.         navOrg: 'explore-panel',
  169.         csfHelp: 'CfsHelpButton',
  170.         csfCols: 'currentColors',
  171.         csfAvl:  'availableColors',
  172.         csfPop:  'CatFlickrSuitePopUp',
  173.         csfTag:  'CFS_Tag_Assoc',
  174.         grpAvl:  'TagMatchAvail'
  175.     },
  176.     // User configurable parameters. Organized by class (the kind of
  177.     // <input> used to represent them) and parameter name, with a
  178.     // description and default value for each.
  179.     userSet: {
  180.         checkbox: {
  181.             colorUser: ["Colorize user IDs",1],
  182.             colorMult: ["Colorize multiple images from same user in group pools",0],
  183.             sumMult:   ["Summarize multi-posters in group pools",0],
  184.             getCom:    ["Pre-fetch all comments associated with photos on a pool page",0],
  185.         },
  186.         text: {
  187.             comWidth:  ["Maximum characters to show for in-line comments",100],
  188.             comSize:   ["Inline comment font size", '9px'],
  189.         },
  190.         textarea: {
  191.             UserColors: ["Colors and descriptions for tagging other users",
  192.                          "Lime Category 1\nAqua Category 2\nFuchsia Category 3\nSilver Category 4\nYellow Category 5\nRed Category 6\nBlack Category 7\nNone"],
  193.         },
  194.     },
  195.     translate: { },
  196.     // The 'minimum RGB spacing' - prevents grays when using string2color and color4string:
  197.     colBuf: 64,
  198.     /* colMin and colMax added for @decembre. Allows the RGB index
  199.        range in pools to be changed from 0-255 to user prefs.
  200.        colMin - increase to prevent 'dark' index values
  201.        colMax - decrease to prevent 'light' index values
  202.        ALWAYS have colMin < colMax, and both between 0 and 255 */
  203.     colMin: 0,
  204.     colMax: 255,
  205.     counter: 0,
  206.     ticket: 0,
  207.     ticketStatus: new Object(),
  208.     // Special links that we want to ignore:
  209.     specialLinks: ['1', '< Prev', 'You', 'Organize', 'Upload', 'upload',
  210.                    'Popular Tags','Organize & Create', "All your content",
  211.                    'Upload Photos and Videos', "Your Photostream",
  212.                    'Your Photos', 'Upload Photos', 'All your photos', ],
  213.     re: {
  214.         // Regular Expression Collection
  215.         frob:       /\?frob=(\S+)$/,             // Authentication frob
  216.         photoID:    /\/photos\/([^\/]+)\/(\d+)/, // PURL and Photo ID
  217.         userPhotos: /\/photos\/([^\/]+)\/$/,     // Link to user photots
  218.         myComs:     /\/photos_comments\.gne/,    // Comments you made
  219.         urlPool1:   /\/([^\/]+)\/pool\//,        // Group pool href
  220.         urlPool2:   /\/in\/pool-([^\/]+)\//,     // Image in pool stream
  221.         urlGroup:   /\/groups\/([^\/]+)\//,      // General group URL
  222.         urlAllGrp:  /\/groups\/?$/,               // Group list page
  223.         statRef:    /\/stats\/([^\/]+)\//,       // Statistics page
  224.         urlSet:     /\/photos\/([^\/]+)\/sets\/(\d+)\/?$/, // Photoset
  225.         elipsed:    /\.\.\.$/,                   // Trailing 3 periods (elipses)
  226.         nsid:       /^\d+\@.{3}$/, // Not sure what the real format is?
  227.         organize:   /\/photos\/organize\//,      // Organize DHTML interface
  228.     },
  229.     init: function() {
  230.         // The primary initialization function
  231.         // GM_log("Starting: " + new Date());
  232.         var now = new Date();
  233.         this.ms = now.getTime().toString();
  234.         this.setEnvironment();
  235.         if (this.env.pageType == 'Organize') return; // Do nothing on Organize
  236.         this.initTransmute();
  237.         this.setSettings();
  238.         this.insertSettings();
  239.         this.refresh();
  240.         this.annotateGroupList();
  241.         this.tagAssociation();
  242.         this.cleanComments();
  243.         this.colorStats();
  244.         this.finalize();
  245.         GM_log("Initialized Flickr Functional Suite");
  246.         window.addEventListener('click', function (e) {
  247.                 // alert('new data!');
  248.                 CatFlickrSuite.checkClickEvent(e);
  249.             }, false);
  250.  
  251.         //window.addEventListener('AutoPagerNewData', function (e) {
  252.         //        document.CatFlickrSuite.refresh();
  253.         //    }, false);
  254.     },
  255.     refresh: function() {
  256.         this.grabFrob();
  257.         this.armLinks();
  258.         this.findMultiPost();
  259.         this.colorUserPhotos();
  260.         this.getAllComments();
  261.     },
  262.     whoami: function() {
  263.         if (this.you == null) {
  264.             var data = this.parseFlickrData();
  265.             this.you = "";
  266.             if (data.flickr && data.flickr.user && data.flickr.user.pathalias) {
  267.                 this.you = data.flickr.user.pathalias;
  268.             } else {
  269.                 GM_log("Could not find path alias");
  270.             }
  271.         }
  272.         return this.you;
  273.     },
  274.     myNSID: function() {
  275.         if (this.nsid == null) {
  276.             var data = this.parseFlickrData();
  277.             this.nsid = 0;
  278.             if (data.flickr && data.flickr.user && data.flickr.user.nsid) {
  279.                 this.nsid = data.flickr.user.nsid;
  280.             } else {
  281.                 GM_log("Could not find NSID");
  282.             }
  283.         }
  284.         return this.nsid;
  285.     },
  286.     parseFlickrData: function () {
  287.         if (!this.yconf) {
  288.             var nodes = document.getElementsByTagName('script');
  289.             for (var n=0; n<nodes.length; n++) {
  290.                 var html = nodes[n].innerHTML;
  291.                 if (!html) continue;
  292.                 var hits = html.match(/\s*var\s+yconf\s+=\s*(\{.+?\})\;\s*/);
  293.                 if (hits) {
  294.                     this.yconf= JSON.parse( hits[1]);
  295.                     break;
  296.                 }
  297.             }
  298.             if (!this.yconf) {
  299.                 this.yconf = new Object();
  300.                 GM_log("Failed to find Flickr yconf hash object");
  301.             }
  302.         }
  303.         return this.yconf;
  304.     },
  305.     groupID: function() {
  306.         if (this.groupid == null) {
  307.             this.groupid = 0;
  308.             var nodes = document.getElementsByTagName('link');
  309.             for (var n=0; n<nodes.length; n++) {
  310.                 if (!nodes[n] || !nodes[n].href) continue;
  311.                 var hits = nodes[n].href.match(/groups_[a-z]+\.gne\?.*id=([^\&]+)/);
  312.                 if (hits) { this.groupid = hits[1]; break; }
  313.             }
  314.             if (!this.groupid) {
  315.                 // Try links
  316.                 nodes = document.getElementsByTagName('a');
  317.                 for (var n=0; n<nodes.length; n++) {
  318.                     if (!nodes[n] || !nodes[n].href) continue;
  319.                     var hits = nodes[n].href.match(/groups_[a-z]+\.gne\?.*id=([^\&]+)/);
  320.                     if (hits) { this.groupid = hits[1]; break; }
  321.                 }
  322.             }
  323.         }
  324.         return this.groupid;
  325.     },
  326.     setEnvironment: function() {
  327.         // Gather some global information
  328.         var href = this.env.href = document.location.href;
  329.         var mat;
  330.         this.env.pageType = this.env.pageView = '';
  331.         // Are we on the organize page?
  332.         mat  = href.match(this.re.organize);
  333.         if (mat) { this.env.pageType = 'Organize';
  334.         this.env.pageView = 'Organize'; return; }
  335.         // Are we on a 'Comments you've made' page?
  336.         mat  = href.match(this.re.myComs);
  337.         if (mat) { this.env.pageType = 'Comment';
  338.         this.env.pageView = 'YourComments'; return; }
  339.         // Are we on a Group's pool page?
  340.         // See if we are in a group pool-related page:
  341.         mat  = href.match(this.re.urlPool1);
  342.         if (mat) { this.env.group = mat[1];
  343.         this.env.pageType = 'Group'; this.env.pageView = 'Pool'; return; }
  344.  
  345.         // Group list page
  346.         mat  = href.match(this.re.urlAllGrp);
  347.         if (mat) { this.env.group = mat[1];
  348.             this.env.pageType = 'Group'; this.env.pageView = 'YourGroups'; return; }
  349.  
  350.         // Generic URL for groups:
  351.         mat  = href.match(this.re.urlGroup);
  352.         if (mat) { this.env.group = mat[1];
  353.         this.env.pageType = 'Group'; return; }
  354.         // A photoset page:
  355.         mat  = href.match(this.re.urlSet);
  356.         if (mat) { this.env.setOwn = mat[1]; this.env.set = mat[2];
  357.         this.env.pageType = this.env.pageView = 'Set'; return; }
  358.         // Statistics Page
  359.         mat  = href.match(this.re.statRef);
  360.         if (mat) {
  361.             this.env.statType = mat[1];
  362.             this.env.pageType = this.env.pageView = 'Stats'; return;
  363.         }
  364.  
  365.     },
  366.     initTransmute: function() {
  367.         // Turn one object type into another This just allows
  368.         // representation of some data structures in a format that is
  369.         // easy for humans to modify, but not the ultimate structure
  370.         // needed by the code.
  371.         this.specLinkHash = new Object();
  372.         for (var i=0; i<this.specialLinks.length;i++) {
  373.             this.specLinkHash[this.specialLinks[i].toLowerCase()] = 1;
  374.         }
  375.         // Flatten settings hash into 2D array
  376.         var uarr = new Array();
  377.         for (var type in this.userSet) {
  378.             // Cycle through each data type
  379.             var info = this.userSet[type];
  380.             for (var tag in info) {
  381.                 // Cycle through each parameter
  382.                 var subarray = [ type, tag ];
  383.                 // Add description, defaultValue:
  384.                 subarray = subarray.concat( info[tag] );
  385.                 uarr.push(subarray);
  386.             }
  387.         }
  388.         this.msg();
  389.         this.userSetArr = uarr;
  390.     },
  391.     setSettings: function() {
  392.         // Scan all configurable tagnames and retrieve value from
  393.         // GreaseMonkey internal store, or set to default value.
  394.         var uarr = this.userSetArr;
  395.         for (var u=0; u < uarr.length; u++) {
  396.             var tag        = uarr[u][1];
  397.             var def        = uarr[u][3];
  398.             this.user[tag] = GM_getValue(tag, def);
  399.         }
  400.         // Special processing of user colors
  401.         var colArr = this.user.UserColors.split(/[\r\n]+/);
  402.         this.user.colList = new Array();
  403.         this.user.colDesc = new Object();
  404.         for (var c=0;c<colArr.length;c++) {
  405.             var cbits = colArr[c].split(/\s+/);
  406.             var cname = cbits.shift();
  407.             if (!cname || cname == '') continue;
  408.             var cdesc = cbits.join(' ');
  409.             this.user.colList.push(cname);
  410.             this.user.colDesc[ cname.toLowerCase() ] = cdesc;
  411.         }
  412.     },
  413.     msg: function() {
  414.         // Record some debugging information. These messages will
  415.         // appear in the FireFox javascript Console, under 'Messages'
  416.         if (this.gmMsg != "") GM_log("Execution messages:\n"+this.gmMsg);
  417.         this.gmMsg = "";
  418.     },
  419.     err: function (msg, e) {
  420.         // Throw an error, also to the JS concole
  421.         if (e) {
  422.             if (e.description) msg += "\n  DESC: " + e.description;
  423.             msg += "\n  ERR: " + e;
  424.         }
  425.         GM_log(msg);
  426.     },
  427.     finalize: function() {
  428.         // Final code to execute after all parsing is done.
  429.         this.msg();
  430.     },
  431.     armLinks: function() {
  432.         // Search for <a> tags on the page that we want to modify
  433.         var links = this.liveUserPhotoList();
  434.         for (var i=0; i < links.length; i++) {
  435.             this.registerUserPhoto( links[i] );
  436.         }
  437.     },
  438.     colors4user: function(purl, asArray) {
  439.         // Get your color(s) for a particular PURL
  440.         // asArray = return array, otherwise return hash
  441.         var rv = asArray ? new Array() : new Object();
  442.         var cstr = GM_getValue("UserColor"+purl);
  443.         if (!cstr) return rv;
  444.         // Split string on spaces
  445.         var clist = cstr.split(/\s+/);
  446.         for (c=0; c < clist.length; c++) {
  447.             var cname = clist[c].toLowerCase();
  448.             if (!cname || cname == 'none') continue; // compatibility with old versions
  449.             if (asArray) {
  450.                 rv.push(cname);
  451.             } else {
  452.                 rv[cname] = clist[c];
  453.             }
  454.         }
  455.         return rv;
  456.     },
  457.     userPhotoClick: function(el, evt) {
  458.         if (evt) {
  459.             evt.preventDefault();
  460.             evt.stopPropagation();
  461.             GM_log(evt);
  462.             evt.returnValue = false;
  463.         }
  464.         // Establish the new pop-up menu that will be used for "/photos/UserName/" links:
  465.         // GM_log(el.id + ' from ' + this.ms);
  466.         var dispid    = this.registerUserPhoto(el);
  467.         var uname     = this.getTranslation(dispid, 'uname');
  468.         var purl      = this.getTranslation(dispid, 'purl');
  469.         var nsid      = this.getTranslation(dispid, 'nsid');
  470.         var colHash   = this.colors4user( purl );
  471.         // GM_log("User click on "+el.id+" ("+el+") = "+dispid);
  472.         // Close box and name:
  473.         var html = "<span class='CancelButt'>[x]</span>&nbsp;<b>"+
  474.         (uname ? uname : dispid)+
  475.         "</b>&nbsp;<span style='color:green;cursor:help' id='"+
  476.         this.fids.csfHelp+"'>[?]</span>\n<div id='currentColors'>";
  477.         for (var ucol in colHash) {
  478.             html += this.userColorBlock( colHash[ucol], 'SetColor' );
  479.         }
  480.         html += "</div>\n";
  481.  
  482.         // Each 'item' is a line in the popup - see makeMenu()
  483.         var items = new Array();
  484.         var links = new Array();
  485.         // This is what the link used to point to:
  486.         links.push( ['Photos', "/photos/"+purl+"/" ] );
  487.         // Add in the user's profile, favorites, sets and tags:
  488.         links.push( [ 'Profile', "/people/"+purl+"/" ] );
  489.         links.push( [ 'Favorites', "/photos/"+purl+"/favorites/"] );
  490. // OK TEST 06.2018 - ARCHIVES   - "/archives/date-taken/detail/?view=sm"
  491. links.push( [ 'Archive', "/photos/"+purl+"/archives/date-taken/detail/?view=sm/"] );
  492.         links.push( [ 'Sets', "/photos/"+purl+"/sets/"] );
  493.         links.push( [ 'Tags', "/photos/"+purl+"/tags/"] );
  494.         if (nsid) {
  495.             links.push( ['Mail', "/messages_write.gne?to="+nsid]);
  496.         }
  497.         if (this.env.pageView == 'Pool') {
  498.             // We are on a pool-related page
  499.             if (nsid) links.push ( ['All Pool Photos', '/groups/'+this.env.group+'/pool/'+nsid]);            if (uname && this.objects.comments) {
  500.                 // Pre-computed comments are available
  501.                 if (this.objects.comments[uname]) {
  502.                     // We found at least one comment for this user
  503.                     var coms = this.objects.comments[uname];
  504.                     var cn   = coms.length;
  505.                     var cmag = {href: "javascript:void(0)", 'class': 'ShowComments', text: ctxt};
  506.                     var ctxt = (this.objects.shownComments[uname] ? 'Hide ' : 'Show ') + cn;
  507.                     ctxt    += " comment"+(cn == 1 ? '' : 's')+ " on this page";
  508.                     cmag.text = ctxt;
  509.                     items.push(cmag);
  510.                 } else {
  511.                     // No comments for this user
  512.                     items.push("<i>No comments found</i>");
  513.                 }
  514.                 // Are the AJAX requests succesfully completed?
  515.                 var stat = this.apiStatus('getComments');
  516.                 if (stat != "") {
  517.                     // Warn the user that some data was not available.
  518.                     items.push({ tag:'i', style:'color:orange; font-size:xxsmall', text:"&rArr;Data missing: "+stat})
  519.                 }
  520.             }
  521.         }
  522.          if (uname) {
  523.             // John Watson's Scout page - list of the user's interesting photos
  524.             links.push
  525.                 ( [ 'Scout (CSF)', "http://flagrantdisregard.com/flickr/scout.php?username="+uname] );
  526.             // Nathan Siemer's HiveMind
  527.             links.push
  528.             ( [ 'HiveMind (CFS)', "http://flickrhivemind.net/User/"+uname] );
  529.  
  530. // DECEMBRE TWEAK - TEST OK - 2018 - ADD VIEWERS in POPUP (contacts page)
  531.  
  532. // TEST OK - 2018 - HiveMind Interesting -  (use +uname+)
  533. // http://flickrhivemind.net/flickr_hvmnd.cgi?search_type=User&textinput=10946586@N02&search_type=Search+User&photo_number=500&photo_type=100&sort=Interestingness&page=1&tag_mode=
  534.  links.push
  535.             ( [ 'HiveMind Interesting TEST  uname', "http://flickrhivemind.net/flickr_hvmnd.cgi?search_type=User&textinput="+uname+"&search_type=Search+User&photo_number=500&photo_type=100&sort=Interestingness&page=1&tag_mode="] );
  536.  
  537. // TEST OK - 2018 - HiveMind UserFAV -  (use +purl+)
  538. // http://flickrhivemind.net/flickr_hvmnd.cgi?search_type=Favorites&textinput=...%20Tina
  539.  links.push
  540.             ( [ 'HiveMind UserFAV - TEST uname', "http://flickrhivemind.net/flickr_hvmnd.cgi?search_type=Favorites&textinput="+uname] );
  541.  
  542. // OK TEST OK - 2018 - FLUIDR -  (use +uname+)
  543. // http://www.fluidr.com/photos/tinatink/favorites
  544. // http://www.fluidr.com/photos/tinatink
  545.             links.push
  546.             ( [ 'FLUIDR', "http://www.fluidr.com/photos/"+uname] );
  547.  
  548. // TEST OK - 2018 - Archives (use +purl+)
  549. // PM =   iconlink += '<a title="User Photo Archive (Small)" href="https://www.flickr.com/photos/' + userid + '/archives/date-taken/detail/?view=sm">' + iconarch + '</a> ';
  550. // https://www.flickr.com/photos/10946586@N02/archives/date-taken/detail/?view=sm
  551.             links.push
  552.             ( [ 'Archives', "https://www.flickr.com/photos/"+purl+"/archives/date-taken/detail/?view=sm"] );
  553. // END TEST ADD LINKS IN POPUP (contacts pages)
  554.  
  555.         }
  556.         if (links.length > 0) {
  557.             // The number of links to stuff into one line:
  558.             var linksPerLine = 3;
  559.             for (var l=0;l<links.length; l++) {
  560.                 html += "| <a class='simple_butt' href='"+links[l][1]+"'>"+links[l][0]+"</a> ";
  561.                 if (!((l+1) % linksPerLine)) html += "|</br>";
  562.             }
  563.             if (links.length % linksPerLine) html += "|</br>";
  564.         }
  565.         // Make a menu of colors to list:
  566.         items.push({ tag:'b', text: 'Available Flags:' });
  567.  
  568.         html += this.makeMenu( items ) +"\n<div style='font-size:smaller' id='"
  569.         + this.fids.csfAvl + "'>";
  570.         for (var c=0; c < this.user.colList.length; c++) {
  571.             var col   = this.user.colList[c];
  572.             if (colHash[col.toLowerCase()]) continue;
  573.             var linkHtml = this.userColorBlock( col, 'SetColor' );
  574.             if (linkHtml != "") html += linkHtml;
  575.         }
  576.         html += "</div>\n";
  577.  
  578.         // Pop up the window
  579.         var div = this.popUp(el, html, evt);
  580.         // Also associate the PURL with the popup, we'll need it for processing clicks later
  581.         var hb = document.getElementById(this.fids.csfHelp);
  582.         if (hb) hb.addEventListener('click', function (e) {
  583.             return CatFlickrSuite.settingsClick(this, e);}, false);
  584.         this.privateHash(div, 'purl', purl);
  585.         this.privateHash(div, 'dispid', dispid);
  586.         this.privateHash(div, 'onclose', function() {
  587.                 CatFlickrSuite.updateUser( el );
  588.             } );
  589.         GM_log("DEBUG="+el.href);
  590.         return false;
  591.     },
  592.     userColorBlock: function(col, cname) {
  593.         // Returns a single div for a category color
  594.         if (/^none$/i.test(col)) return ""; // compatibility with old versions
  595.         // Get the description for this color:
  596.         var text  = this.user.colDesc[col.toLowerCase()];
  597.         // Use the color name itself if no description available:
  598.         if (!text) text = col;
  599.         return this.makeMenu( [ {
  600.             tag: 'div', 'class':cname, text: "<em>"+text+"</em>", colName: col, style: "background-color:"+col
  601.         } ] );
  602.     },
  603.     escapeXML: function (txt) {
  604.         if (txt == null) return '';
  605.         txt = txt.replace(/&gt;/g,'>');
  606.         txt = txt.replace(/&lt;/g,'<');
  607.         txt = txt.replace(/&amp;/g,'&');
  608.         return txt;
  609.     },
  610.     unescapeXML: function (txt) {
  611.         if (txt == null) return '';
  612.         txt = txt.replace(/\>/g,'&gt;');
  613.         txt = txt.replace(/\</g,'&lt;');
  614.         txt = txt.replace(/\&/g,'&amp;');
  615.         return txt;
  616.     },
  617.     liveUserPhotoList: function () {
  618.         /* Designed to always return a list of user photos, even
  619.            if the page has been modified by another script */
  620.         var arr = new Array();
  621.         var links = document.getElementsByTagName("a");
  622.         for (var l=0; l < links.length; l++) {
  623.             var el = links[l];
  624.             if (this.isUserPhoto(el)) arr.push(el);
  625.         }
  626.         return arr;
  627.     },
  628.     isUserPhoto: function (el) {
  629.         if (!el) return false;
  630.         var dispid  = el.text;
  631.         // Skip special links:
  632.         if (dispid == null || dispid == '' || this.specLinkHash[dispid.toLowerCase()] ||
  633.             /photostream$/.test(dispid)) return false;
  634.         var hit  = el.href.match( this.re.userPhotos );
  635.         if (!hit) return false;
  636.         return hit[1];
  637.     },
  638.     colorUserPhotos: function (purlReq) {
  639.         if (this.user.colorUser != 1) return;
  640.         // This routine colors <a> tags pointing to
  641.         // "/photos/UserName/", so long as you have assigned a color
  642.         // to that particular user. If purlReq is null, then all links
  643.         // are processed, otherwise only the requested purl will be
  644.         // altered.
  645.         var arr = this.liveUserPhotoList();
  646.         this.gmMsg += "Coloring User photos for '"+purlReq+"' out of " +arr.length+" total images\n";
  647.         for (var i=0; i < arr.length; i++) {
  648.             var el     = arr[i];
  649.             // if (this.privateHash(el, 'cupDone')) continue;
  650.             this.privateHash(el, 'cupDone', true);
  651.             var purl   = this.privateHash(el, 'purl');
  652.             // Skip if there is a specific request and this is not it
  653.             if (purlReq && purl != purlReq) continue;
  654.             // this.gmMsg += "Coloring "+purl+"\n";
  655.             var colArr  = this.colors4user( purl, true );
  656.             var colNum  = colArr.length;
  657.             var text    = this.privateHash(el, 'innerText');
  658.             var htmlNow = el.innerHTML;
  659.             if (colNum < 1) {
  660.                // Decolorize the link if it was previously colored:
  661.                 if (text) el.innerHTML = htmlNow.replace(/\<span[^\>]+span\>/,'');
  662.                 continue;
  663.             }
  664.             if (!text) {
  665.                 text = this.unescapeXML(el.text);
  666.                 this.privateHash(el, 'innerText', text);
  667.             }
  668.             // The routine below will color the user's name into one or more colored blocks:
  669.             var tlen   = text.length;
  670.             var step   = tlen / colNum;
  671.             var html   = "";
  672.             for (var c=0; c < colNum; c++) {
  673.                 var start = Math.floor(0.5 + c * step);
  674.                 var end   = (c == colNum - 1) ? tlen : Math.floor(0.5 + (c+1) * step);
  675.                 if (start == end) continue;
  676.                 html += "<span style='background-color:"+colArr[c]+"'>"+
  677.                     this.escapeXML(text.substring(start, end)) + "</span>";
  678.             }
  679.             if (/\<span/.test(el.innerHTML)) {
  680.                 el.innerHTML = htmlNow.replace(/\<span.+span\>/, html);
  681.             } else {
  682.                 el.innerHTML = htmlNow.replace(text, html);
  683.             }
  684.         }
  685.         this.msg();
  686.     },
  687.     tagAssociation: function () {
  688.         var main  = document.getElementById(this.fids.main);
  689.         if (!main) return;
  690.         var type = this.env.pageType;
  691.         if (type == 'Set') {
  692.             // Only associate with sets you own
  693.             var you   = this.whoami();
  694.             var owner = this.env.setOwn;
  695.             if (!owner || !you || owner != you) return;
  696.         } else if (type != 'Group') {
  697.             return;
  698.         }
  699.         var div = document.getElementById(this.fids.csfTag);
  700.         if (!div) {
  701.             // Make the div if we have not already done so
  702.             div = document.createElement('div');
  703.             div.id = this.fids.csfTag;
  704.             main.insertBefore(div, main.firstChild);
  705.             div.style.border = 'solid navy 1px';
  706.         }
  707.         var tags = new Array();
  708.         var hash = this.getTagAssociations(  );
  709.         for (var tag in hash) {
  710.             if (tag && tag != '') tags.push( tag );
  711.         }
  712.         var html = "";
  713.         var form = document.createElement('form');
  714.         form.innerHTML = "<i>Associate tag rule with this "+type+":</i> "+
  715.         "<input name='tagassoc' width='20' value='' />";
  716.         div.innerHTML = html;
  717.         div.appendChild(form);
  718.         form.addEventListener('submit', function (e) {
  719.             e.preventDefault();
  720.             return CatFlickrSuite.addTagAssociation(this);}, false);
  721.         if (tags.length != 0) {
  722.             var tform = document.createElement('form');
  723.             var thtml = "<input type='submit' id='cfs_ta_search' value='Find new members with tag:'>";
  724.             tags = tags.sort();
  725.             for (var t=0; t < tags.length; t++) {
  726.                 var tag = tags[t];
  727.                 thtml += "<span style='border: grey solid 1px; margin: 3px; padding: 3px;'><input type='checkbox' name='cfs_ta' value='"+tag+
  728.                     "' checked='checked'/>"+hash[tags[t]] +" <span class='CancelButt'>[x]</span></span> ";
  729.             }
  730.             var lim = GM_getValue("TagAssocLimit", '');
  731.             thtml += "<br /><span style='color: #006'>";
  732.             thtml += "Report at most <input id='cfs_ta_limit' size='3' value='"+lim+
  733.                 "' /> matches";
  734.             thtml += ", uploaded between <input id='cfs_ta_minage' size='3' />";
  735.            thtml += " to <input id='cfs_ta_maxage' size='3' /> days ago";
  736.             thtml += "</span>\n";
  737.             tform.innerHTML = thtml;
  738.             div.appendChild(tform);
  739.             // Monitor tag interactions:
  740.             tform.addEventListener('click', function (e) {
  741.                 return CatFlickrSuite.tagAssocClickHandler(e);}, false);
  742.         }
  743.         this.taDiv = div;
  744.     },
  745.     intFromField: function( fieldId, desc, errors) {
  746.         var obj = document.getElementById( fieldId);
  747.         if (!obj) return null;
  748.         if (/^\d+$/.test(obj.value)) return obj.value;
  749.         if (desc && errors) errors.push(desc);
  750.         return null;
  751.     },
  752.     UTC: new Date(1970,0,1),
  753.     flickrDateFormat: function( offset ) {
  754.         if (!offset) offset = 0;
  755.         var dt = new Date();
  756.         dt.setDate(dt.getDate() - offset);
  757.         return (dt.getTime() - this.UTC.getTime()) / 1000 ;
  758.         return dt.toUTCString();
  759.  
  760.         var string = dt.getFullYear();
  761.         var bits   = [ dt.getMonth() + 1, dt.getDate(),
  762.                        dt.getHours(), dt.getMinutes(), dt.getSeconds() ];
  763.         for (var b=0; b < bits.length; b++) {
  764.             var val = bits[b];
  765.             if (val < 10) val = '0' + val;
  766.             string += val;
  767.         }
  768.         return string;
  769.     },
  770.     gmObjId: function () {
  771.         var type = this.env.pageType.toLowerCase();
  772.         if (!type || type == '' || !this.env[type] ) return '';
  773.         return type + '_' + this.env[type];
  774.     },
  775.     getTagAssociations: function() {
  776.         var hash = new Object();
  777.         var gid  = this.gmObjId();
  778.         if (gid == '') return hash;
  779.         var txt = GM_getValue("TagAssoc_"+gid, '');
  780.         var words = txt.split(',');
  781.         for (var w=0; w < words.length; w++) {
  782.             var raw = words[w];
  783.             hash[ this.cleanTag(raw) ] = raw;
  784.         }
  785.         return hash;
  786.     },
  787.     setTagAssociations: function(hash) {
  788.         var gid  = this.gmObjId();
  789.         if (!hash || gid == '') return 0;
  790.         var arr = new Array();
  791.         for (var tag in hash) {
  792.             if (tag && tag != '') arr.push(hash[tag]);
  793.         }
  794.         arr = arr.sort();
  795.         GM_setValue("TagAssoc_"+gid, arr.join(','));
  796.         return arr.length;
  797.     },
  798.     addTagAssociation: function (el) {
  799.         var val = this.valuesForNamedInput(el, 'tagassoc');
  800.         if (!val || val.length != 1 || ! val[0]) return;
  801.         var raw  = val[0];
  802.         var tag  = this.cleanTag(raw);
  803.         var hash = this.getTagAssociations();
  804.         hash[ tag ] = raw;
  805.         this.setTagAssociations(hash);
  806.         // Redraw the interface
  807.         this.tagAssociation();
  808.     },
  809.     tagAssocClickHandler: function(e) {
  810.         // Respond to a click event in a tag list
  811.         if (!e || !e.originalTarget) return;
  812.         var targ  = e.originalTarget;
  813.         var cn, id;
  814.         try {
  815.             // For some reason this throws a Permission Denied sometimes
  816.             cn = targ.className;
  817.             id = targ.id;
  818.         } catch (e) {
  819.             this.err("Failed to find node information for "+targ, e);
  820.         }
  821.         if (cn == 'CancelButt') {
  822.             var el    = targ.parentNode.firstChild;
  823.             if (!el || !el.value) return;
  824.             var raw  = el.value;
  825.             var hash = this.getTagAssociations();
  826.             delete hash[ this.cleanTag(raw) ];
  827.             this.setTagAssociations(hash);
  828.             this.tagAssociation(); // Redraw the interface
  829.         } else if ( id == 'cfs_ta_search' ) {
  830.             e.preventDefault();
  831.             this.searchTagAssociations();
  832.         }
  833.     },
  834.     cleanTag: function (raw) {
  835.         // "Hot Dog" !cat rat !"My Mouse"
  836.         // leading exclamation point
  837.         if (raw == null) return '';
  838.         var tag = raw.toLowerCase();
  839.         tag = tag.replace(/\s+/g, ' ');   // Space runs trimmed to single spaces
  840.         tag = tag.replace(/\! /g, '!');   // Remove trailing space after !
  841.         var tagList = new Array();
  842.         // Identify and extract contents of quoted strings
  843.         var quotes  = [ "'", '"' ];
  844.         for (var q = 0; q < quotes.length; q++) {
  845.             var qt = quotes[q];
  846.             // Make sure ! is inside quotes:
  847.             var swRE   = new RegExp('!' + qt, 'g');
  848.             tag        = tag.replace(swRE, qt + '!');
  849.             var nestRe = new RegExp(qt+ '([^' + qt + ']+)' + qt);
  850.             while (1) {
  851.                 var quoted = tag.match( nestRe );
  852.                 if (!quoted || quoted.length < 2) break;
  853.                 var inner = quoted[1];
  854.                 // Check for leading !
  855.                 var isNot = /^\!/.test(inner) ? '!' : '';
  856.                 inner     = inner.replace(/[^a-z0-9]/g, '');
  857.                 tagList.push( isNot + inner );
  858.                 tag = tag.replace(quoted[0],'');
  859.             }
  860.         }
  861.         tag = tag.replace(/^\s+/, '');   // Remove leading space
  862.         tag = tag.replace(/\s+$/, '');   // Remove trailing space
  863.         var words = tag.split(/\s+/);
  864.         for (var w = 0; w < words.length; w++) {
  865.             var word  = words[w];
  866.             var isNot = /^\!/.test(word) ? '!' : '';
  867.             word     = word.replace(/[^a-z0-9]/g, '');
  868.             tagList.push( isNot + word );
  869.         }
  870.         // GM_log("Raw: "+raw+" | Parsed: "+tagList.join(' '));
  871.         return tagList.join(' ');
  872.     },
  873.     searchTagAssociations: function () {
  874.         var cbs  = document.getElementsByName('cfs_ta');
  875.         if (!cbs || cbs.length < 1) return;
  876.         var type = this.env.pageType;
  877.         var ta   = this.tagAssocResults = { shown: {}, shownum: 0 };
  878.         var nsid = this.myNSID();
  879.         if (!nsid) {
  880.             alert("Failed to determine your Flickr NSID for searching");
  881.             return;
  882.         }
  883.         var commonFlickrArgs = {
  884.             method: 'flickr.photos.search', user_id: nsid, per_page: 500, extras: 'icon_server'
  885.         };
  886.  
  887.         var atErr = new Array();
  888.         var minA  = this.intFromField('cfs_ta_minage', "minimum age", atErr);
  889.         var maxA  = this.intFromField('cfs_ta_maxage', "maximum age", atErr);
  890.         if (minA && maxA && minA > maxA) {
  891.             var tmp = minA;
  892.             minA    = maxA;
  893.             maxA    = tmp;
  894.         }
  895.         if (minA) {
  896.             commonFlickrArgs.min_upload_date = this.flickrDateFormat( minA );
  897.         }
  898.         if (maxA) {
  899.             commonFlickrArgs.max_upload_date = this.flickrDateFormat( maxA );
  900.         }
  901.         // GM_log("Date range: "+commonFlickrArgs.min_upload_date + ' to ' + commonFlickrArgs.max_upload_date);
  902.  
  903.         if (type == 'Set') {
  904.         } else if (type == 'Group') {
  905.             commonFlickrArgs.privacy_filter = 1; // Assume we want only public photos for groups
  906.         } else {
  907.             alert("I do not know how to find photos for a '"+type+"' page");
  908.             return;
  909.         }
  910.         ta.what = type;
  911.  
  912.         // Determine which tag criteria the user wants to use to find photos
  913.         var tagCriteria = new Object();
  914.         for (var c=0; c < cbs.length; c++) {
  915.             // Scan through all the user's tag criteria defined for this set / pool
  916.             var cb = cbs[c];
  917.             if (!cb.checked) continue; // Not selected for this search
  918.             // Each criteria should have one or more tags in it
  919.             var tagSet  = cb.value.split(' ');
  920.  
  921.             var crit    = { tags: [], not: [] };
  922.             var goodTag = 0;
  923.             for (var ts = 0; ts < tagSet.length; ts++) {
  924.                 var tag = tagSet[ts];
  925.                 if (!tag) continue;
  926.                 if (/\!/.test(tag)) {
  927.                     // This is a 'not' tag
  928.                     crit.not.push( tag.replace(/\!/g, '') );
  929.                 } else {
  930.                     crit.tags.push( tag );
  931.                 }
  932.                 goodTag++;
  933.             }
  934.             if (goodTag == 0) continue;
  935.             // Use the not requests to aggregate each criteria
  936.             crit.not.sort();
  937.             var nkey = crit.not.join(' ') || '';
  938.             if (!tagCriteria[ nkey ]) {
  939.                 tagCriteria[ nkey ] = {
  940.                     'not': crit.not,
  941.                     'and': new Array(),
  942.                     'or':  new Array(),
  943.                 };
  944.             }
  945.             var posTags = crit.tags;
  946.             if (posTags.length == 1) {
  947.                 // Single positive tag request, put it in an OR stack
  948.                 tagCriteria[ nkey ].or.push( posTags[0] );
  949.             } else if (posTags.length > 1) {
  950.                 tagCriteria[ nkey ].and.push( posTags.join(',') );
  951.             }
  952.         }
  953.  
  954.         var searchArgs = new Array();
  955.         for (var nkey in tagCriteria) {
  956.             var crit      = tagCriteria[ nkey ];
  957.             var orLen     = crit.or.length;
  958.             var andLen    = crit.and.length;
  959.             var notLen    = crit.not.length;
  960.             if (orLen + andLen + notLen == 0) continue; // We need at least one criteria...
  961.             var notHash;
  962.             if (notLen) {
  963.                 // There is a least one tag we should exclude
  964.                 notHash = new Object();
  965.                 for (var n = 0; n < notLen; n++) {
  966.                     notHash[ crit.not[n] ] = 1;
  967.                 }
  968.             }
  969.  
  970.             var flickArgs = new Object();
  971.             for (var cfa in commonFlickrArgs) {
  972.                 // Make a copy of the common arguments hash:
  973.                 flickArgs[ cfa ] = commonFlickrArgs[cfa];
  974.             }
  975.  
  976.             var notQuery  = "";
  977.             if (notLen) {
  978.                 // We will need to know the tags for each photo in order to exclude them
  979.                 flickArgs.extras = flickArgs.extras + ',tags';
  980.                 if (orLen || andLen) {
  981.                     // The NOT query is going to be combined with other positive criteria
  982.                     notQuery = " <span style='font-weight:bold; color:red'>but NOT</span> " +
  983.                         "<u>" + crit.not.join('</u> <b>OR</b> <u>') + "</u>";
  984.                 } else {
  985.                     // This is simply an exclusionary criteria
  986.                     searchArgs.push( [ flickArgs, {
  987.                         ticktype: 'tagAssoc', search: 'tags', not: notHash,
  988.                                       query: "<span style='font-weight:bold; color:red'>NOT</span> " +
  989.                                       "<u>" + crit.not.join('</u> <b>OR</b> <u>') + "</u>"} ] );
  990.                 }
  991.             }
  992.             if (orLen) {
  993.                 // Request to do an OR tag search
  994.                 // We need to locally copy the hash again
  995.                 var flickArgsCopy = new Object();
  996.                 for (var fa in flickArgs) {
  997.                     flickArgsCopy[fa] = flickArgs[ fa ];
  998.                 }
  999.                 flickArgsCopy.tags     = crit.or.join(',');
  1000.                 flickArgsCopy.tag_mode = 'any';
  1001.                 searchArgs.push( [ flickArgsCopy, {
  1002.                         ticktype: 'tagAssoc', search: 'tags', not: notHash,
  1003.                                       query:  "<u>" + crit.or.join('</u> <b>OR</b> <u>') +"</u>"+ notQuery }] );
  1004.             }
  1005.             if (andLen) {
  1006.                 // Request to do one or more AND tag searches
  1007.                 for (var ca = 0; ca < andLen; ca ++) {
  1008.                     // We need to locally copy the hash again
  1009.                     var flickArgsCopy = new Object();
  1010.                     for (var fa in flickArgs) {
  1011.                         flickArgsCopy[fa] = flickArgs[ fa ];
  1012.                     }
  1013.                     flickArgsCopy.tags     = crit.and[ca];
  1014.                     flickArgsCopy.tag_mode = 'all';
  1015.                     searchArgs.push( [ flickArgsCopy, {
  1016.                         ticktype: 'tagAssoc', search: 'tags', not: notHash,
  1017.                                       query: "<u>" + crit.and[ca].split(',').join('</u> <b>AND</b> <u>') +"</u>"+ notQuery } ] );
  1018.                 }
  1019.             }
  1020.         }
  1021.         var numSearch = searchArgs.length;
  1022.         if (numSearch == 0) {
  1023.             alert("You need to select at least one tag to perform a search");
  1024.             return;
  1025.         }
  1026.  
  1027.         // At least one valid set of criteria
  1028.         // Add the search to find pre-existing photos
  1029.         if (type == 'Set') {
  1030.             var sid = this.env.set;
  1031.             if (!sid) { alert("Unable to determine the Set identifier"); return; }
  1032.             searchArgs.push( [
  1033.                 { method: 'flickr.photosets.getPhotos', photoset_id: sid },
  1034.                 { ticktype: 'tagAssoc', search: 'photoset' } ] );
  1035.         } else if (type == 'Group') {
  1036.             var gid = this.groupID();
  1037.             if (!gid) { alert("Failed to find Flickr Group ID"); return }
  1038.             searchArgs.push( [
  1039.                 { method: 'flickr.groups.pools.getPhotos', per_page: 500, user_id: nsid, group_id: gid },
  1040.                 { ticktype: 'tagAssoc', search: 'photos' } ] );
  1041.         }
  1042.  
  1043.         // Set up a DIV for output:
  1044.         var resID = 'cfs_tag_search_results';
  1045.         var targ  = document.getElementById( resID );
  1046.         if (targ) {
  1047.             targ.innerHTML = "";
  1048.         } else {
  1049.             targ = document.createElement('div');
  1050.             targ.id = resID;
  1051.             this.taDiv.appendChild(targ);
  1052.         }
  1053.         ta.resultsDiv = targ;
  1054.  
  1055.         // Parse the limit value
  1056.         ta.limit  = 0;
  1057.         ta.limit  = this.intFromField('cfs_ta_limit', "search limit", atErr);
  1058.         if (ta.limit) GM_setValue("TagAssocLimit", ta.limit);
  1059.  
  1060.  
  1061.         // launch all Flickr API searches:
  1062.         for (var sa = 0; sa < searchArgs.length; sa++) {
  1063.             var div = document.createElement('div');
  1064.             var qry = searchArgs[sa][1].query;
  1065.             if (qry) {
  1066.                 qry = "with tags " + qry;
  1067.             } else {
  1068.                 qry = "already present in the " + type;
  1069.             }
  1070.             div.innerHTML = "<i>Searching for your photos "+qry+"</i>";
  1071.             targ.appendChild(div);
  1072.             searchArgs[sa][1].div = div;
  1073.             this.flickrApi( searchArgs[sa][0], 'tagAssocCB', searchArgs[sa][1] );
  1074.         }
  1075.         // The callback (tagAssocCB, below) will perform the subtraction of [Present - Needed]
  1076.     },
  1077.     tagAssocCB: function(rsp, args, cfsArgs) {
  1078.         // This function captures and combines two Ajax calls
  1079.         var st   = cfsArgs.search;
  1080.         var ta   = this.tagAssocResults;
  1081.         if (ta.complete) return;
  1082.         var targ = ta.resultsDiv;
  1083.         var qDiv = cfsArgs.div;
  1084.         var type = ta.what;
  1085.         if (st == 'tags') {
  1086.             // This query is reporting photos matching the user's tag criteria
  1087.             var notHash = cfsArgs.not;
  1088.             var keeping = new Array();
  1089.  
  1090.             for each (photos in rsp.photos) {
  1091.                 for each (photo in photos.photo) {
  1092.                     if (notHash) {
  1093.                         // We need to verify that the photo does not have an excluded tag
  1094.                         var notCount = 0;
  1095.                         var tags = photo['@tags'].split(' ');
  1096.                         for (var t = 0; t < tags.length; t++) {
  1097.                             if (notHash[ tags[t] ]) { notCount++; break; }
  1098.                         }
  1099.                         // If any of the excluded tags match, then do not use the photo
  1100.                         if (notCount) continue;
  1101.                     }
  1102.                     var pid = photo['@id'];
  1103.                     keeping.push( { id: pid, title: photo['@title'], src: 'http://static.flickr.com/'+
  1104.                                             photo['@server']+'/'+pid+'_'+photo['@secret']+'_s.jpg'} );
  1105.                 }
  1106.             }
  1107.             if (!ta.results) ta.results = new Array();
  1108.             var rdat = { photos: keeping, args: cfsArgs };
  1109.             ta.results.push( rdat );
  1110.             var numMatched = keeping.length;
  1111.             var nstyle     = 'color: blue;';
  1112.             if (numMatched == 0) {
  1113.                 rdat.done = 1;
  1114.                 nstyle = 'color: red;';
  1115.             }
  1116.             qDiv.innerHTML = "<b style='"+nstyle+"'>" + numMatched + "</b> photo"+
  1117.                 (numMatched == 1 ? ' matches ' : 's match ') + cfsArgs.query;
  1118.         } else {
  1119.             // This query is reporting photos already in the set / pool
  1120.             var numExisting = 0;
  1121.             ta.exists = new Object();
  1122.             for each (refs in rsp[st]) {
  1123.                 for each (photo in refs.photo) {
  1124.                     ta.exists[ photo['@id'] ] = 1;
  1125.                     numExisting++;
  1126.                 }
  1127.             }
  1128.             var msg = "<a target='_blank' href='http://www.flickr.com/groups/"+this.groupID()+"/pool/"+this.myNSID()+"/'><b>" + numExisting + "</b> photo" +
  1129.             (numExisting == 1 ? '' : 's') + "</a> found already in " + type;
  1130.             if (numExisting == 500) {
  1131.                 msg += ". <i style='font-size:0.7em; color:brown'>This is the maximum number of matches that Flickr will return. Some of the images suggested below (as being absent from the "+type+") may in fact already be present.</i>";
  1132.             }
  1133.             qDiv.innerHTML = msg;
  1134.         }
  1135.         // We need information from both the tag query and the pre-existing members to continue:
  1136.         if (!ta.exists || !ta.results) return;
  1137.  
  1138.         var toShow = new Array();
  1139.         var available = 0;
  1140.         for (var r = 0; r < ta.results.length; r++) {
  1141.             var rdat = ta.results[r];
  1142.             if (rdat.done) continue; // do not process result set if already processed
  1143.             var kept = rdat.photos;
  1144.             // Find results that are not already in pool / set
  1145.             var ok = 0;
  1146.             for (var k = 0; k < kept.length; k++) {
  1147.                 if (!ta.exists[ kept[k].id ]) {
  1148.                     // This photo is not in the pool
  1149.                     toShow.push( kept[k] );
  1150.                     ok++;
  1151.                 }
  1152.             }
  1153.             var msg = rdat.args.div.innerHTML;
  1154.             if (ok == 0) {
  1155.                 msg += ", all of which are already in the " + type;
  1156.             } else {
  1157.                 msg += ", <span style='font-weight:bold; color:green'>"+ok+ "</span> of which " +
  1158.                     (ok == 1 ? 'is' : 'are')+ " not yet in the " +type;
  1159.                 if (ok > available) available = ok;
  1160.             }
  1161.             rdat.args.div.innerHTML = msg;
  1162.             rdat.done = 1;
  1163.         }
  1164.         GM_setValue(this.groupID() + this.fids.grpAvl, available);
  1165.         if (toShow.length == 0) return;
  1166.  
  1167.         var you  = this.whoami();
  1168.         var form = ta.form;
  1169.         if (!form) {
  1170.             var clr  = document.createElement('button'); clr.innerHTML = "Clear All";
  1171.             var chk  = document.createElement('button'); chk.innerHTML = "Check All";
  1172.             var form = ta.form = document.createElement('form');
  1173.             form.innerHTML = "<input style='font-weight:bold; background-color: #6f6' type='submit' value='Submit to "+type+"' /> | ";
  1174.             form.appendChild(clr);
  1175.             form.appendChild(chk);
  1176.             targ.appendChild(form);
  1177.             // Arm the various buttons
  1178.             form.addEventListener('submit', function (e) {
  1179.                     // GM_log("E: " +e);
  1180.                 e.preventDefault();
  1181.                 return CatFlickrSuite.tagAssocSubmit(form);}, false);
  1182.             clr.addEventListener('click', function (e) {
  1183.                 e.preventDefault();
  1184.                 return CatFlickrSuite.setCheckboxes('orgIds', false);}, false);
  1185.             chk.addEventListener('click', function (e) {
  1186.                 e.preventDefault();
  1187.                 return CatFlickrSuite.setCheckboxes('orgIds', true);}, false);
  1188.         }
  1189.         var lim   = ta.limit;
  1190.         var defc  = (type == 'Set') ? " checked='checked'" : '';
  1191.         var togF  = this.toggleChildVisibility;
  1192.  
  1193.         for (var ts = 0; ts < toShow.length; ts++) {
  1194.             if (lim && ta.shownum >= lim) {
  1195.                 // Limit reached
  1196.                 ta.complete = 1;
  1197.                 var comp = document.createElement('div');
  1198.                 comp.innerHTML = "Requested limit of "+lim+" reached.";
  1199.                 comp.style.fontSize  = '0.7em';
  1200.                 comp.style.fontStyle = 'italic';
  1201.                 comp.style.color     = 'brown';
  1202.                 targ.appendChild(comp);
  1203.                 break;
  1204.             }
  1205.             var photo = toShow[ts];
  1206.             var pid   = photo.id;
  1207.             if (ta.shown[ pid ]) continue;
  1208.             var div   = document.createElement('div');
  1209.             div.className = 'photodiv';
  1210.             div.innerHTML = "<input type='checkbox' name='orgIds' value='"+pid+"' "+defc+"><a target='_blank' style='color: blue ! important' href='http://www.flickr.com/photos/"+you+"/"+pid+"/'>"+ photo.title+"</a><div style='display:none; position:relative;'><img src='"+photo.src+"' style='position:absolute; top: 5px; left:2em;' /></div>";
  1211.             div.addEventListener('mouseout',  togF, false);
  1212.             div.addEventListener('mouseover', togF, false);
  1213.             form.appendChild( div );
  1214.             ta.shown[ pid ] = 1;
  1215.             ta.shownum++;
  1216.         }
  1217.       // http://www.flickr.com/photos/organize/?ids=189644777,189670488
  1218.     },
  1219.     toggleChildVisibility: function( e ) {
  1220.         var obj  = e.target;
  1221.         while (obj && obj.className != 'photodiv') {
  1222.            obj = obj.parentNode;
  1223.         }
  1224.         if (!obj) return;
  1225.         var kids = obj.childNodes;
  1226.         if (!kids) return;
  1227.         if (e.type == 'mouseout') {
  1228.             kids[2].style.display = 'none';
  1229.             kids[1].style.backgroundColor = null;
  1230.        } else {
  1231.             kids[2].style.display = 'block';
  1232.             kids[1].style.backgroundColor = '#ff9';
  1233.          }
  1234.     },
  1235.     setCheckboxes: function(name, bool) {
  1236.         var ids = document.getElementsByName(name);
  1237.         if (!ids) return;
  1238.         for (var i=0; i<ids.length; i++) { ids[i].checked = bool }
  1239.     },
  1240.     tagAssocSubmit: function (frm) {
  1241.         var ids = document.getElementsByName('orgIds');
  1242.         if (!ids) return;
  1243.         var apiArgs = {  };
  1244.         var type    = this.env.pageType;
  1245.         if (type == 'Group') {
  1246.             apiArgs.method   = 'flickr.groups.pools.add';
  1247.             apiArgs.group_id = this.groupID();
  1248.         } else if (type == 'Set' && this.env.set) {
  1249.             apiArgs.method   = 'flickr.photosets.addPhoto';
  1250.             apiArgs.photoset_id = this.env.set;
  1251.         }
  1252.         if (!apiArgs.method) { alert("Failed to find ID for "+type); return; }
  1253.         var tot = 0;
  1254.         // We are going to be stripping elements out - this mucks up the ids[] array, so we will first map the checkboxes into a static array
  1255.         var elements = new Array();
  1256.         for (var i=0; i < ids.length; i++) {
  1257.             elements.push(ids[i]);
  1258.         }
  1259.         // Now we can cycle over elements:
  1260.         for (var i=0; i < elements.length; i++) {
  1261.             var el = elements[i];
  1262.             if (!el || !el.checked) continue;
  1263.             var par  = el.parentNode;
  1264.             var span = document.createElement('span');
  1265.             par.insertBefore(span,el);
  1266.             span.style.color = 'orange';
  1267.             span.style.fontSize = 'smaller';
  1268.             span.innerHTML = "Loading...";
  1269.             apiArgs.photo_id = el.value;
  1270.             par.removeChild( el );
  1271.             this.getAuthToken();
  1272.             this.flickrApi( apiArgs, 'tasubCB', {ticktype: 'groupLoad', el: span, failok: 1} );
  1273.             tot++;
  1274.         }
  1275.         if (tot < 1) { alert("Please select at least one photo"); return }
  1276.     },
  1277.     tasubCB: function (rsp, args, cfsArgs) {
  1278.         // This function just reports on the success or failure of an
  1279.         // attempt to add photos to a group or set
  1280.         var el  = cfsArgs.el;
  1281.         if (!el) return;
  1282.         var txt = rsp.toString();
  1283.         if (!txt || /^\s*$/.test(txt)) {
  1284.             // A response of nothing means success:
  1285.             el.style.color = 'green';
  1286.             el.innerHTML = 'Success! ';
  1287.         } else {
  1288.             el.style.color = 'red';
  1289.             var err = cfsArgs.error;
  1290.             if (!err) { err = "|Unknown Error|"; GM_log(txt) }
  1291.             el.innerHTML = err + ' ';
  1292.        }
  1293.     },
  1294.     cleanComments: function () {
  1295.         // Only relevant for "Comments You've Made":
  1296.         if (this.env.pageView != 'YourComments' ) return;
  1297.         var tabs = document.getElementsByTagName('table');
  1298.         var re  = new RegExp("Here's the latest \\d+\\)", "g");
  1299.         for (var t=0; t < tabs.length; t++) {
  1300.             var tab = tabs[t];
  1301.             if (tab.className != 'NewComments') continue;
  1302.             var par = tab.parentNode;
  1303.             var partabs = par.getElementsByTagName('table');
  1304.             var prior = partabs[0];
  1305.             var html = prior.innerHTML;
  1306.             var hit  = html.match(re);
  1307.             if (!hit || hit.length < 1) continue;
  1308.             tab.style.display = 'none';
  1309.  
  1310.             var span = document.createElement('span');
  1311.             span.style.color = 'blue';
  1312.             span.style.backgroundColor = 'yellow';
  1313.             span.innerHTML = "click to view comments";
  1314.             par.insertBefore(span, tab);
  1315.             tab.style.display = 'none';
  1316.             span.addEventListener('click', function (e) {
  1317.                 this.innerHTML = 'clicked';
  1318.                 var targ = this.nextSibling.style;
  1319.                 if (targ.display == 'none') {
  1320.                     this.innerHTML = 'click to hide comments';
  1321.                     targ.display   = 'block';
  1322.                 } else {
  1323.                     this.innerHTML = 'click to view comments';
  1324.                     targ.display   = 'none';
  1325.                 }
  1326.             }, false);
  1327.         }
  1328.     },
  1329.     annotateGroupList: function () {
  1330.         // alert(this.env.pageView);
  1331.         if (this.env.pageView != 'YourGroups' ) return;
  1332.         this.groupsWithTagMatches();
  1333.     },
  1334.     sortGroups: function() {
  1335.         var avail = this.groupsWithTagMatches();
  1336.         if (!avail.length) return;
  1337.         if (1) {
  1338.             sorted = avail.sort( function (a,b) { return b[1] - a[1]; } );
  1339.         } else {
  1340.             sorted = avail.sort( function (a,b) { return a[1] - b[1]; } );
  1341.         }
  1342.  
  1343.         for (var l = 0; l < sorted.length; l++) {
  1344.             var li = sorted[l][0];
  1345.             var ul = li.parentNode;
  1346.             if (!ul) continue;
  1347.             // GM_log("#"+l+" = "+li+" Group: "+li.cfsGroup+" Num: "+sorted[l][1]);
  1348.             ul.removeChild(li);
  1349.             ul.appendChild(li);
  1350.         }
  1351.     },
  1352.     groupsWithTagMatches: function () {
  1353.         if (this.groupsAvailable) return this.groupsAvailable;
  1354.         var lis    = document.getElementsByTagName('li');
  1355.         var idTest = /group_(\S+)/;
  1356.         var tot    = 0;
  1357.         var par;
  1358.         var found = new Array();
  1359.         for (var l = 0; l < lis.length; l++) {
  1360.             var li = lis[l];
  1361.             var id = li.id;
  1362.             if (!id) continue;
  1363.             var hits = id.match(idTest);
  1364.             if (!hits) continue;
  1365.             var available = GM_getValue(hits[1] + this.fids.grpAvl);
  1366.             if (available == null) {
  1367.                 available = -1;
  1368.             } else {
  1369.                 var ht = document.createElement('i');
  1370.                 ht.style.color = available == 0 ? 'gray' : 'orange';
  1371.                 ht.innerHTML = ' '+ available;
  1372.                 li.appendChild(ht);
  1373.                 tot += available;
  1374.                 par = li;
  1375.             }
  1376.             found.push([li, available]);
  1377.             li.cfsGroup = found.length;
  1378.         }
  1379.         if (par) {
  1380.             var msg = "<i style='color:orange'>"+tot+
  1381.             "</i> = photos matching your tag criteria available ";
  1382.             var mli = document.createElement('li');
  1383.             mli.innerHTML = msg;
  1384.             var srt = document.createElement('button'); srt.innerHTML = "Sort";
  1385.             mli.appendChild(srt);
  1386.             par  = par.parentNode;
  1387.             par.insertBefore(mli, par.firstChild);
  1388.             srt.addEventListener('click', function (e) {
  1389.                     e.preventDefault();
  1390.                     return CatFlickrSuite.sortGroups();}, false);
  1391.         }
  1392.         return this.groupsAvailable = found;
  1393.     },
  1394.     colorStats: function () {
  1395.         if (this.env.pageView != 'Stats' ) return;
  1396.         var nodes = document.getElementsByTagName('a');
  1397.         var isSearch = new RegExp('Searched for:');
  1398.         var isGroup  = new RegExp('\/groups\/');
  1399.         var isSet    = new RegExp('\/sets\/');
  1400.     var isTag    = new RegExp('\/([^\/]+)\/tags\/[^\/]+')
  1401.         for (var n=0; n<nodes.length; n++) {
  1402.         var node = nodes[n];
  1403.             if (!node || !node.href) continue;
  1404.         var styles = 0;
  1405.             var iH = node.innerHTML;
  1406.              if (isSearch.test(iH)) {
  1407.          styles = { color: 'green', fontWeight: 'bold', border:'solid green 1px' };
  1408.          } else if (isTag.test(iH)) {
  1409.          mat  = iH.match(isTag);
  1410.          styles = { fontWeight : 'bold', color: mat[1] == 'photos' ? 'purple' : 'blue' };
  1411.          } else if (isGroup.test(iH)) {
  1412.          styles = { fontWeight : 'bold', color: 'orange' };
  1413.          } else if (isSet.test(iH)) {
  1414.          styles = { fontWeight : 'bold', color: '#f0f' };
  1415.          }
  1416.         if (!styles) continue;
  1417.         for (var s in styles) {
  1418.             node.style[ s ] = styles[s];
  1419.         }
  1420.         }
  1421.     },
  1422.     findMultiPost: function () {
  1423.         // Only relevant for group photo pools:
  1424.         if (this.env.pageView != 'Pool' ) return;
  1425.         // Find users that have posted more than one photo on the current page
  1426.         var struct = new Object();
  1427.         // Scan all 'user photo links' - as found by registerUserPhoto()
  1428.         var arr  = this.liveUserPhotoList();
  1429.         var pool = this.objects.poolImages = new Object();
  1430.         for (var i=0; i < arr.length; i++) {
  1431.             var el  = arr[i];
  1432.             var par = el.parentNode;
  1433.             // Only take note of links inside a PoolList classed object
  1434.             if (par.className != 'PoolList') continue;
  1435.             var pid = this.imageIdFromNode(par);
  1436.             if (pid) pool[pid] = par;
  1437.             var purl   = this.privateHash(el, 'purl');
  1438.             if (!struct[purl]) {
  1439.                 // Initiate data stucture for this user
  1440.                 struct[purl] = {
  1441.                     pars: new Array(),
  1442.                     name: this.privateHash(el, 'dispid'),
  1443.                 };
  1444.             }
  1445.             struct[purl].pars.push( par );
  1446.         }
  1447.         var pcount = 0; // Total thumbnails on the page
  1448.         var ucount = 0; // Total unique users on the page
  1449.         var multi = new Array();
  1450.         // Now look at each unique user
  1451.         for (var purl in struct) {
  1452.             ucount++;
  1453.             var pars  = struct[purl].pars;
  1454.             var count = pars.length;
  1455.             pcount   += count;
  1456.             if (count > 1) {
  1457.                 // This user has more than one photo on the page
  1458.                 // Get the user's automatically generated color...
  1459.                 var col  = "rgb("+this.color4string( purl )+")";
  1460.                 // ... and set the background of each thumbnail to that color
  1461.                 if (this.user.colorMult == 1) {
  1462.                     for (var p=0; p < pars.length; p++) {
  1463.                         pars[p].style.backgroundColor = col;
  1464.                     }
  1465.                 }
  1466.                 var name = struct[purl].name;
  1467.                 // Now make a summary token to put at the top of the
  1468.                 // page - we will have a link that will be activated
  1469.                 // as a pop up:
  1470.                 var link = document.createElement('a');
  1471.                 link.href = "/photos/" + purl +"/";
  1472.                 link.innerHTML = name;
  1473.                 this.registerUserPhoto(link);
  1474.                 // ... which will be held in a span that will have the
  1475.                 // user's auto-color:
  1476.                 var span = document.createElement('span');
  1477.                 span.innerHTML = "<b>"+count+":</b>&nbsp;";
  1478.                 span.appendChild(link);
  1479.                 span.style.backgroundColor = col;
  1480.                 // Store in array for later sorting:
  1481.                 multi.push( [count, span ] );
  1482.             }
  1483.         }
  1484.         // If all images are singletons, return with no action:
  1485.         if (multi.length < 1) return;
  1486.         // Also exit if the user does not want a summary
  1487.         if (this.user.sumMult != 1) return;
  1488.         // Sort the users high-to-low:
  1489.         multi = multi.sort( function (a,b) { return b[0] - a[0]; } );
  1490.         // Slap in the little spans under the Main div:
  1491.         var target  = document.getElementById(this.fids.main);
  1492.         for (var m=0; m < multi.length; m++) {
  1493.             target.parentNode.insertBefore(multi[m][1],target);
  1494.         }
  1495.         // Generate a summary of how many users are unique:
  1496.         var divId = 'CatFlickrSuiteMultiDiv';
  1497.         var div = document.getElementById(divId);
  1498.         if (!div) {
  1499.             div = document.createElement('div');
  1500.             div.id = divId;
  1501.         }
  1502.         var uperc = Math.floor(100 * ucount / pcount);
  1503.         div.innerHTML = "<b>"+ucount + "</b> users = <b>"+uperc+"%</b> of maximum";
  1504.         target.parentNode.insertBefore(div,target);
  1505.     },
  1506.     getAllComments: function () {
  1507.         // Only relevant for group photo pools:
  1508.         if (this.env.pageView != 'Pool' ) return;
  1509.         // Do nothing if the user has requested so:
  1510.         if (this.user.getCom != 1) return;
  1511.         var pool = this.objects.poolImages;
  1512.         this.objects.comments = new Object();
  1513.         this.objects.shownComments = new Object();
  1514.         for (var pid in pool) {
  1515.             var ticket = this.requestImageComments( pid );
  1516.         }
  1517.     },
  1518.     imageIdFromNode: function(el) {
  1519.         var focus = el;
  1520.         while (!focus.href || !/\/photos\/[^\/]+\/\d+/.test(focus.href)) {
  1521.             if (focus.hasChildNodes()) {
  1522.                 focus = focus.firstChild;
  1523.             } else if (focus.nextSibling) {
  1524.                 focus = focus.nextSibling;
  1525.             } else {
  1526.                 // Failed to find image link
  1527.                 focus = null; break;
  1528.             }
  1529.         }
  1530.         var id;
  1531.         if (focus && focus.href) {
  1532.             var hits = focus.href.match( this.re.photoID );
  1533.             if (hits) id = hits[2];
  1534.         }
  1535.         return id;
  1536.     },
  1537.     requestImageComments: function( id ) {
  1538.         if (!id) return;
  1539.         // If we have already recovered comments for this id, do not do so again:
  1540.         if (!this.doneTask.getComments) this.doneTask.getComments = new Object();
  1541.         if (this.doneTask.getComments[ id ]++) return true;
  1542.         var tkey = 'getComments';
  1543.         // Set up ticket status queue if needed
  1544.         if (!this.ticketStatus[tkey]) this.ticketStatus[tkey] = new Object();
  1545.         return this.flickrApi
  1546.         ( { method: 'flickr.photos.comments.getList', photo_id: id },
  1547.           'ricCB', {ticktype: tkey} );
  1548.     },
  1549.     ricCB: function(rsp) {
  1550.         var hash = this.objects.comments;
  1551.         for each (comments in rsp.comments) {
  1552.             // for (var cs = 0; cs < rsp.comments.length; cs++) {
  1553.             // var comments = rsp.comments[cs];
  1554.             var pid  = comments['@photo_id'];
  1555.             for each (com in comments.comment) {
  1556.                 var uname  = com['@authorname'];
  1557.                 var nsid   = com['@author'];
  1558.                 this.setTranslation( { uname: uname, nsid: nsid } );
  1559.                 // var create = new Date( com.@datecreate );
  1560.                 var ctxt  = com + '';
  1561.                 // Strip out HTML tags:
  1562.                 ctxt = ctxt.replace(/(\<|\&lt\;).+?(\>|\&gt\;)/g,'');
  1563.                 // Collapse all whitespace runs to single spaces:
  1564.                 ctxt = ctxt.replace(/[\s\n\r\t]+/g, ' ');
  1565.                 if (/^\s*$/.test(ctxt)) ctxt = "HTML only" ;
  1566.                 // Store data under both authorname and photo ID (hash
  1567.                 // will collide only if someone is using a pure
  1568.                 // integer as a name AND a photo has same integer).
  1569.                 var info = { txt: ctxt, uname: uname, photo: pid };
  1570.                 if (!hash[uname]) hash[uname] = new Array();
  1571.                 if (!hash[pid])   hash[pid]   = new Array();
  1572.                 hash[uname].push(info);
  1573.                 hash[pid].push(info);
  1574.             }
  1575.         }
  1576.         this.msg();
  1577.     },
  1578.     checkClickEvent: function(evt) {
  1579.         if (!evt) {
  1580.             GM_log("Click event fails to pass the event object!");
  1581.             return false;
  1582.         }
  1583.         if (evt.which != 1) return false; // Only consider for left button
  1584.         var el = evt.target;
  1585.         while (1) {
  1586.             if (el.getAttribute && el.getAttribute('userPhoto')) {
  1587.                 // This is a link to a user photo stream
  1588.                 return this.userPhotoClick(el, evt);
  1589.             }
  1590.             if (!el.parentNode) break;
  1591.             el = el.parentNode;
  1592.         }
  1593.         // GM_log("Click on "+el+" deemed not a user photo link");
  1594.         return false;
  1595.     },
  1596.     registerUserPhoto: function(el) {
  1597.         // Take a link with target '/photos/USERID/' and add pop-up
  1598.         // functionality to it. This method also stores the link in an
  1599.         // internal structure
  1600.         var purl = this.isUserPhoto(el);
  1601.         if (!purl) return 0;
  1602.         var dispid  = el.text;
  1603.         if (this.privateHash(el, 'parsed')) return dispid;
  1604.         // Ok, this looks like the sort of link we want to modify
  1605.         // (and have not already done so)
  1606.         this.privateHash(el, 'purl', purl);
  1607.         this.privateHash(el, 'dispid', dispid);
  1608.         this.privateHash(el, 'parsed', true);
  1609.         this.setTranslation( { dispid: dispid, purl: purl } );
  1610.         el.setAttribute('userPhoto', dispid);
  1611.         return dispid;
  1612.     },
  1613.     insertSettings: function() {
  1614.         // Add a settings menu
  1615.         var targ = document.getElementById(this.fids.navOrg);
  1616.         if (!targ) {
  1617.             GM_log("Failed to find 'Organize' menu item for settings");
  1618.             return;
  1619.         }
  1620.         var liL  = document.createElement('li');
  1621.         var setL = document.createElement('a');
  1622.         setL.href = "javascript:void(0)";
  1623.         setL.innerHTML = "Flickr Suite Settings";
  1624.         setL.addEventListener('click', function (e) {
  1625.             return CatFlickrSuite.settingsClick(this, e);}, false);
  1626.         liL.appendChild(setL);
  1627.         targ.appendChild(liL);
  1628.     },
  1629.     settingsClick: function (el, evt) {
  1630.         var items = new Array();
  1631.  
  1632.        items.push("<span class='CancelButt'>[x]</span>&nbsp;"+
  1633.                    "<span style='font-weight:bold;color:blue'>"+
  1634.                    "Flickr Suite GreaseMonkey Settings</span>");
  1635.         for (var type in this.userSet) {
  1636.             // Cycle through each data type
  1637.             var info = this.userSet[type];
  1638.             if (type == 'checkbox') items.push( "<b>Active Suite Functions:</b>" );
  1639.             for (var tag in info) {
  1640.                 var val  = this.user[tag];
  1641.                 var desc = info[tag][0];
  1642.                 var inpt = { tag: 'input', type: type, id: 'CFS'+tag };
  1643.                 if (type == 'checkbox') {
  1644.                     inpt.text = desc;
  1645.                     if (val == 1) inpt.checked = 'checked';
  1646.                 } else if (type == 'text') {
  1647.                     inpt.text  = desc;
  1648.                     inpt.value = val;
  1649.                     inpt.size  = 5;
  1650.                 } else if (type == 'textarea') {
  1651.                     delete inpt.type;
  1652.                     inpt.tag  = type;
  1653.                     inpt.cols = 30;
  1654.                     inpt.rows = 10;
  1655.                     inpt.pre  = "<b>"+desc+":</b><br />";
  1656.                     inpt.text = val;
  1657.                 }
  1658.                 items.push(inpt);
  1659.             }
  1660.         }
  1661.         items.push( "<i>Close menu, then <b>reload page</b> to see changes</i>" );
  1662.         items.push({ pre: "Check ", post: " for updates to this suite", text: 'userscripts.org', target:'userscripts', href: 'http://userscripts.org/scripts/show/5016', });
  1663.         var html;
  1664.         try {
  1665.             // Some weirdness during development, so be safe in a try/catch
  1666.             html = this.makeMenu( items );
  1667.         } catch(e) {
  1668.             this.err("Failed to make settings menu for ", e);
  1669.             return false;
  1670.         }
  1671.         // Pop up the window
  1672.         var div = this.popUp(el, html, evt);
  1673.         this.privateHash(div, 'onclose', function() {
  1674.                 CatFlickrSuite.updateSettings();
  1675.             } );
  1676.         return false;
  1677.     },
  1678.     updateSettings: function (el) {
  1679.         // Note that we have succesfully gotten to the callback
  1680.         this.privateHash(el, 'onclose', false );
  1681.         // Scan all user configurable settings, put values in GreaseMonkey persistant store
  1682.         var uarr = this.userSetArr;
  1683.         for (var u=0; u < uarr.length; u++) {
  1684.             var type = uarr[u][0];
  1685.             var tag  = uarr[u][1];
  1686.             var inpt = document.getElementById('CFS' + tag);
  1687.             if (!inpt) continue;
  1688.             var val;
  1689.             if (type == 'checkbox') {
  1690.                 // Data in checkboxes
  1691.                 val = (inpt.checked) ? 1 : 0;
  1692.             } else {
  1693.                 val = inpt.value;
  1694.             }
  1695.             GM_setValue(tag , val);
  1696.         }
  1697.         // Reparse all user settings
  1698.         this.setSettings();
  1699.     },
  1700.     updateUser: function (el) {
  1701.         // Note that we have succesfully gotten to the callback
  1702.         this.privateHash(el, 'onclose', false );
  1703.         var purl    = this.privateHash(el, 'purl');
  1704.         var setCol = document.getElementById(this.fids.csfCols);
  1705.         if (setCol) {
  1706.             // There is a list of colors attributed to this user
  1707.             var kids = setCol.childNodes;
  1708.             var setCols = new Array();
  1709.             for (var k=0; k < kids.length; k++) {
  1710.                 var col = kids[k].getAttribute('colName');
  1711.                 if (col) setCols.push(col);
  1712.             }
  1713.             GM_setValue("UserColor"+purl, setCols.join(" "));
  1714.         }
  1715.         // Re-color any user links
  1716.         this.colorUserPhotos( purl );
  1717.     },
  1718.     color4string: function( txt ) {
  1719.         // This method takes a string (for example a user ID) and
  1720.         // converts it to a reproducible RGB color. There are probably
  1721.         // more elegant ways to do this (I'd like to know of them) -
  1722.         // the basic goal is to get good spread of the color spectrum,
  1723.         // without being too dark or too light, and avoiding gray
  1724.         // scales.
  1725.         // Make a string that's just the concatenation of all the decimal ascii codes:
  1726.         var hash = "";
  1727.         for (var j = 0; j < txt.length; j++) {
  1728.             var code = txt.charCodeAt(j);
  1729.             hash    += code.toString();
  1730.         }
  1731.         var col     = new Array();
  1732.         var colMv   = 0;
  1733.         // Break the long integer into three equal sized pieces:
  1734.         var block   = Math.floor(hash.length/3);
  1735.         var colSpan = this.colMax - this.colMin + 1;
  1736.         var colBuf  = this.colBuf;
  1737.         if (colBuf > colSpan / 4) colBuf = Math.floor(colSpan / 4);
  1738.         for (var b = 0; b < 3; b++) {
  1739.             var si   = b * block;
  1740.             var subh = hash.substring( si, si + block - 1 );
  1741.             // Turn the sub-hash into colMin-colMax (modulus colSpan)
  1742.             var val = this.colMin + ((parseInt(subh) * 7353) % colSpan);
  1743.             for (var c = 0; c < b; c++) {
  1744.                 // make sure this color value is far enough from the prior ones
  1745.                 var pval = col[c];
  1746.                 if (val > pval - this.colBuf && val < pval + this.colBuf) {
  1747.                     // This color index is too close to another
  1748.                     if (colMv == 0) colMv = (val < pval) ? -1 : 1;
  1749.                     if (colMv > 0) {
  1750.                         val = pval + this.colBuf;
  1751.                         if (val > this.colMax) val -= colSpan;
  1752.                     } else {
  1753.                         val = pval - this.colBuf;
  1754.                         if (val < this.colMin) val += colSpan;
  1755.                     }
  1756.                 }
  1757.             }
  1758.             col.push(val);
  1759.         }
  1760.         // Now finally rotate the three colors
  1761.         var rot = parseInt(hash) %3;
  1762.         for (var r = 0; r < rot; r++) {
  1763.             col.push( col.shift() );
  1764.         }
  1765.         return col;
  1766.     },
  1767.     popUp: function (el, html, evt) {
  1768.         // Popup a mini menu on a mouse click
  1769.         var div = document.getElementById(this.fids.csfPop);
  1770.         if (!div) {
  1771.             // Create the object once, then keep it
  1772.             div = document.createElement('div');
  1773.             div.id = this.fids.csfPop;
  1774.             div.className = 'ToolTip';
  1775.             div.style.zIndex = '9999';
  1776.             document.body.appendChild(div);
  1777.             // Using an event listener to monitor activity on our popup
  1778.             div.addEventListener('click', function (e) {
  1779.                     return CatFlickrSuite.popClick(e);
  1780.                 }, false);
  1781.         } else {
  1782.             // We are recycling the same object as the popup (so only
  1783.             // one popup allowed at a time). Registration of user
  1784.             // changes in the popup occurs when the user closes the
  1785.             // window - make sure that if the window was not
  1786.             // explicitly closed (that is, it is open in another
  1787.             // location) that we still save the user settings:
  1788.             var cb = this.privateHash(div, 'onclose');
  1789.             if (cb) cb( div );
  1790.         }
  1791.         // Position div under click (-15 offset attempts to get close box under mouse)
  1792.         div.style.top     = (evt.pageY - 15) + 'px';
  1793.         div.style.left    = (evt.pageX - 15) + 'px';
  1794.         // Update content:
  1795.         div.innerHTML     = html;
  1796.         // Make sure it is visible:
  1797.         div.style.display = 'block';
  1798.         // clear any private variables associated with the popup
  1799.         this.clearPrivate(div);
  1800.         return div;
  1801.     },
  1802.     popClick: function (e) {
  1803.         // Respond to a click event in our popup
  1804.         if (!e || !e.originalTarget) return;
  1805.         var targ  = e.originalTarget;
  1806.         var par   = targ.parentNode;
  1807.         var cname = targ.className;
  1808.         while (!cname && par) {
  1809.             // Back out of DOM tree until we find a classed target
  1810.             targ  = par;
  1811.             par   = targ.parentNode;
  1812.             cname = targ.className;
  1813.         }
  1814.         if (!par || !cname) return;
  1815.         if (cname == 'CancelButt') {
  1816.             // A click within a close button [x] - hide the popup
  1817.             this.popClose(par);
  1818.         } else if (cname == 'CloseComment') {
  1819.             this.toggleComments(par);
  1820.         } else if (cname == 'SetColor') {
  1821.             // A click within a 'set user color' selection
  1822.             var addTo = document.getElementById
  1823.             (par.id == this.fids.csfCols ? this.fids.csfAvl : this.fids.csfCols);
  1824.             if (!addTo) return;
  1825.             addTo.appendChild(targ);
  1826.         } else if (cname == 'ShowComments') {
  1827.             this.toggleComments(par);
  1828.         }
  1829.     },
  1830.     toggleComments: function (el) {
  1831.         var dispid = this.registerUserPhoto(el);
  1832.         var uname  = this.getTranslation(dispid, 'uname');
  1833.         if (!uname) return;
  1834.         var coms   = this.objects.comments[uname];
  1835.         if (!coms) return;
  1836.         var list  = this.objects.shownComments[uname];
  1837.         if (list) {
  1838.             // Already showing comments - hide them
  1839.             for (var l=0; l < list.length; l++) {
  1840.                 var el = list[l];
  1841.                 el.parentNode.removeChild(el);
  1842.             }
  1843.             this.objects.shownComments[uname] = null;
  1844.             return;
  1845.         }
  1846.         list = this.objects.shownComments[uname] = new Array();
  1847.         var struct = new Object();
  1848.         for (var i=0; i < coms.length; i++) {
  1849.             var info = coms[i];
  1850.             var pid  = info.photo;
  1851.             if (!struct[pid]) struct[pid] = new Array();
  1852.             struct[pid].push( info.txt );
  1853.         }
  1854.         var pool = this.objects.poolImages;
  1855.         var purl  = this.privateHash(el, 'purl');
  1856.         var col  = "rgb("+this.color4string( purl )+")";
  1857.         var elips = "<span style='color:red;font-weight:bold'>&hellip;</span>";
  1858.         for (var pid in struct) {
  1859.             var targ = pool[pid];
  1860.             if (!targ) {
  1861.                 this.err("Could not find pool image ID="+pid);
  1862.                 continue;
  1863.             }
  1864.             var html = "<span style='color:red;background-color:yellow' class='CloseComment'>["+dispid+"]</span>";
  1865.             var maxlen = this.user.comWidth;
  1866.             for (var c=0; c < struct[pid].length; c++) {
  1867.                 // If there are multiple comments, separate them by
  1868.                 // slightly differnt background colors
  1869.                 var com = struct[pid][c];
  1870.                 if (maxlen > 0 && com.length > maxlen) com = com.substr(0,maxlen) + elips;
  1871.                 html += "<span style='background-color:"+( c%2 ? '#cfc' : '#9f9')+"'>"+com+"</span>";
  1872.             }
  1873.             var el = document.createElement('div');
  1874.             el.innerHTML = html;
  1875.             targ.appendChild(el);
  1876.             el.style.fontSize  = this.user.comSize;
  1877.             el.style.border    = col+" solid 1px";
  1878.             el.style.textAlign = 'left';
  1879.             el.style.zIndex   = 100;
  1880.             el.style.position = 'relative';
  1881.             el.style.left     = '-4px';
  1882.             el.style.width    = '118px';
  1883.             el.className      = "CommentBox";
  1884.             this.privateHash(el, 'dispid', dispid);
  1885.             list.push(el);
  1886.             el.addEventListener('click', function (e) {
  1887.                     return CatFlickrSuite.popClick(e);
  1888.                 }, false);
  1889.             //targ.style.height = "100%";
  1890.         }
  1891.     },
  1892.     popClose: function (el) {
  1893.         // Does the window have a callback associated with it?
  1894.         var cb = this.privateHash(el, 'onclose');
  1895.         if (cb) cb( el );
  1896.         // Set display to none to hide menu
  1897.         el.style.display = 'none';
  1898.     },
  1899.     makeMenu: function( arr ) {
  1900.         // Build a little menu given an array of 'lines'
  1901.         if (!arr || arr.length < 1) return "";
  1902.         var lines = new Array();
  1903.         try {
  1904.             for (var i=0; i < arr.length; i++) {
  1905.                 var info = arr[i];
  1906.                 if (typeof(info) == 'string') {
  1907.                     // If this entry is a simple string, use it as is
  1908.                     lines.push(info);
  1909.                     continue;
  1910.                 }
  1911.                 // Otherwise, assume it is an object that will be turned
  1912.                 // into a tag, with keys being attribute names. The
  1913.                 // 'text' attribute is taken as the link text:
  1914.                 var tag = info.tag;
  1915.                 if (!tag || tag == '') tag = 'a';
  1916.                 var txt = ">"+info.text+"</"+tag+">";
  1917.                 // Free text to put after the link
  1918.                 if (info.post) txt += info.post;
  1919.                 // Remove special attrs so they do not become an part of the tag
  1920.                 delete info.tag; delete info.post; delete info.text;
  1921.                 var attrs = new Array();
  1922.                 for (var attr in info) {
  1923.                     var val = info[attr];
  1924.                     if (!val) val = "";
  1925.                     // Escape single quotes
  1926.                     if (typeof(val) == 'string') val = val.replace("'", "\\'");
  1927.                     attrs.push(attr+"='"+val+"'");
  1928.                 }
  1929.                 txt = "<"+tag+" "+attrs.join(' ')+txt;
  1930.                 if (info.pre) txt = info.pre + txt;
  1931.                 // Join all attributes into an anchor tag
  1932.                 lines.push(txt);
  1933.             }
  1934.         } catch (e) {
  1935.             this.err("Failed to make menu for "+arr.length+" items", e);
  1936.             return "";
  1937.         }
  1938.         // Join all lines into a single block of text
  1939.         return lines.join("<br />\n");
  1940.     },
  1941.     hashToLog: function (obj, title) {
  1942.         var msg = "Hash report for "+obj+":\n";
  1943.         if (title) msg += title + "\n";
  1944.         if (obj) {
  1945.             var num = 0;
  1946.             for (var key in obj) {
  1947.                 msg += key + ": "+obj[key]+"\n";
  1948.                 num++;
  1949.             }
  1950.             msg += num + " key"+(num == 1 ? '' : 's')+"\n";
  1951.         } else {
  1952.             msg += "/null/\n";
  1953.         }
  1954.         GM_log(msg);
  1955.     },
  1956.     valuesForNamedInput: function(form, name) {
  1957.         /* Huh. It used to work that if I had an <input> named 'foo' in a form
  1958.            represented by object bar, I could get it with bar.foo, but
  1959.            not anymore... */
  1960.         var rv = new Array();
  1961.         if (!form || !form.elements || !name) return null;
  1962.         name = name.toLowerCase();
  1963.         for (var e = 0; e < form.elements.length; e++) {
  1964.             var inp = form.elements[e];
  1965.             if (inp.name.toLowerCase() != name) continue;
  1966.             rv.push(inp.value);
  1967.         }
  1968.         return rv;
  1969.     },
  1970.     privateHash: function(el, tag, val) {
  1971.         // Private hash holding tag / value pairs on HTML elements
  1972.         // The function is both a setter and a getter
  1973.         var key = this.setElementId(el);
  1974.         if (!key || !tag)  return null;
  1975.         // Initialize the hash if this is the first access on the element:
  1976.         if (!this.privateData[key]) {
  1977.             // this.gmMsg += "Establish new private data for " + key+"\n";
  1978.             this.privateData[key] = new Object();
  1979.         }
  1980.         tag = tag.toLowerCase();
  1981.         if (val != null) {
  1982.             // Request to set the value
  1983.             this.privateData[key][tag] = val;
  1984.             // this.gmMsg += "Set "+tag+"="+val+" for " + key+"\n";
  1985.         }
  1986.         // Return the current value
  1987.         return this.privateData[key][tag];
  1988.     },
  1989.     logPrivateData: function( hash ) {
  1990.         if (!hash) hash = this.privateData;
  1991.         var txt = "Hash dump for " + hash + "\n";
  1992.         for (var key in hash) {
  1993.             txt += "["+key+"]\n";
  1994.             var kh = hash[key];
  1995.             if (typeof(kh) == 'object') {
  1996.                 for (var tag in kh) {
  1997.                     txt += "  ["+tag+"] = '"+kh[tag]+"'\n";
  1998.                 }
  1999.             } else {
  2000.                 txt += "  !! Hash expected, found '"+kh+"'\n";
  2001.             }
  2002.         }
  2003.         GM_log(txt);
  2004.     },
  2005.     setTranslation: function (data, debugMsg) {
  2006.         // Are any other data associated with these?
  2007.         for (var type in data) {
  2008.             var existing = this.translate[ data[type] ];
  2009.             if (existing) {
  2010.                 // One of these values is already recorded
  2011.                 var novel = this.mergeHash( existing, data );
  2012.                 if (novel == 0) {
  2013.                     // No new additions to the tranlsation hash:
  2014.                     return;
  2015.                 }
  2016.                 data = existing;
  2017.                 break;
  2018.             }
  2019.         }
  2020.         // If uname is not set it is the same as the display ID, if it is not elipsed
  2021.         if (!data.uname && data.dispid &&
  2022.             !this.re.elipsed.test(data.dispid)) data.uname = data.dispid;
  2023.         // We can generally get the username from the displayed ID
  2024.         if (!data.dispid && data.uname) {
  2025.             // I *THINK* 20 is the upper limit... ?
  2026.             if (data.uname < 20) {
  2027.                 data.dispid = uname;
  2028.             } else {
  2029.                 // Hmm...
  2030.             }
  2031.         }
  2032.         // Cycle through the hash again, update all keys in translation to point to same hash:
  2033.         for (var type in data) {
  2034.             if (debugMsg) this.gmMsg += type + " = " + data[type] + "\n";
  2035.             this.translate[data[type]] = data;
  2036.         }
  2037.     },
  2038.     getTranslation: function( term, type ) {
  2039.         var thash = this.translate;
  2040.         var data  = (term in thash) ? thash[term] : null;
  2041.         if (!data) return null; // Nothing at all found
  2042.         if (!type) return data; // The user wants the full hash
  2043.         // Specific term type requested for return
  2044.         if (type in data) return data[type]; // That type is present, return it
  2045.         // We found data, but no match for this type
  2046.         if ('dispid' in data && !('uname' in data) &&  this.re.elipsed.test(term)) {
  2047.             // The query term is an elipsed display id, which does not
  2048.             // yet have a true uname associated with it - see if we
  2049.             // can find a match to a username
  2050.             var elipre  = this.re.elipsed;
  2051.             // Thanks to John Carney for pointing out that I need to
  2052.             // escape RE tokens in the username when building this
  2053.             // regexp. I miss \Q..\E in perl!
  2054.             var matcher = new RegExp(term.replace(elipre,'').
  2055.                                      replace(/([(){}.^$*?+\[\]\\])/g, '\\$1')+'.+$');
  2056.             for (var tag in thash) {
  2057.                 // Ignore if this tag does not match, or is itself
  2058.                 // elipsed, or the matched hash does not have a
  2059.                 // username defined.
  2060.                 if (!matcher.test(tag) || elipre.test(tag) ||
  2061.                     !('uname' in thash[tag])) continue;
  2062.                 // We have a match - combine the two hashes
  2063.                 this.mergeHash(data, thash[tag]);
  2064.                 this.setTranslation(data);
  2065.                 if (type in data) return data[type];
  2066.             }
  2067.         }
  2068.         return null;
  2069.     },
  2070.     mergeHash: function(hashA, hashB) {
  2071.         // Adds the key/val pairs of hashB to hashA
  2072.         var newVals = 0;
  2073.         for (var tag in hashB) {
  2074.             if (!(tag in hashA)) { hashA[tag] = hashB[tag]; newVals++; }
  2075.         }
  2076.         return newVals;
  2077.     },
  2078.     setElementId: function(el) {
  2079.         if (!el) return null;
  2080.         if (!el.id) {
  2081.             // this.gmMsg += "New ID added to " + el + "\n";
  2082.             el.id = "CFS" + "_" + this.ms + '_' + ++this.counter;
  2083.         }
  2084.         return el.id;
  2085.     },
  2086.     clearPrivate: function(el) {
  2087.         // Clear out private variables associated with an element
  2088.         if (!el) return;
  2089.         var key = this.setElementId(el);
  2090.         var rv  = this.privateData[key];
  2091.         // Totally wipe the settings:
  2092.         // this.gmMsg += "Clearing private data for " +key+"\n";
  2093.         this.privateData[key] = new Object();
  2094.         return rv; // Return the old settings
  2095.     },
  2096.     buildGetString: function (args, noToken) {
  2097.         delete args[ "api_sig" ];
  2098.         if (unsafeWindow.global_auth_hash) {
  2099.             // I believe this is the old authentication?
  2100.             args[ "auth_hash" ] = unsafeWindow.global_auth_hash;
  2101.         } else {
  2102.             delete args[ "auth_hash" ];
  2103.         }
  2104.         // Always add the api_key:
  2105.         args[ "api_key" ] = this.apikey;
  2106.         // Add the auth token if it is available
  2107.         if (this.authTok) args[ "auth_token" ] = this.authTok;
  2108.         // if (!noToken && ! args['frob']) args[ "auth_token" ] = this.getAuthToken();
  2109.  
  2110.         // Sort all the parameter names:
  2111.         var argList  = new Array();
  2112.         for (var arg in args) { argList.push( arg ); }
  2113.         argList = argList.sort();
  2114.         // Build the basic parameter list
  2115.         var get = "";
  2116.         for (var al = 0; al < argList.length; al++) {
  2117.             var arg = argList[al];
  2118.             var val = args[arg];
  2119.             if (val == null) continue;
  2120.             if (get) get += "&";
  2121.             get += arg + "=" + val;
  2122.             sig += arg + val;
  2123.         }
  2124.         var signIt = args['auth_token'] || args['frob'] ? 1 : 0;
  2125.         if (signIt) {
  2126.             // We also need to sign the GET parameters
  2127.             var sig = this.getSecKey();
  2128.             for (var al = 0; al < argList.length; al++) {
  2129.                 var arg = argList[al];
  2130.                 var val = args[arg];
  2131.                 if (val == null) continue;
  2132.                 sig += arg + val;
  2133.             }
  2134.             // MD5 the signature string and append as a new parameter:
  2135.             var md5 = hex_md5(sig);
  2136.             get += "&api_sig="+md5;
  2137.         }
  2138.         return get;
  2139.     },
  2140.     getSecKey : function () {
  2141.         return this.seckey;
  2142.     },
  2143.     getAuthToken : function () {
  2144.         if (this.authTok) return this.authTok;
  2145.         // Try to get token from local store:
  2146.         this.authTok = GM_getValue("FlickrAuthTok");
  2147.         if (this.authTok) {
  2148.             GM_log("Authorization token recovered from local storage - " + this.authTok);
  2149.             return this.authTok;
  2150.         }
  2151.         // Need to generate new token:
  2152.         return this.frobToAuth();
  2153.     },
  2154.     frobToAuth: function () {
  2155.         var frob = this.getFrob();
  2156.         this.flickrApi( { method: "flickr.auth.getToken", frob: frob }, 'parseAuth' );
  2157.         return this.authTok;
  2158.     },
  2159.     parseAuth: function (rsp) {
  2160.         var toks = new Array();
  2161.         for each (auth in rsp.auth) {
  2162.            for each (tok in auth.token) {
  2163.                var token = tok + "";
  2164.                if (token) toks.push(token);
  2165.            }
  2166.         }
  2167.         if (toks.length == 1) {
  2168.             this.authTok = toks[0];
  2169.             GM_setValue("FlickrAuthTok", toks[0]);
  2170.             GM_log("Authorization token returned by Flickr API");
  2171.         } else {
  2172.             this.err("Failed to recover auth_token");
  2173.         }
  2174.         return this.authTok;
  2175.     },
  2176.     getFrob: function () {
  2177.         if (this.frob) return this.frob;
  2178.         // Try to get frob from local store:
  2179.         this.frob = GM_getValue("FlickrFrob");
  2180.         if (this.frob) {
  2181.             GM_log("Frob recovered from local storage");
  2182.             return this.frob;
  2183.         }
  2184.         // Need to go to authentication page
  2185.         var get = this.buildGetString( { perms: 'write' }, 'NO AUTH' );
  2186.         var url = "http://www.flickr.com/services/auth/?" + get;
  2187.         // Need to open a new page
  2188.         window.open(url, "_blank");
  2189.         var msg = "";
  2190.         if (this.frobTry) msg += "Hm. You have tried "+this.frobTry+
  2191.                               " time(s) but have not authenticated...\n";
  2192.         msg +=
  2193.         "Flicker Functional Suite has opened a new window.\n"+
  2194.         "It will request 'write' permission for your photo stream.\n"+
  2195.         "If you are comfortable with that, please grant permissions\n"+
  2196.         "and close this alert AFTER you grant them.\n"+
  2197.         "If you are not, you may ignore the request, but FFS will not function properly"
  2198.         alert(msg);
  2199.         this.frobTry++;
  2200.         return grabFrob();
  2201.     },
  2202.     grabFrob: function () {
  2203.         var loc = document.location.href;
  2204.         var mat = loc.match(this.re.frob);
  2205.         if (!mat) return;
  2206.         GM_setValue("FlickrFrob", this.frob = mat[1]);
  2207.         GM_log("Frob recognized from Flickr authentication page");
  2208.     },
  2209.     flickrApi: function( args, cbname, cfsArgs ) {
  2210.         /* Generic method for an AJAX call to the Flickr API
  2211.          * cfsArgs
  2212.          *   failok: if true, then still execute the callback when error code encountered
  2213.          */
  2214.         var callback = this[cbname];
  2215.         var get      = this.buildGetString( args );
  2216.         if (!get) {
  2217.             // Failed to build get string. This usually means that authentication is under way
  2218.             // Store the arguments for recovery after
  2219.             return this.delayApi( args, cbname, cfsArgs );
  2220.         }
  2221.  
  2222.         var url = "http://api.flickr.com/services/rest/?" + get;
  2223.  
  2224.         var ticket = ++this.ticket;
  2225.         if (!cfsArgs) cfsArgs = new Object();
  2226.         cfsArgs.ticket = ticket;
  2227.         cfsArgs.url = url;
  2228.         if (cfsArgs.ticktype) {
  2229.             var tkey = cfsArgs.ticktype;
  2230.             if (!this.ticketStatus[tkey]) this.ticketStatus[tkey] = new Object();
  2231.             this.ticketStatus[tkey][ticket] = 'pending';
  2232.         }
  2233.         GM_xmlhttpRequest
  2234.         ({ method: "GET", url: url, headers: {
  2235.             "User-agent": "Mozilla/4.0 (compatible) Greasemonkey (Flickr Functional Suite)",
  2236.                 "Accept": "application/atom+xml,application/xml,text/xml",
  2237.             }, onload: function(rsp) { CatFlickrSuite.parseXML(rsp,cbname,args,cfsArgs, url) } } );
  2238.         return ticket;
  2239.     },
  2240.     delayApi: function ( args, cbname, cfsArgs ) {
  2241.         if (!this.apiDelayStack) this.apiDelayStack = new Array;
  2242.         this.apiDelayStack.push( [ args, cbname, cfsArgs ] );
  2243.         return this.apiDelayStack.length;
  2244.     },
  2245.     resumeApi : function () {
  2246.         var stack = this.apiDelayStack;
  2247.         // Transfer full contents of delayed stack to local array
  2248.         var lStck = new Array();
  2249.         while (stack.length) {
  2250.             lStck.push( stack.shift() );
  2251.         }
  2252.         while (lStck.length) {
  2253.             var dat = lStck.shift();
  2254.             var ticket = this.flickrApi( dat[0], dat[1], dat[2] );
  2255.             GM_log("Resuming API call: "+dat[1]);
  2256.         }
  2257.     },
  2258.     parseXML: function(response, cbname, args,cfsArgs, url) {
  2259.         // Takes an XML response from Flickr services and turns it into a E4X object
  2260.         // http://developer.mozilla.org/presentations/xtech2005/e4x/
  2261.         var txt  = response.responseText;
  2262.         txt      = txt.replace(/\s*\<\?[^\>]+\?\>\s*/g,'');
  2263.         var rsp  = new XML(txt);
  2264.         var stat = rsp['@stat'];
  2265.         var tick = cfsArgs.ticket;
  2266.         var tt   = cfsArgs.ticktype;
  2267.         var rv   = null;
  2268.         if (stat != 'ok') {
  2269.             var msg = "Failed to retrieve Flickr data via API:\n";
  2270.             cfsArgs.error = '|';
  2271.             var seenErrs = new Object();
  2272.             for each (err in rsp.err) {
  2273.                 var ec = err['@code'];
  2274.                 seenErrs[ec] = 1;
  2275.                 msg += "  Error: " +ec+ " = "+err['@msg']+"\n";
  2276.                 // cfsArgs.error += 'id='+args.photo_id+' ';
  2277.                 cfsArgs.error += err['@msg'] + '|';
  2278.             }
  2279.             for (var arg in args) {
  2280.                 msg += "  "+arg+'='+args[arg]+"\n";
  2281.             }
  2282.             msg += "URL: "+ url;
  2283.             if (seenErrs['99']) {
  2284.                 // Insufficient priveleges. We need to start the authentication chain
  2285.                 if (cfsArgs.pendingAuth) {
  2286.                     msg += "\nFAILED PRIOR AUTHENTICATION";
  2287.                 } else {
  2288.                     this.delayApi( args, cbname, cfsArgs );
  2289.                     cfsArgs.error = '[Authenticating...]';
  2290.                     cfsArgs.pendingAuth = 1;
  2291.                     this.getAuthToken();
  2292.                     this.resumeApi();
  2293.                     return '[Authenticating...]';
  2294.                     msg = "Authentication required, attempting to authenticate";
  2295.                 }
  2296.             }
  2297.             this.err(msg);
  2298.             if (tt) this.ticketStatus[tt][tick] = 'failed';
  2299.             if (!cfsArgs.failok) return rv;
  2300.         }
  2301.         try {
  2302.             rv = this[cbname](rsp, args, cfsArgs);
  2303.         } catch (e) {
  2304.             this.err("Failed to execute API callback "+cbname, e);
  2305.             if (tt) this.ticketStatus[tt][tick] = 'failed';
  2306.         }
  2307.         // Remove the ticket from the queue as being completed
  2308.         if (tt) delete this.ticketStatus[tt][tick];
  2309.         return rv;
  2310.     },
  2311.     apiStatus: function(tt) {
  2312.         // Report the status of an API queue
  2313.         if (!(tt in this.ticketStatus)) return "";
  2314.         var struct = new Object();
  2315.         for (var ticket in this.ticketStatus[tt]) {
  2316.             var stat = this.ticketStatus[tt][ticket];
  2317.             if (!struct[ stat ]) struct[ stat ] = 0;
  2318.             struct[ stat ]++;
  2319.         }
  2320.         var list = new Array();
  2321.         for (var stat in struct) {
  2322.             list.push( struct[ stat ] + ' ' + stat);
  2323.         }
  2324.         return list.join(",");
  2325.     },
  2326. };
  2327.  
  2328. // This is the call that starts the ball rolling - it launches the
  2329. // init() method when the page finishes loading:
  2330. window.addEventListener('load', function (e) {CatFlickrSuite.init();}, false);
  2331.  
  2332.  
  2333. /*
  2334.  * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
  2335.  * Digest Algorithm, as defined in RFC 1321.
  2336.  * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
  2337.  * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
  2338.  * Distributed under the BSD License
  2339.  * See http://pajhome.org.uk/crypt/md5 for more info.
  2340.  */
  2341.  
  2342. /*
  2343.  * Configurable variables. You may need to tweak these to be compatible with
  2344.  * the server-side, but the defaults work in most cases.
  2345.  */
  2346. var hexcase = 0;   /* hex output format. 0 - lowercase; 1 - uppercase        */
  2347. var b64pad  = "";  /* base-64 pad character. "=" for strict RFC compliance   */
  2348.  
  2349. /*
  2350.  * These are the functions you'll usually want to call
  2351.  * They take string arguments and return either hex or base-64 encoded strings
  2352.  */
  2353. function hex_md5(s)    { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
  2354. function b64_md5(s)    { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
  2355. function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
  2356. function hex_hmac_md5(k, d)
  2357.   { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
  2358. function b64_hmac_md5(k, d)
  2359.   { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
  2360. function any_hmac_md5(k, d, e)
  2361.   { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
  2362.  
  2363. /*
  2364.  * Perform a simple self-test to see if the VM is working
  2365.  */
  2366. function md5_vm_test()
  2367. {
  2368.   return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
  2369. }
  2370.  
  2371. /*
  2372.  * Calculate the MD5 of a raw string
  2373.  */
  2374. function rstr_md5(s)
  2375. {
  2376.   return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
  2377. }
  2378.  
  2379. /*
  2380.  * Calculate the HMAC-MD5, of a key and some data (raw strings)
  2381.  */
  2382. function rstr_hmac_md5(key, data)
  2383. {
  2384.   var bkey = rstr2binl(key);
  2385.   if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
  2386.  
  2387.   var ipad = Array(16), opad = Array(16);
  2388.   for(var i = 0; i < 16; i++)
  2389.   {
  2390.     ipad[i] = bkey[i] ^ 0x36363636;
  2391.     opad[i] = bkey[i] ^ 0x5C5C5C5C;
  2392.   }
  2393.  
  2394.   var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
  2395.   return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
  2396. }
  2397.  
  2398. /*
  2399.  * Convert a raw string to a hex string
  2400.  */
  2401. function rstr2hex(input)
  2402. {
  2403.   try { hexcase } catch(e) { hexcase=0; }
  2404.   var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  2405.   var output = "";
  2406.   var x;
  2407.   for(var i = 0; i < input.length; i++)
  2408.   {
  2409.     x = input.charCodeAt(i);
  2410.     output += hex_tab.charAt((x >>> 4) & 0x0F)
  2411.            +  hex_tab.charAt( x        & 0x0F);
  2412.   }
  2413.   return output;
  2414. }
  2415.  
  2416. /*
  2417.  * Convert a raw string to a base-64 string
  2418.  */
  2419. function rstr2b64(input)
  2420. {
  2421.   try { b64pad } catch(e) { b64pad=''; }
  2422.   var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  2423.   var output = "";
  2424.   var len = input.length;
  2425.   for(var i = 0; i < len; i += 3)
  2426.   {
  2427.     var triplet = (input.charCodeAt(i) << 16)
  2428.                 | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
  2429.                 | (i + 2 < len ? input.charCodeAt(i+2)      : 0);
  2430.     for(var j = 0; j < 4; j++)
  2431.     {
  2432.       if(i * 8 + j * 6 > input.length * 8) output += b64pad;
  2433.       else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
  2434.     }
  2435.   }
  2436.   return output;
  2437. }
  2438.  
  2439. /*
  2440.  * Convert a raw string to an arbitrary string encoding
  2441.  */
  2442. function rstr2any(input, encoding)
  2443. {
  2444.   var divisor = encoding.length;
  2445.   var i, j, q, x, quotient;
  2446.  
  2447.   /* Convert to an array of 16-bit big-endian values, forming the dividend */
  2448.   var dividend = Array(Math.ceil(input.length / 2));
  2449.   for(i = 0; i < dividend.length; i++)
  2450.   {
  2451.     dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
  2452.   }
  2453.  
  2454.   /*
  2455.    * Repeatedly perform a long division. The binary array forms the dividend,
  2456.    * the length of the encoding is the divisor. Once computed, the quotient
  2457.    * forms the dividend for the next step. All remainders are stored for later
  2458.    * use.
  2459.    */
  2460.   var full_length = Math.ceil(input.length * 8 /
  2461.                                     (Math.log(encoding.length) / Math.log(2)));
  2462.   var remainders = Array(full_length);
  2463.   for(j = 0; j < full_length; j++)
  2464.   {
  2465.     quotient = Array();
  2466.     x = 0;
  2467.     for(i = 0; i < dividend.length; i++)
  2468.     {
  2469.       x = (x << 16) + dividend[i];
  2470.       q = Math.floor(x / divisor);
  2471.       x -= q * divisor;
  2472.       if(quotient.length > 0 || q > 0)
  2473.         quotient[quotient.length] = q;
  2474.     }
  2475.     remainders[j] = x;
  2476.     dividend = quotient;
  2477.   }
  2478.  
  2479.   /* Convert the remainders to the output string */
  2480.   var output = "";
  2481.   for(i = remainders.length - 1; i >= 0; i--)
  2482.     output += encoding.charAt(remainders[i]);
  2483.  
  2484.   return output;
  2485. }
  2486.  
  2487. /*
  2488.  * Encode a string as utf-8.
  2489.  * For efficiency, this assumes the input is valid utf-16.
  2490.  */
  2491. function str2rstr_utf8(input)
  2492. {
  2493.   var output = "";
  2494.   var i = -1;
  2495.   var x, y;
  2496.  
  2497.   while(++i < input.length)
  2498.   {
  2499.     /* Decode utf-16 surrogate pairs */
  2500.     x = input.charCodeAt(i);
  2501.     y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
  2502.     if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
  2503.     {
  2504.       x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
  2505.       i++;
  2506.     }
  2507.  
  2508.     /* Encode output as utf-8 */
  2509.     if(x <= 0x7F)
  2510.       output += String.fromCharCode(x);
  2511.     else if(x <= 0x7FF)
  2512.       output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
  2513.                                     0x80 | ( x         & 0x3F));
  2514.     else if(x <= 0xFFFF)
  2515.       output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
  2516.                                     0x80 | ((x >>> 6 ) & 0x3F),
  2517.                                     0x80 | ( x         & 0x3F));
  2518.     else if(x <= 0x1FFFFF)
  2519.       output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
  2520.                                     0x80 | ((x >>> 12) & 0x3F),
  2521.                                     0x80 | ((x >>> 6 ) & 0x3F),
  2522.                                     0x80 | ( x         & 0x3F));
  2523.   }
  2524.   return output;
  2525. }
  2526.  
  2527. /*
  2528.  * Encode a string as utf-16
  2529.  */
  2530. function str2rstr_utf16le(input)
  2531. {
  2532.   var output = "";
  2533.   for(var i = 0; i < input.length; i++)
  2534.     output += String.fromCharCode( input.charCodeAt(i)        & 0xFF,
  2535.                                   (input.charCodeAt(i) >>> 8) & 0xFF);
  2536.   return output;
  2537. }
  2538.  
  2539. function str2rstr_utf16be(input)
  2540. {
  2541.   var output = "";
  2542.   for(var i = 0; i < input.length; i++)
  2543.     output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
  2544.                                    input.charCodeAt(i)        & 0xFF);
  2545.   return output;
  2546. }
  2547.  
  2548. /*
  2549.  * Convert a raw string to an array of little-endian words
  2550.  * Characters >255 have their high-byte silently ignored.
  2551.  */
  2552. function rstr2binl(input)
  2553. {
  2554.   var output = Array(input.length >> 2);
  2555.   for(var i = 0; i < output.length; i++)
  2556.     output[i] = 0;
  2557.   for(var i = 0; i < input.length * 8; i += 8)
  2558.     output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
  2559.   return output;
  2560. }
  2561.  
  2562. /*
  2563.  * Convert an array of little-endian words to a string
  2564.  */
  2565. function binl2rstr(input)
  2566. {
  2567.   var output = "";
  2568.   for(var i = 0; i < input.length * 32; i += 8)
  2569.     output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
  2570.   return output;
  2571. }
  2572.  
  2573. /*
  2574.  * Calculate the MD5 of an array of little-endian words, and a bit length.
  2575.  */
  2576. function binl_md5(x, len)
  2577. {
  2578.   /* append padding */
  2579.   x[len >> 5] |= 0x80 << ((len) % 32);
  2580.   x[(((len + 64) >>> 9) << 4) + 14] = len;
  2581.  
  2582.   var a =  1732584193;
  2583.   var b = -271733879;
  2584.   var c = -1732584194;
  2585.   var d =  271733878;
  2586.  
  2587.   for(var i = 0; i < x.length; i += 16)
  2588.   {
  2589.     var olda = a;
  2590.     var oldb = b;
  2591.     var oldc = c;
  2592.     var oldd = d;
  2593.  
  2594.     a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
  2595.     d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
  2596.     c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
  2597.     b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
  2598.     a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
  2599.     d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
  2600.     c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
  2601.     b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
  2602.     a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
  2603.     d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
  2604.     c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
  2605.     b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
  2606.     a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
  2607.     d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
  2608.     c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
  2609.     b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);
  2610.  
  2611.     a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
  2612.     d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
  2613.     c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
  2614.     b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
  2615.     a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
  2616.     d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
  2617.     c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
  2618.     b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
  2619.     a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
  2620.     d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
  2621.     c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
  2622.     b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
  2623.     a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
  2624.     d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
  2625.     c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
  2626.     b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
  2627.  
  2628.     a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
  2629.     d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
  2630.     c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
  2631.     b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
  2632.     a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
  2633.     d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
  2634.     c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
  2635.     b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
  2636.     a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
  2637.     d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
  2638.     c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
  2639.     b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
  2640.     a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
  2641.     d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
  2642.     c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
  2643.     b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
  2644.  
  2645.     a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
  2646.     d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
  2647.     c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
  2648.     b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
  2649.     a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
  2650.     d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
  2651.     c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
  2652.     b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
  2653.     a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
  2654.     d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
  2655.     c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
  2656.     b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
  2657.     a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
  2658.     d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
  2659.     c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
  2660.     b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
  2661.  
  2662.     a = safe_add(a, olda);
  2663.     b = safe_add(b, oldb);
  2664.     c = safe_add(c, oldc);
  2665.     d = safe_add(d, oldd);
  2666.   }
  2667.   return Array(a, b, c, d);
  2668. }
  2669.  
  2670. /*
  2671.  * These functions implement the four basic operations the algorithm uses.
  2672.  */
  2673. function md5_cmn(q, a, b, x, s, t)
  2674. {
  2675.   return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
  2676. }
  2677. function md5_ff(a, b, c, d, x, s, t)
  2678. {
  2679.   return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
  2680. }
  2681. function md5_gg(a, b, c, d, x, s, t)
  2682. {
  2683.   return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
  2684. }
  2685. function md5_hh(a, b, c, d, x, s, t)
  2686. {
  2687.   return md5_cmn(b ^ c ^ d, a, b, x, s, t);
  2688. }
  2689. function md5_ii(a, b, c, d, x, s, t)
  2690. {
  2691.   return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
  2692. }
  2693.  
  2694. /*
  2695.  * Add integers, wrapping at 2^32. This uses 16-bit operations internally
  2696.  * to work around bugs in some JS interpreters.
  2697.  */
  2698. function safe_add(x, y)
  2699. {
  2700.   var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  2701.   var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  2702.   return (msw << 16) | (lsw & 0xFFFF);
  2703. }
  2704.  
  2705. /*
  2706.  * Bitwise rotate a 32-bit number to the left.
  2707.  */
  2708. function bit_rol(num, cnt)
  2709. {
  2710.   return (num << cnt) | (num >>> (32 - cnt));
  2711. }
  2712.  
Comments
  • decembre
    31 days
    # text 0.73 KB | 0 0
    1. Better to use with my userstle:
    2. Flickr WideScreen - BigONE
    3. https://userstyles.world/style/248/flickr-widescreen-bigone-v-320-usw
    4.  
    5. This userscript add a Tweak 2025.03 to make Flickr Functional Suite work in Photo page:
    6. I don't know why, but in photo page clicking on the username (near the Buddy icon), don't open the FFS popup.
    7. But with the GM "View in FlickRiver", it is possible to open it.
    8. So, I add this FlickRiver button in FFS and rename it as "F. Functional S."
    9.  
    10. Now if you click on it it open the FFS popup.
    11. It Receive too the Color Tags of FFS (When you Hover it, you see them)
    12.  
    13. I Publish too another version of GM "View in FlickrRiver" to work with this FFS:
    14. In Photo page, It add the FlicRiver button near the new FF button
Add Comment
Please, Sign In to add comment