Flickr Fav Set

By kuy Last update Feb 20, 2009 — Installed 372 times. Daily Installs: 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 3, 0, 1, 1, 0, 1, 0, 2, 3, 0, 3, 0, 1, 0, 1, 0, 1

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           Flickr Fav Set
// @namespace      http://endflow.net/
// @description    Provides 'Set' feature for your favorites on Flickr.
// @include        http://www.flickr.com/photos/*/favorites/*
// @include        http://flickr.com/photos/*/favorites/*
// @resource       YUI_EVT_JS http://yui.yahooapis.com/2.6.0/build/event/event-min.js
// @resource       YUI_DD_JS http://yui.yahooapis.com/2.6.0/build/dragdrop/dragdrop-min.js
// @resource       YUI_SLDR_JS http://yui.yahooapis.com/2.6.0/build/slider/slider-min.js
// @resource       YUI_SLDR_CSS http://yui.yahooapis.com/2.6.0/build/slider/assets/skins/sam/slider.css
// ==/UserScript==
// @author         Yuki KODAMA (twitter:kuy, skype:netkuy)
// @version        0.1.5
// @history {{{1
//  [2008-09-26] 0.1.0 first version
//  [2008-09-27] 0.1.1 supports AutoPagerize
//  [2008-09-28] 0.1.2 some bugfix and improvements
//  [2008-09-29] 0.1.3 supports Multilanguage (en & ja)
//  [2008-09-29] 0.1.4 add confing
//  [2008-09-29] 0.1.5 add default language
// @todo {{{1
//  *System
//   -[IMP] provide 2 import modes: 'override' and 'merge'
//   -[UTL] utility script to delete crashed favset data
//   -[FTR] (share favset on Wedata)
//  *FavSet Selector
//   -[BUG] crash FavSetSelector when it include long name favset. that is not fixed width.
//   -[IMP] show photo only included in favset if click favset again
//   -[IMP] optimize: updateMarker/scrollTo: store selected SPAN obj as member variable
//   -[NEW] arrange order of favset with dnd
//   -[BUG] (fix slider bug)
//   -[FTR] (multiple selection)
//  *FavSet Info
//   -[NEW] add username list under FavSetInfo (like 'Tag Cloud')
//   -[NEW] add 'Tag Cloud' under FavSetInfo
//  *Photo Selector
//   -[IMP] floating PhotoSelector same as FavSetInfo
//   -[IMP] optimize: PhotoSelector.updateCounter()
//   -[NEW] advanced search (tag or full-text, sort, date, not in favset)
//   -[FTR] (support 'tag autocomplete' in search box)
//  *Photo Deck
//   -[IMP] optimize: PhotoDeck.loadThumbnail: applyOverlay/updateCounter make it slowly
//   -[NEW] put 'tick marks' to the left side of PhotoDeck for counting num of photos
//   -[NEW] arrange order of photo with dnd
//  *Others
//   -[IMP] cleanup CSS: divide in some of section using folding marker.
//   -[NEW] provide 'Wide mode' for 1280x1024 or more large display
// }}}1

(function(){
if(document.title !== 'Your favorites on Flickr') return;

//// Config {{{1
var cfg = {
    lang:'en',      // UI language: 'en', 'ja'
    hide:true,      // hide default search box
    follow:true,    // FavSet Info panel follow page scroll
    num:{           // display num of photos like "FavSet#1 [18]"
        sel:false,  // in FavSet Selector
        dd:true     // in dropdown-list of Photo Selector
    }
}

//// I18N {{{1
var I18N = {
list:['en', 'ja'],
data:{
    // Common UI
    UI_SAVE:['SAVE', '保存'],
    UI_CANCEL:['CANCEL', 'キャンセル'],
    UI_NEW:['NEW', '作成'],
    UI_DELETE:['DELETE', '削除'],
    UI_EDIT:['EDIT', '編集'],
    UI_CLOSE:['CLOSE', '閉じる'],
    UI_OR:['OR', 'または'],
    UI_UNTITLED:['Untitled', '無題'],
    UI_GO:['GO', '検索'],
    UI_CLEAR:['CLEAR', 'クリア'],
    UI_IMPORT:['IMPORT', 'インポート'],
    UI_EXPORT:['EXPORT', 'エクスポート'],
    // Toolbar
    TB_DEL_CONFIRM:['Are you sure you want to delete?', '本当に削除してもよろしいですか?'],
    TB_DATA:['data', 'データ'],
    // FavSet Info
    FI_PHOTO:['%1 photos', '%1枚の写真'],
    FI_VIEW:['%1 views', '%1回の閲覧'],
    FI_PUBLIC:['Public', '公開'],
    FI_EDIT:['edit', '編集'],
    FI_UPDATE:['Updated on %1', '%1に更新'],
    FI_CREATE:['Created on %1', '%1に作成'],
    // Photo Selector
    PS_ALL:['All', '全て'],
    PS_NOT_IN_FS:['Not in a FavSet', 'FavSetに含まれない'],
    PS_YOUR_FS:['Your FavSets', 'あなたのFavSet'],
    PS_CNT_FAV:['<b>%1</b> favs', '全部で<b>%1</b>個のお気に入り'],
    PS_CNT_LOAD:['<b>%1</b> loaded', '<b>%1</b>個読み込み済'],
    PS_CNT_SHOW:['<b>%1</b> items', '<b>%1</b>個を表示'],
    PS_CNT_SEL:['<b>%1</b> selected', '<b>%1</b>個を選択'],
    PS_SEL_ALL:['Select all', '全て選択'],
    PS_CLR_ALL:['Clear selection', '選択を解除']
},
init:function(){
    var i = this.list.indexOf(cfg.lang);
    this.dict = {};
    for(var k in this.data) this.dict[k] = this.data[k][i] || this.data[k][0];
},
$:function(id){
    var str = this.dict[id];
    if(1 < arguments.length){
        var args = Array.prototype.slice.call(arguments);
        args.shift();
        var len = args.length;
        for(var i = 0, k = 1; i < len; i++, k++){
            str = str.replace('%' + k, args[i]);
        }
    }
    return str;
}
}

//// Styles {{{1
GM_addStyle(<><![CDATA[
#Main.view #ffs_new, #Main.view #ffs_delete,
#Main.view #ffs_import, #Main.view #ffs_export { visibility: hidden; }
#Main.view #ffs_desc_edit, #Main.edit #ffs_desc_view,
#ffs_info.rename #ffs_info_title, #Main.view span.pc_s > div,
#ffs_msg_cont.ffs_confirm #ffs_msg_text, #ffs_msg_cont.ffs_message #ffs_msg_text,
#ffs_msg_cont.ffs_message #ffs_msg_ok, #ffs_msg_cont.ffs_message #ffs_msg_cancal,
#ffs_fs > #ffs_fs_mark, #favoriteThumbs > span.ffs_hidden,
#Main.view #ffs_ps_sel_cont { display: none; }
#Main.edit #ffs_new, #Main.edit #ffs_delete, #Main.edit #ffs_desc_edit,
#Main.view #ffs_desc_edit, #Main.edit #ffs_import, #Main.edit #ffs_export,
#Main.edit #ffs_ps_sel_cont { display: inline; }
#ffs_ui #ffs_status.ffs_mode_info .ffs_status_info,
#ffs_ui #ffs_status.ffs_mode_confirm .ffs_status_confirm,
#ffs_ui #ffs_status.ffs_mode_error .ffs_status_error,
#Main.edit span.pc_s > div, #ffs_info.rename #ffs_info_form,
#ffs_fs.selected > #ffs_fs_mark, #favoriteThumbs > span { display: block; }
#ffs_toolbar > p { position: relative; margin-bottom: 0; }
#ffs_import { margin-left: 20px; }
.ffs_separator {
    display: inline;
    height: 16px;
    width: 2px;
    background-color: #666666;
    margin: 0 6px;
}
#ffs_switch { position: absolute; right: 11px; top: 10px; }
#Main.view #ffs_switch {
    color: white;
    background-color: #0063DC;  /* flickr blue */
}
#ffs_fs { position: relative; overflow: hidden; margin-bottom: 6px; }
#ffs_fs_cont {
    position: absolute;
    top: 0;
    margin-top: 10px;
    width: 10700px;
}
#ffs_fs_mark { position: absolute; left: 7px; top: 2px; }
#ffs_fs_mark > img { display: block; }
#ffs_fs_cont > span { text-align: center; float: left; margin: 0 16px; }
#ffs_fs_cont > span > img { display: block; margin: 0 auto; cursor: pointer; }
#ffs_fs_cont > span > p {
    margin-top: -68px;
    padding: 0;
    color: #333;
    cursor: pointer;
}
#ffs_fs_slider {
    background-image:url('http://l.yimg.com/g/images/slider_bg.gif');
    background-position:0 4px;
    background-repeat:repeat-x;
    margin:0 6px;
    position:absolute;
    top:114px;
    width:788px;
}
.overlay {
    width: 800px;
    height: 130px;
    position: absolute;
    top: 0px;
    display: none;
}
#ffs_overlay { opacity: 0.75; background-color: white; }
#ffs_msg { text-align: center; padding-top: 40px; }
#ffs_msg_body { padding: 0 14px; }
#ffs_info { width: 280px; float: left; }
#ffs_info_cont { width: 280px; }
#ffs_cover {
    padding: 20px;
    text-align: center;
    background-color: #F3F3F3; /* flickr ui gray */
}
#ffs_cover > div { margin: -10px 0 10px; }
#ffs_cover > p { margin: 0; }
#ffs_cover > p > span.pc_m, #ffs_cover.loading > p > span.pc_m.photo_container { display: none; }
#ffs_cover > p > span.pc_m.photo_container, #ffs_cover.loading > p > span.pc_m { display: block; }
#ffs_cover > p > span > div { width: 240px; height: 180px; }
#ffs_cover > p > span > div > img { margin-top: 82px; }
#Main.edit #ffs_info_title:hover { background-color: #FFFFD3; }
#ffs_info_form {
    text-align: left;
    margin: 0 20px;
    padding: 2px 0;
    display: none;
    font-size: 12px;
}
#ffs_info_text {
    background-color: #FFFFD3;
    border-width: 1px;
    border-color: #E9E9AE;
    font-size: 14px;
    font-weight: bold;
    padding: 3px;
    margin-bottom: 5px;
    width: 240px;
}
#ffs_info_d {
    background-color: white;
    color: #999999;
    font-size: 11px;
    margin: 15px;
    padding: 5px;
}
#ffs_info_d > span { display: block; margin: 3px 0; }
#ffs_ps {
    position: relative;
    background-color: #F3F3F3;
    padding: 10px;
    margin: 0 0 3px 317px;
}
#ffs_ps > div { font-size: 11px; color: black; margin: 5px 0 -4px -2px; }
#ffs_ps > div > *, #ffs_ps > div > * > * { margin: 0 2px; }
.ffs_ps_button { cursor: pointer; color: #1057AE; text-decoration: underline; }
.ffs_ps_button:hover { color: white; }
#ffs_ps > #ffs_ps_msg {
    position: absolute;
    width: 483px;
    margin: 0;
    top: 70px;
    left: 0;
    text-align: center;
    display: none;
}
#ffs_ps_msg > span { color: #CCCCCC; font-size: 32px; }
#ffs_ps_msg.result > #ffs_ps_result,
#ffs_ps_msg.search > #ffs_ps_search { display: block; }
#ffs_ps_msg.search > #ffs_ps_result,
#ffs_ps_msg.result > #ffs_ps_search { display: none; }
#favoriteThumbs { margin-left: 317px; }
#Main.edit #ffs_switch, #ffs_info_cancel,
#ffs_ui #ffs_status.ffs_mode_info, #ffs_ps_clear,
#ffs_msg_cancal {
    color: #666666;                     /* dark gray */
    background-color: #DDDDDD;          /* light gray */
}
#ffs_ui #ffs_status { margin: 10px 0 0; padding: 4px 8px; text-align: center; }
#ffs_ui #ffs_status.ffs_mode_info span { margin: 0 8px; }
#ffs_ui #ffs_status.ffs_mode_confirm {
    color: #005A17;                     /* dark green */
    background-color: rgb(203,255,216); /* light green */
    font-weight: bold;
    font-size: 14px;
}
#ffs_ui #ffs_status.ffs_mode_error {
    color: #98002E;                     /* dark red */
    background-color: rgb(255,219,238); /* light red */
    font-weight: bold;
    font-size: 14px;
}
.pc_s > div { position: absolute; top: 0; width: 75px; height: 75px; }
.pc_s > div.ffs_selected {
    border: 3px solid rgb(255,0,132);   /* flickr red */
    width: 69px;
    height: 69px;
}
/* rgb(255,0,132); // red // rgb(0,99,220);  // blue // rgb(0,255,64);  // green */
]]></> + GM_getResourceText('YUI_SLDR_CSS'));

//// Global Variables {{{1
var w = this.unsafeWindow || window;
var Y = w.Y.util.Dom;
var main = $('Main');
var fav = $('favoriteThumbs');
var head = $X('//head');
head.appendChild($n('script', {innerHTML:
    GM_getResourceText('YUI_EVT_JS')+
    GM_getResourceText('YUI_DD_JS')+
    GM_getResourceText('YUI_SLDR_JS')
}));

//// Storage {{{1
var Storage = {
def_s:'http://farm4.static.flickr.com/3292/2911868500_1f7704b84f_s.jpg',
def_m:'http://farm4.static.flickr.com/3292/2911868500_1f7704b84f_m.jpg',
init:function(){
    if(!this.setRawData(GM_getValue('data'))) this.clear();
},
valid:function(){
    if(!isO(this.data) || !$h(this.data, ['favsets'])
        || !isA(this.data.favsets)) return false;
    this.data.favsets.forEach(function(favset){
        if(!$h(favset, ['name', 'create', 'update', 'photo', 'cover'])) return false;
        if(!isA(favset.photo) || !isO(favset.cover)) return false;
        if(!$h(favset.cover, ['src_s', 'src_m'])) return false;
    });
    return true;
},
save:function(){
    //! to avoid "Error: Greasemonkey access violation: unsafeWindow cannot call GM_setValue.",
    //! it has to use "setTimeout" function in this function call.
    setTimeout($b(this, function(){
        GM_setValue('data', this.data.toSource());
    }), 0);
},
clear:function(){
    this.data = {favsets:[]};
    this.index = {};
    this.save();
},
updateIndex:function(){
    this.index = {};
    this.data.favsets.forEach(function(favset, i){
        this.index[favset.name] = i;
    }, this);
},
getRawData:function(){ return GM_getValue('data') || ''; },
setRawData:function(data){
    if(!data || data == '') return false;
    try{
        this.data = eval(data);
    }catch(e){
        return false;
    }
    if(!this.valid()) return false;
    this.updateIndex();
    this.save();
    return true;
},
getFavSet:function(name){
    if(!this.existFavSet(name)) return null;
    return this.data.favsets[this.index[name]];
},
addFavSet:function(name, favset){
    if(this.existFavSet(name)) return false;
    var now = (new Date()).getTime().toString();
    favset = favset || {name:name, create:now, update:now, photo:[], cover: {src_s:this.def_s, src_m:this.def_m}};
    this.data.favsets.push(favset);
    this.updateIndex();
    this.save();
    Event.fire('onAddFavSet', name);
    return true;
},
removeFavSet:function(name){
    if(!this.existFavSet(name)) return false;
    var pos = this.index[name];
    this.data.favsets = this.data.favsets.slice(0, pos)
        .concat(this.data.favsets.slice(pos + 1));
    this.updateIndex();
    this.save();
    Event.fire('onRemoveFavSet', name);
    return true;
},
renameFavSet:function(oldName, newName){
    if(!this.existFavSet(oldName)) return false;
    if(this.existFavSet(newName)) return false;
    var set = this.getFavSet(oldName);
    set.name = newName;
    set.update = (new Date()).getTime().toString();
    this.updateIndex();
    this.save();
    Event.fire('onRenameFavSet', oldName, newName);
    Event.fire('onChangeFavSet', newName);
    return true;
},
existFavSet:function(name){ return $h(this.index, [name]); },
getNumFavSet:function(){ return this.data.favsets.length; },
getAllSetName:function(){ return this.data.favsets.map(function(fs){return fs.name}); },
addPhoto:function(name, urls, silent){
    var set = this.getFavSet(name);
    var num = set.photo.length;
    urls = isA(urls) ? urls : [urls];
    urls.forEach(function(url){
        url = url.replace('http://www.flickr.com', '');
        if(set.photo.indexOf(url) == -1){
            set.photo.push(url);
            set.update = (new Date()).getTime().toString();
        }
    }, this);
    if(num != set.photo.length && silent !== true){
        this.save();
        Event.fire('onChangeFavSet', name);
    }
},
removePhoto:function(name, urls){
    var set = this.getFavSet(name);
    var num = set.photo.length;
    urls = isA(urls) ? urls : [urls];
    urls.forEach(function(url){
        url = url.replace('http://www.flickr.com', '');
        var pos = set.photo.indexOf(url);
        if(pos != -1){
            set.photo = set.photo.slice(0, pos).concat(set.photo.slice(pos + 1));
            set.update = (new Date()).getTime().toString();
        }
    }, this);
    if(num != set.photo.length){
        this.save();
        Event.fire('onChangeFavSet', name);
    }
},
getAllPhoto:function(){
    var all = [];
    this.data.favsets.forEach(function(favset){
        all = all.concat(favset.photo.filter(function(url){
            return all.indexOf(url) == -1
        }));
    });
    return all;
},
setCoverPhoto:function(name, url){
    url = url.replace('http://www.flickr.com', '');
    var a = $X('id("favoriteThumbs")//a[contains(@href,"' + url + '")]');
    var set = this.getFavSet(name);
    set.cover.href = a.href;
    set.cover.src_s = a.firstChild.src;
    set.cover.src_m = a.firstChild.src.replace('_s.', '_m.');
    set.cover.alt = a.firstChild.alt;
    set.update = (new Date()).getTime().toString();
    this.addPhoto(name, url, true);
    this.save();
    Event.fire('onChangeFavSet', name);
}
};

//// Event {{{1
var Event = {
init:function(){
    head.appendChild($n('script', {innerHTML:<><![CDATA[
        if(!FFS) var FFS = {};
        if(!FFS.Event) FFS.Event = {};
        FFS.Event.onAddFavSet = new Y.util.CustomEvent('onAddFavSet');
        FFS.Event.onRemoveFavSet = new Y.util.CustomEvent('onRemoveFavSet');
        FFS.Event.onSelectFavSet = new Y.util.CustomEvent('onSelectFavSet');
        FFS.Event.onChangeFavSet = new Y.util.CustomEvent('onChangeFavSet');
        FFS.Event.onSelectPhoto = new Y.util.CustomEvent('onSelectPhoto');
        FFS.Event.onRenameFavSet = new Y.util.CustomEvent('onRenameFavSet');
        FFS.Event.onDropCoverPhoto =  new Y.util.CustomEvent('onDropCoverPhoto');
    ]]></>}));

    this.onAddFavSet = w.FFS.Event.onAddFavSet;
    this.onRemoveFavSet = w.FFS.Event.onRemoveFavSet;
    this.onSelectFavSet = w.FFS.Event.onSelectFavSet;
    this.onChangeFavSet = w.FFS.Event.onChangeFavSet;
    this.onSelectPhoto = w.FFS.Event.onSelectPhoto;
    this.onRenameFavSet = w.FFS.Event.onRenameFavSet;
    this.onDropCoverPhoto = w.FFS.Event.onDropCoverPhoto;
},
subs:function(type, callback){ this[type].subscribe(callback); },
fire:function(){
    var args = Array.prototype.slice.call(arguments);
    var type = args.shift();
    this[type].fire.apply(this[type], args);
}
};

//// Toolbar {{{1
var Toolbar = {
// init {{{2
init:function(){
    main.insertBefore($n('div', {id:'ffs_toolbar'}, [
      $n('p', {className:'Focus'}, [
        $n('b', {id:'ffs_desc_view', innerHTML:'Flickr Fav Set'}),
        $n('b', {id:'ffs_desc_edit', innerHTML:''}),
        $n('input', {id:'ffs_new', className:'Butt', type:'button', value:I18N.$('UI_NEW')}), $t(' '),
        $n('input', {id:'ffs_delete', className:'Butt', type:'button', value:I18N.$('UI_DELETE')}), $t(' '),
        $n('input', {id:'ffs_import', className:'Butt', type:'button', value:I18N.$('UI_IMPORT')}), $t(' '),
        $n('input', {id:'ffs_export', className:'Butt', type:'button', value:I18N.$('UI_EXPORT')}), $t(' '),
        $n('input', {id:'ffs_switch', className:'Butt', type:'button', value:I18N.$('UI_EDIT')})
    ])]), fav);
    Y.addClass(main, 'view');

    if(cfg.hide) {
        Y.setStyle($X('id("Main")/form'), 'display', 'none');
        GM_addStyle('#SubNav{margin-bottom:0;margin-top:10px;}');
    }

    this.toolbar = $('ffs_toolbar');
    this.btnSwitch = $('ffs_switch');
    this.btnNew = $('ffs_new');
    this.btnDelete = $('ffs_delete');
    this.btnImport = $('ffs_import');
    this.btnExport = $('ffs_export');

    this.onedit = $b(this, this.onedit);
    this.onnew = $b(this, this.onnew);
    this.ondelete = $b(this, this.ondelete);
    this.onimport = $b(this, this.onimport);
    this.onexport = $b(this, this.onexport);

    this.btnSwitch.addEventListener('click', this.onedit, false);
    this.btnNew.addEventListener('click', this.onnew, false);
    this.btnDelete.addEventListener('click', this.ondelete, false);
    this.btnImport.addEventListener('click', this.onimport, false);
    this.btnExport.addEventListener('click', this.onexport, false);
},
// DOM Operation {{{2
enableToolbar:function(bool){
    if(bool){
        Y.addClass(main, 'edit');
        Y.removeClass(main, 'view');
        this.btnSwitch.value = I18N.$('UI_CLOSE');
        PhotoDeck.updateDeck();
    }else{
        Y.removeClass(main, 'edit');
        Y.addClass(main, 'view');
        this.btnSwitch.value = I18N.$('UI_EDIT');
        PhotoSelector.updateCounter();
    }
    FavSetSelector.updateMessage();
},
isView:function(){return Y.hasClass(main, 'view')},
isEdit:function(){return Y.hasClass(main, 'edit')},
// Event Handler {{{2
onedit:function(){ this.enableToolbar(Y.hasClass(main, 'view')); },
onnew:function(){
    Storage.addFavSet(I18N.$('UI_UNTITLED'));
    FavSetSelector.fireSelectFavSet(I18N.$('UI_UNTITLED'));
    FavSetInfo.startRenameFavSet();
    FavSetSelector.scrollTo();
},
ondelete:function(){
    var name = FavSetSelector.selectedFavSet;
    if(!name || name == '') return;
    Msg.confirm(I18N.$('TB_DEL_CONFIRM'), $b(this, function(res){
        if(res) Storage.removeFavSet(name);
        if(FavSetInfo.isRenaming()) FavSetInfo.endRenameFavSet();
        return true;
    }));
},
onimport:function(){
    Msg.input(I18N.$('TB_DATA') + ' :', $b(this, function(res, value){
        if(res){
            if(!Storage.setRawData(value)) return false;
            w.location.reload();
            return true;
        }
    }), '');
},
onexport:function(){
    Msg.input(I18N.$('TB_DATA') + ' :', null, Storage.getRawData());
    return true;
}
};

//// FavSetSelector {{{1
var FavSetSelector = {
mark_src:'http://farm4.static.flickr.com/3137/2933968184_ee346cbcae_o.png',
width:800, height:130, max:751,
// init {{{2
init:function(){
    main.insertBefore($n('div', {id:'ffs_fs'}, [
      $n('canvas', {id:'ffs_fs_canvas', width:this.width, height:this.height}),
      $n('span', {id:'ffs_fs_mark'}, [
        $n('img', {src:this.mark_src}),
        $n('canvas', {width:93, height:83})
      ]),
      $n('div', {id:'ffs_fs_cont'}),
      $n('div', {id:'ffs_fs_slider'}, [
        $n('div', {id:'ffs_fs_thumb'}, [
            $n('img', {src:'http://l.yimg.com/g/images/findr_dragger_default.gif'})])])
    ]), fav);

    this.ui = $('ffs_fs');
    this.cont = $('ffs_fs_cont');
    this.mark = $('ffs_fs_mark');
    this.uiSlider = $('ffs_fs_slider');

    this.selectedFavSet = '';
    this.uiRgn = Y.getRegion(this.ui);

    this.onchangeslider = $b(this, this.onchangeslider);
    this.onaddfavset = $b(this, this.onaddfavset);
    this.onremovefavset = $b(this, this.onremovefavset);
    this.onrenamefavset = $b(this, this.onrenamefavset);
    this.onchangefavset = $b(this, this.onchangefavset);
    this.fireSelectFavSet = $b(this, this.fireSelectFavSet);

    Msg.init();

    head.appendChild($n('script', {innerHTML:$e(<><![CDATA[
        if(!FFS) var FFS = {};
        if(!FFS.UI) FFS.UI = {};
        FFS.UI.slider = new YAHOO.widget.Slider.getHorizSlider('ffs_fs_slider', 'ffs_fs_thumb', 0, #{max});
    ]]></>, {max:this.max})}));
    this.slider = w.FFS.UI.slider;
    this.slider.subscribe('change', this.onchangeslider);

    var gc = $('ffs_fs_canvas').getContext('2d');
    gc.fillStyle = 'rgb(249,249,249)';
    gc.fillRect(0, 0, this.width, this.height);
    var lgrad = gc.createLinearGradient(0, 0, 0, this.height);
    lgrad.addColorStop(0, 'rgb(249,249,249)');
    lgrad.addColorStop(0.5, 'rgb(249,249,249)');
    lgrad.addColorStop(1, 'rgb(220,220,220)');
    gc.fillStyle = lgrad;
    gc.fillRect(0, 0, this.width, this.height);

    this.drawReflection(this.mark_src, this.mark.lastChild.getContext('2d'), 93, 83);
    Storage.getAllSetName().forEach(function(name){
        this.addFavSet(name);
    }, this);
    this.updateSelector(true);

    this.cont.addEventListener('click', this.fireSelectFavSet, true);

    Event.subs('onAddFavSet', this.onaddfavset);
    Event.subs('onRemoveFavSet', this.onremovefavset);
    Event.subs('onRenameFavSet', this.onrenamefavset);
    Event.subs('onChangeFavSet', this.onchangefavset);
},
// DOM Operation {{{2
addFavSet:function(name){
    var set = Storage.getFavSet(name);
    var span = $n('span', {title:set.name}, [
        $n('img', {src:set.cover.src_s || '', alt:set.cover.alt}),
        $n('canvas', {width:'75', height:'75'}),
        $n('p', {innerHTML:this.formatFavSetName(set.name, cfg.num.sel)})
    ]);
    span.firstChild.addEventListener('click', this.fireSelectFavSet, false)
    span.lastChild.addEventListener('click', this.fireSelectFavSet, false)
    this.cont.appendChild(span);
    this.updateFavSet(name);
    this.updateSelector();
},
removeFavSet:function(name){
    if($X('//span[@title="' + name + '"]')){
        this.cont.removeChild($X.$);
        this.updateSelector();
        this.fireSelectFavSet('');
    }
},
renameFavSet:function(oldName, newName){
    if($X('//span[@title="' + oldName + '"]')){
        var span = $X.$;
        span.title = newName;
        span.lastChild.innerHTML = this.formatFavSetName(newName, cfg.num.sel);
        this.fireSelectFavSet(newName);
    }
},
updateFavSet:function(name){
    if($X('//span[@title="' + name + '"]')){
        var set = Storage.getFavSet(name);
        var span = $X.$;
        span.title = set.name;
        span.firstChild.src = set.cover.src_s;
        span.lastChild.innerHTML = this.formatFavSetName(set.name, cfg.num.sel);
        this.drawReflection(set.cover.src_s, span.firstChild.nextSibling.getContext('2d'), 75, 75);
    }
},
drawReflection:function(src, gc, width, height){
    var img = new Image();
    img.addEventListener('load', function(){
        // draw vertical-reversed image
        gc.clearRect(0, 0, width, height);
        gc.save();
        gc.translate(0, height);
        gc.scale(1, -1);
        gc.drawImage(img, 0, 0);
        gc.restore();
        // apply alpha
        gc.save();
        gc.globalCompositeOperation = 'destination-out';
        var lgrad = gc.createLinearGradient(0, 0, 0, height);
        lgrad.addColorStop(0, 'rgba(0,0,0,0.55)');
        lgrad.addColorStop(0.3, 'rgba(0,0,0,1)');
        lgrad.addColorStop(1, 'rgba(0,0,0,1)');
        gc.fillStyle = lgrad;
        gc.fillRect(0, 0, width, height);
        gc.restore();
    }, false);
    img.src = src;
},
formatFavSetName:function(name, bool){
    var set = Storage.getFavSet(name);
    return (bool && set) ? (name + ' [' + set.photo.length + ']') : name;
},
updateSelector:function(force){
    this.updateMessage();
    var cont_w = 107 * Storage.getNumFavSet();
    if(cont_w <= this.width){
        if(force || Y.getStyle(this.cont, 'left') !== '0px'){
            Y.setStyle(this.cont, 'left', '0px');
            Y.setStyle(this.uiSlider, 'visibility', 'hidden');
            this.slider.setValue(0, false, true, true);
        }
        this.updateMarker();
        return;
    }
    var rest = cont_w - this.width;
    var left = (rest / this.max) * this.slider.getValue();
    Y.setStyle(this.cont, 'left', '-' + left + 'px');
    Y.setStyle(this.uiSlider, 'visibility', '');
    this.updateMarker();
},
updateMarker:function(){
    if($X('id("ffs_fs_cont")/span[@title="' + this.selectedFavSet + '"]')){
        var left = Y.getRegion($X.$).left - this.uiRgn.left - 9; // '-9' is a offset of blur width
        Y.setStyle(this.mark, 'left', left + 'px');
    }
},
updateMessage:function(){
    if(Storage.getNumFavSet() == 0){
        setTimeout(function(){Msg.message('No FavSet')},0);
    }else{
        Msg.hide();
    }
},
scrollTo:function(name){
    name = name || this.selectedFavSet;
    if($X('id("ffs_fs_cont")/span[@title="' + name + '"]')){
        var cont_w = 107 * Storage.getNumFavSet();
        var fsRgn = Y.getRegion($X.$);
        if(cont_w <= this.width || this.uiRgn.contains(fsRgn)) return;
        var offset = 16; // margin width of SPAN elem
        var fsW = fsRgn.right - fsRgn.left;
        if(fsRgn.left < this.uiRgn.left){
            var diff = this.uiRgn.left - fsRgn.left + offset;
        }else{
            var diff = (this.uiRgn.right - fsW) - fsRgn.left - offset;
        }
        var left = parseFloat(Y.getStyle('ffs_fs_cont', 'left').replace('px', ''));
        Y.setStyle(this.cont, 'left', (left + diff) + 'px');
        this.updateSlider();
        this.updateMarker();
    }
},
updateSlider:function(){
    var cont_w = 107 * Storage.getNumFavSet();
    var rest = cont_w - this.width;
    var left = - parseInt(Y.getStyle('ffs_fs_cont', 'left').replace('px', ''));
    var value = Math.round((this.max / rest) * left);
    this.slider.setValue(value, false, true, true);
},
// Event Handler {{{2
fireSelectFavSet:function(e){
    if(isS(e)){
        this.selectedFavSet = e;
    }else{
        this.selectedFavSet = (e.target.nodeName == 'CANVAS' || e.target.nodeName == 'DIV')
                ? '' : e.target.parentNode.title;
        e.stopPropagation();
    }
    this.updateSelector();
    this.scrollTo();
    (this.selectedFavSet == '')
        ? Y.removeClass(this.ui, 'selected')
        : Y.addClass(this.ui, 'selected');
    Event.fire('onSelectFavSet', this.selectedFavSet);
},
onchangeslider:function(){ this.updateSelector(); },
onaddfavset:function(type, args){ this.addFavSet(args[0]); },
onremovefavset:function(type, args){ this.removeFavSet(args[0]); },
onrenamefavset:function(type, args){ this.renameFavSet(args[0], args[1]); },
onchangefavset:function(type, args){ this.updateFavSet(args[0]); }
//}}}2
};

//// Msg {{{1
var Msg = {
init:function(){
    this.overlay = $n('div', {id:'ffs_overlay', className:'overlay'});
    FavSetSelector.ui.appendChild(this.overlay);
    FavSetSelector.ui.appendChild($n('div', {id:'ffs_msg', className:'overlay'}, [
      $n('span', {id:'ffs_msg_cont'}, [
        $n('span', {id:'ffs_msg_body'}),
        $n('input', {id:'ffs_msg_text', type:'text', size:'40'}), $t(' '),
        $n('input', {id:'ffs_msg_ok', type:'button', className:'Butt', value:'OK'}), $t(' '),
        $n('input', {id:'ffs_msg_cancal', type:'button', className:'Butt', value:I18N.$('UI_CANCEL')})])]));

    this.uiCont = $('ffs_msg_cont');
    this.uiBody = $('ffs_msg');
    this.uiMsg = $('ffs_msg_body');
    this.uiText = $('ffs_msg_text');
    this.uiOK = $('ffs_msg_ok');
    this.uiCancel = $('ffs_msg_cancal');

    this.onclickok = $b(this, this.onclickok);
    this.onclickcancel = $b(this, this.onclickcancel);

    this.uiOK.addEventListener('click', this.onclickok, false);
    this.uiCancel.addEventListener('click', this.onclickcancel, false);
},
confirm:function(text, callback){
    if(this.isShow()) this.hide();
    this.callback = callback;
    Y.addClass(this.uiCont, 'ffs_confirm');
    this.show(text);
},
input:function(text, callback, value){
    if(this.isShow()) this.hide();
    this.callback = callback;
    Y.addClass(this.uiCont, 'ffs_input');
    this.show(text, value);
    this.uiText.focus();
    this.uiText.select();
},
message:function(text){
    if(this.isShow()) this.hide();
    Y.addClass(this.uiCont, 'ffs_message');
    this.show(text);
},
show:function(text, value){
    this.uiMsg.innerHTML = text;
    this.uiText.value = value || '';
    Y.setStyle(this.overlay, 'display', 'block');
    Y.setStyle(this.uiBody, 'display', 'block');
},
hide:function(){
    this.callback = null;
    this.uiMsg.innerHTML = '';
    this.uiText.value = '';
    this.uiCont.className = '';
    Y.setStyle(this.overlay, 'display', 'none');
    Y.setStyle(this.uiBody, 'display', 'none');
},
isShow:function(){
    return Y.hasClass(this.uiCont, 'ffs_confirm')
        || Y.hasClass(this.uiCont, 'ffs_input')
        || Y.hasClass(this.uiCont, 'ffs_message');
},
onclickok:function(){
    if(this.callback){
        if(this.callback(true, this.uiText.value)){
            this.callback = null;
            this.hide();
        }else{
            this.uiText.focus();
            this.uiText.select();
        }
    }else{
        this.hide();
    }
},
onclickcancel:function(){
    if(this.callback){
        this.callback(false, this.uiText.value);
        this.callback = null;
    }
    FavSetSelector.updateMessage();
}
};

//// FavSetInfo {{{1
var FavSetInfo = {
// init {{{2
init:function(){
    main.insertBefore($n('div', {id:'ffs_info'}, [
      $n('div', {id:'ffs_info_cont'}, [
        $n('div', {id:'ffs_cover'}, [
          $n('div', {id:'ffs_info_title', innerHTML:''}),
          $n('div', {id:'ffs_info_form'}, [
            $n('input', {id:'ffs_info_text', type:'text'}), $n('br'),
            $n('input', {id:'ffs_info_ok', type:'button', className:'Butt', value:I18N.$('UI_SAVE')}),
            $t('  '), $n('span', {innerHTML:I18N.$('UI_OR')}), $t('  '),
            $n('input', {id:'ffs_info_cancel', type:'button', className:'Butt', value:I18N.$('UI_CANCEL')}),
          ]),
          $n('p', null, [
            $n('span', {className:'photo_container pc_m'}, [
              $n('a', {id:'ffs_cover_a', href:''}, [
                $n('img', {id:'ffs_cover_img', src:Storage.def_m})])]),
            $n('span', {className:'pc_m'}, [
              $n('div', null, [
                $n('img', {src:'http://l.yimg.com/g/images/progress/balls-16x16-trans.gif'})])])])]),
        $n('div', {id:'ffs_info_d'}, [
          $n('span', null, [
            $n('span', {id:'ffs_info_num', innerHTML:I18N.$('FI_PHOTO', 0)}), $t(' | '),
            $n('span', {innerHTML:I18N.$('FI_VIEW', 0)}), $t(' | '),
            $n('span', {innerHTML:I18N.$('FI_PUBLIC') + '(' + I18N.$('FI_EDIT') + ')'})]),
          $n('span', null, [
            $n('span', {id:'ffs_info_updated', innerHTML:I18N.$('FI_UPDATE', '-')}), $t(' | '),
            $n('span', {id:'ffs_info_created', innerHTML:I18N.$('FI_CREATE', '-')})])])])]), fav);

    this.ui = $('ffs_info');
    this.cont = $('ffs_info_cont');
    this.infoTitle = $('ffs_info_title');
    this.cover = $('ffs_cover');
    this.coverAnchor = $('ffs_cover_a');
    this.coverImg = $('ffs_cover_img');
    this.infoNum = $('ffs_info_num');
    this.infoUpdated = $('ffs_info_updated');
    this.infoCreated = $('ffs_info_created');
    this.infoOK = $('ffs_info_ok');
    this.infoCancel = $('ffs_info_cancel');
    this.infoText = $('ffs_info_text');

    this.offset = 6;
    this.loading = false;
    this.it = Y.getRegion(this.cont).top;

    this.ondropcoverphoto = $b(this, this.ondropcoverphoto);
    this.oncommonfavset = $b(this, this.oncommonfavset);
    this.onremovefavset = $b(this, this.onremovefavset);
    this.onkeyup = $b(this, this.onkeyup);
    this.onscroll = $b(this, this.onscroll);
    this.onloadcoverphoto = $b(this, this.onloadcoverphoto);
    this.updateFavSetInfo = $b(this, this.updateFavSetInfo);
    this.startRenameFavSet = $b(this, this.startRenameFavSet);
    this.endRenameFavSet = $b(this, this.endRenameFavSet);
    this.doRename = $b(this, this.doRename);

    // drag & drop for changing cover photo
    head.appendChild($n('script', {innerHTML:<><![CDATA[
        if(!FFS) var FFS = {};
        if(!FFS.DD) FFS.DD = {};
        FFS.DD.target = new Y.util.DDTarget("ffs_cover");
        FFS.DD.photo = [];
    ]]></>}));
    main.appendChild($n('img', {id:'ffs_dd_proxy'}));
    w.FFS.Event.onDropCoverPhoto.subscribe(this.ondropcoverphoto);

    this.infoTitle.addEventListener('click', this.startRenameFavSet, false)
    this.infoOK.addEventListener('click', this.doRename, false)
    this.infoCancel.addEventListener('click', this.endRenameFavSet, false)
    this.infoText.addEventListener('keyup', this.onkeyup, false)
    if(cfg.follow) w.addEventListener('scroll', this.onscroll, false);
    this.coverImg.addEventListener('load', this.onloadcoverphoto, false);

    Event.subs('onSelectFavSet', this.oncommonfavset);
    Event.subs('onChangeFavSet', this.oncommonfavset);
    Event.subs('onAddFavSet', this.oncommonfavset);
    Event.subs('onRemoveFavSet', this.onremovefavset);
},
// DOM Operation {{{2
updateFavSetInfo:function(name){
    var set = Storage.getFavSet(name);
    if(!set){ this.clearFavSetInfo(); return; }

    this.infoTitle.innerHTML = set.name;
    this.infoNum.innerHTML = I18N.$('FI_PHOTO', set.photo.length);
    this.infoUpdated.innerHTML = I18N.$('FI_UPDATE', fmtD(set.update));
    this.infoCreated.innerHTML = I18N.$('FI_CREATE', fmtD(set.create));
    this.coverAnchor.href = set.cover.href;
    this.coverAnchor.title = set.cover.alt;
    this.coverImg.src = set.cover.src_m;
    this.coverImg.alt = set.cover.alt;

    this.loading = true;
    setTimeout($b(this, function(){
        if(this.loading) Y.addClass(this.cover, 'loading');
    }), 200);
},
clearFavSetInfo:function(){
    this.infoTitle.innerHTML = '';
    this.infoNum.innerHTML = I18N.$('FI_PHOTO', 0);
    this.infoUpdated.innerHTML = I18N.$('FI_UPDATE', '-');
    this.infoCreated.innerHTML = I18N.$('FI_CREATE', '-');
    this.coverAnchor.href = '#';
    this.coverAnchor.title = '';
    this.coverImg.src = Storage.def_m;
    this.coverImg.alt = '';
},
startRenameFavSet:function(){
    if(!Y.hasClass(main, 'edit')) return;
    Y.addClass(this.ui, 'rename');
    this.infoText.value = this.infoTitle.innerHTML;
    this.infoText.focus();
    this.infoText.select();
},
endRenameFavSet:function(){
    Y.removeClass(this.ui, 'rename');
    this.infoText.value = '';
    var set = Storage.getFavSet(FavSetSelector.selectedFavSet);
    if(set && set.name == I18N.$('UI_UNTITLED') && set.photo.length == 0 &&
            set.cover.src_s == Storage.def_s && set.cover.src_m == Storage.def_m){
        Storage.removeFavSet(I18N.$('UI_UNTITLED'));
    }
},
isRenaming:function(){ return Y.hasClass(this.ui, 'rename'); },
enableFloat:function(bool){
    if(arguments.callee.prev == bool) return;
    if(bool){
        var r = Y.getRegion(this.cont);
        Y.setStyle(this.ui, 'height', r.bottom - r.top);
        Y.setStyle(this.cont, 'position', 'fixed');
        Y.setStyle(this.cont, 'top', this.offset + 'px');
    }else{
        Y.setStyle(this.cont, 'position', '');
        Y.setStyle(this.cont, 'top', '');
        Y.setStyle(this.ui, 'height', '');
    }
    arguments.callee.prev = bool;
},
// Logic {{{2
doRename:function(){
    var newName = this.infoText.value;
    if(!newName || newName == '' || newName == I18N.$('UI_UNTITLED')){
        this.infoText.focus();
        this.infoText.select();
        return;
    }
    var oldName = FavSetSelector.selectedFavSet;
    if(!Storage.renameFavSet(oldName, newName)){
        this.infoText.focus();
        this.infoText.select();
        return;
    }
    this.endRenameFavSet();
    return true;
},
doChangeCoverPhoto:function(url){
    var name = FavSetSelector.selectedFavSet;
    if(!name || name == '') return;
    Storage.setCoverPhoto(name, url);
    PhotoDeck.updateDeck();
},
// Event Handler {{{2
ondropcoverphoto:function(type, args){ this.doChangeCoverPhoto(args[0]); },
oncommonfavset:function(type, args){ this.updateFavSetInfo(args[0]); },
onremovefavset:function(type, args){ this.updateFavSetInfo(); },
onscroll:function(){ this.enableFloat(this.it - this.offset < Y.getDocumentScrollTop(w.document)); },
onkeyup:function(e){
    switch(e.keyCode){
        case e.DOM_VK_RETURN: this.doRename(); break;
        case e.DOM_VK_ESCAPE: this.endRenameFavSet(); break;
    }
},
onloadcoverphoto:function(e){
    if(this.loading) this.loading = false;
    if(Y.hasClass(this.cover, 'loading')) Y.removeClass(this.cover, 'loading');
}
};

//// PhotoDeck {{{1
var PhotoDeck = {
// init {{{2
init:function(){
    this.onfetch = $b(this, this.onfetch);
    this.onselectphoto = $b(this, this.onselectphoto);
    this.onselectfavset = $b(this, this.onselectfavset);
    this.onloadphotoinfo = $b(this, this.onloadphotoinfo);
    this.fireSelectPhoto = $b(this, this.fireSelectPhoto);

    // AutoPagerize
    $b(this, function(callback, count){
        count = count || 4;
        if(window.AutoPagerize && window.AutoPagerize.addFilter){
            window.AutoPagerize.addFilter(callback);
        }else if (0 < count) {
            setTimeout(arguments.callee, 1000, callback, count - 1);
        }
    })(this.onfetch);

    Event.subs('onSelectPhoto', this.onselectphoto);
    Event.subs('onSelectFavSet', this.onselectfavset);

    this.cleanup();
    this.applyOverlay();
},
// DOM Operation {{{2
cleanup:function(){
    this.clearfix = $X('id("favoriteThumbs")/div[@class="clearfix"]');
    this.clearfix.id = 'clearfix';
    $x('id("Main")/div/p[not(@class)]').forEach(function(e){e.parentNode.removeChild(e)});
    $x('//a[@class="autopagerize_link"]/..').forEach(function(e){e.parentNode.removeChild(e)});
    var hrs = document.getElementsByTagName('hr');
    for(var i = 0; i < hrs.length; i++) hrs[i].parentNode.removeChild(hrs[i]);
},
applyOverlay:function(){
    this.ddc = this.ddc || 0;
    var script = '';
    $x('//span[contains(concat(" ",@class," ")," pc_s ") and not(child::div)]').forEach(function(span, i){
        var anchor = span.firstChild;
        var overlay = $n('div', {id:'ffs_photo_' + this.ddc, title:anchor.title});
        overlay.addEventListener('click', this.fireSelectPhoto, false);
        span.appendChild(overlay);
        script += $e(<><![CDATA[
            FFS.DD.photo[#{this.ddc}] = new Y.util.DDProxy('ffs_photo_#{this.ddc}', 'default', {dragElId:'ffs_dd_proxy'});
            FFS.DD.photo[#{this.ddc}].startDrag = function(){
                Y.util.Dom.setStyle(this.getDragEl(), 'opacity', 0.5);
                this.startPos = Y.util.Dom.getXY(this.getEl());
                Y.util.Dom.get('ffs_dd_proxy').src = this.getEl().parentNode.firstChild.firstChild.src;
            };
            FFS.DD.photo[#{this.ddc}].onDragDrop = function(e, id){
                if(id == 'ffs_cover'){
                    FFS.Event.onDropCoverPhoto.fire(this.getEl().parentNode.firstChild.href);
                }
            };
            FFS.DD.photo[#{this.ddc}].endDrag = function(){
                Y.util.Dom.setStyle(this.getDragEl(), 'opacity', 1);
                Y.util.Dom.setXY(this.getEl(), this.startPos);
            };
        ]]></>, {'this.ddc':this.ddc});
        this.ddc++;
    }, this);
    head.appendChild($n('script', {innerHTML:script}));
},
updateDeck:function(){
    //! use delayed call for quick UI response. this function is too slow
    setTimeout($b(this, function(){
    var urls = [];
    var fsName = FavSetSelector.selectedFavSet;
    var psName = PhotoSelector.getSelected();
    // show & hide photos on deck
    if(fsName && fsName != ''){
        var fsSet = Storage.getFavSet(fsName).photo;
        switch(psName){
            case PhotoSelector.ALL:
                urls = Toolbar.isEdit() ? unionA(this.getAllPhoto(), fsSet) : fsSet;
                break;
            case PhotoSelector.NOT_IN_FAVSET: break;
            default:
                var psSet = Storage.getFavSet(psName).photo;
                urls = (fsName == psName) ? fsSet : interA(fsSet, psSet);
        }
    }else{
        switch(psName){
            case PhotoSelector.ALL: urls = this.getAllPhoto(); break;
            case PhotoSelector.NOT_IN_FAVSET:
                urls = diffA(this.getAllPhoto(), Storage.getAllPhoto());
                break;
            default: urls = Storage.getFavSet(psName).photo;
        }
    }
    this.hideAllPhoto();
    urls.forEach(function(url){this.showPhoto(url)}, this);
    // show & hide selection mark
    if(Toolbar.isEdit()){
        var anchors = $x('//span[contains(concat(" ",@class," ")," pc_s ")]/a');
        var set = Storage.getFavSet(FavSetSelector.selectedFavSet);
        if(set){
            anchors.forEach(function(a){
                url = a.href.replace('http://www.flickr.com', '');
                (set.photo.indexOf(url) != -1)
                    ? Y.addClass(a.nextSibling, 'ffs_selected')
                    : Y.removeClass(a.nextSibling, 'ffs_selected');
            });
        }else{
            anchors.forEach(function(a){
                Y.removeClass(a.nextSibling, 'ffs_selected');
            });
        }
    }
    this.sortDeck();
    PhotoSelector.updateCounter();
    }),0);
},
sortDeck:function(){
    if(Toolbar.isView() || FavSetSelector.selectedFavSet == PhotoSelector.getSelected()) return;
    var ref = Y.getFirstChild('favoriteThumbs');
    $x('id("favoriteThumbs")/span[child::div[@class="ffs_selected"]]').forEach(function(span){
        span.parentNode.insertBefore(span, ref);
    });
},
getAllPhoto:function(){
    return $x('//span[contains(concat(" ",@class," ")," pc_s ")]/a')
        .map(function(a){return a.href.replace('http://www.flickr.com', '')});
},
allPhoto:function(bool){
    $x('//span[contains(concat(" ",@class," ")," pc_s ")]').forEach(function(span){
        if(bool){
            Y.removeClass(span, 'ffs_hidden');
        }else{
            Y.addClass(span, 'ffs_hidden');
        }
    });
},
showAllPhoto:function(){this.allPhoto(true)},
hideAllPhoto:function(){this.allPhoto(false)},
showPhoto:function(url){
    url = url.replace('http://www.flickr.com', '');
    if($X('//span[contains(concat(" ",@class," ")," pc_s ") and child::a[contains(@href,"' + url + '")]]')){
        Y.removeClass($X.$, 'ffs_hidden');
    }else{
        var photo_id = url.match(/\/(\d+)\//)[1];
        w.F.API.callMethod('flickr.photos.getInfo', {
            photo_id:photo_id, format:'json'
        }, {flickr_photos_getInfo_onLoad:this.onloadphotoinfo});
    }
},
loadThumbnail:function(title, href, src){
    Y.insertBefore($n('span', {className:'photo_container pc_s'}, [
        $n('a', {title:title, href:href}, [
            $n('img', {className:'pc_img', width:'75', height:'75', alt:title, src:src})
        ])
    ]), 'clearfix');
    this.applyOverlay();
    PhotoSelector.updateCounter();
},
// Event Handler {{{2
fireSelectPhoto:function(e){ Event.fire('onSelectPhoto', e.target, !Y.hasClass(e.target, 'ffs_selected')); },
onfetch:function(nodes){
    this.cleanup();
    $x('id("Main")/div/div[@class="clearfix"]').forEach(function(e){e.parentNode.removeChild(e)});
    var divs = $x('//span[contains(concat(" ",@class," ")," pc_s ")]/..');
    var firstDiv = divs.shift();
    var loaded = toA(firstDiv.childNodes).filter(function(e){
        return e.nodeType == 1 && e.nodeName == 'SPAN';
    }).map(function(e){return e.firstChild.href});
    divs.forEach(function(div){
        toA(div.childNodes).forEach(function(span){
            if(span.nodeType == 1 && span.nodeName == 'SPAN' &&
              loaded.indexOf(span.firstChild.href) == -1){
                firstDiv.appendChild(span);
            }
        });
        div.parentNode.removeChild(div);
    });
    firstDiv.appendChild($n('div', {id:'clearfix', className:'clearfix'}));
    this.applyOverlay();
    this.updateDeck();
},
onselectphoto:function(type, args){
    var url = args[0].previousSibling.href;
    var name = FavSetSelector.selectedFavSet;
    if(args[1]){
        Y.addClass(args[0], 'ffs_selected');
        (name && name != '') && Storage.addPhoto(name, url);
    }else{
        Y.removeClass(args[0], 'ffs_selected');
        (name && name != '') && Storage.removePhoto(name, url);
    }
    PhotoSelector.updateCounter();
},
onselectfavset:function(type, args){ this.updateDeck(); },
onloadphotoinfo:function(s, rx, rt){
    var p = eval(rt.replace(/jsonFlickrApi/, '')).photo;
    var title = p.title._content + ' by ' + p.owner.username;
    var href = p.urls.url[0]._content;
    var src = 'http://farm' + p.farm + '.static.flickr.com/' + p.server + '/' + p.id + '_' + p.secret + '_s.jpg';
    this.loadThumbnail(title, href, src);
}
//}}}2
};

//// PhotoSelector {{{1
var PhotoSelector = {
ALL:'ffs_all', NOT_IN_FAVSET:'ffs_not_in_favset',
// init {{{2
init:function(){
    main.insertBefore($n('div',{id:'ffs_ps'}, [
      $n('select', {id:'ffs_ps_select'}, [
        $n('option', {value:this.ALL, innerHTML:I18N.$('PS_ALL')}),
        $n('option', {value:this.NOT_IN_FAVSET, innerHTML:I18N.$('PS_NOT_IN_FS')}),
        $n('optgroup', {id:'ffs_ps_favset', label:I18N.$('PS_YOUR_FS')})
      ]), $t(' '),
      $n('input', {id:'ffs_ps_text', type:'text', size:'30'}), $t(' '),
      $n('input', {id:'ffs_ps_go', className:'Butt', type:'button', value:I18N.$('UI_GO')}), $t(' '),
      $n('input', {id:'ffs_ps_clear', className:'Butt', type:'button', value:I18N.$('UI_CLEAR')}),
      $n('div', null, [
        $n('span', {id:'ffs_ps_show', innerHTML:I18N.$('PS_CNT_SHOW', 0)}), $n('b', {innerHTML:'::'}),
        $n('span', {id:'ffs_ps_load', innerHTML:I18N.$('PS_CNT_LOAD', 0)}), $n('b', {innerHTML:'::'}),
        $n('span', {id:'ffs_ps_fav', innerHTML:I18N.$('PS_CNT_FAV', $X('//div[@class="Results"]')
            .innerHTML.match(/[\d,]+/)[0].replace(',', ''))}),
        $n('span', {id:'ffs_ps_sel_cont'}, [
          $n('b', {innerHTML:'::'}),
          $n('span', {id:'ffs_ps_sel', innerHTML:I18N.$('PS_CNT_SEL', 0)}), $n('span', {innerHTML:'|'}),
          $n('a', {id:'ffs_ps_selall', className:'ffs_ps_button', innerHTML:I18N.$('PS_SEL_ALL')}), $n('span', {innerHTML:'|'}),
          $n('a', {id:'ffs_ps_clrall', className:'ffs_ps_button', innerHTML:I18N.$('PS_CLR_ALL')})
        ])
      ]),
      $n('div', {id:'ffs_ps_msg'}, [
        $n('span', {id:'ffs_ps_result', innerHTML:'No matches'}),
        $n('span', {id:'ffs_ps_search'}, [$n('img', {src:'http://l.yimg.com/g/images/progress/balls-16x16-trans.gif'})])
      ])
    ]), fav);

    this.ui = $('ffs_ps');
    this.uiSelect = $('ffs_ps_select');
    this.uiFavSets = $('ffs_ps_favset');
    this.uiText = $('ffs_ps_text');
    this.uiSearch = $('ffs_ps_go');
    this.uiClear = $('ffs_ps_clear');
    this.cntLoad = $('ffs_ps_load');
    this.cntShow = $('ffs_ps_show');
    this.cntSel = $('ffs_ps_sel');
    this.uiSelAll = $('ffs_ps_selall');
    this.uiClrAll = $('ffs_ps_clrall');
    this.uiMsg = $('ffs_ps_msg');

    this.onclicksearch = $b(this, this.onclicksearch);
    this.onclickclear = $b(this, this.onclickclear);
    this.onkeyuptext = $b(this, this.onkeyuptext);
    this.onchangeselect = $b(this, this.onchangeselect);
    this.oncommonfavset = $b(this, this.oncommonfavset);
    this.onclickselall = $b(this, this.onclickselall);
    this.onclickclrall = $b(this, this.onclickclrall);

    this.uiText.addEventListener('keyup', this.onkeyuptext, false);
    this.uiSearch.addEventListener('click', this.onclicksearch, false);
    this.uiClear.addEventListener('click', this.onclickclear, false);
    this.uiSelect.addEventListener('change', this.onchangeselect, false);
    this.uiSelAll.addEventListener('click', this.onclickselall, false);
    this.uiClrAll.addEventListener('click', this.onclickclrall, false);

    Event.subs('onAddFavSet', this.oncommonfavset);
    Event.subs('onRemoveFavSet', this.oncommonfavset);
    Event.subs('onRenameFavSet', this.oncommonfavset);

    this.updateList();
    this.updateCounter();
},
// DOM Operation {{{2
updateList:function(){
    this.uiFavSets.innerHTML = '';
    Storage.getAllSetName().forEach(function(name){
        this.uiFavSets.appendChild($n('option',{value:name
            , innerHTML:FavSetSelector.formatFavSetName(name, cfg.num.dd)}));
    }, this);
},
updateCounter:function(){
    this.cntShow.innerHTML = I18N.$('PS_CNT_SHOW', $x('id("favoriteThumbs")/span[not(contains(concat(" ",@class," ")," ffs_hidden "))]').length);
    this.cntSel.innerHTML = I18N.$('PS_CNT_SEL', $x('id("favoriteThumbs")/span/div[contains(concat(" ",@class," ")," ffs_selected ")]').length);
    this.cntLoad.innerHTML = I18N.$('PS_CNT_LOAD', $x('id("favoriteThumbs")/span[contains(concat(" ",@class," ")," pc_s ")]').length);
},
search:function(keyword, options){
    keyword = keyword.replace(' ', '+');
    GM_xmlhttpRequest({
        method:'GET', url:'http://www.flickr.com/search/?w=faves&q='+keyword,
        onload:$b(this, function(r){
            var html = r.responseText.replace(/<!DOCTYPE.*?>/, '').replace(/<html.*?>/, '').replace(/<\/html.*?>/, '');
            var doc  = document.implementation.createDocument(null, 'html', null);
            var range = document.createRange();
            range.setStartAfter(document.body);
            var fragment = range.createContextualFragment(html);
            try{
                fragment = doc.adoptNode(fragment);
            }catch(e){
                fragment = doc.importNode(fragment, true);
            }
            doc.documentElement.appendChild(fragment);

            var cnt = 0;
            $x('//span[@class="photo_container pc_m"]/a', doc).forEach(function(a){
                PhotoDeck.showPhoto(a.href);
                cnt++;
            });
            this.enableMsg(cnt === 0, 'result');
            this.updateCounter();
        }),
        onerror:function(r){console.log('ERROR:\n'+r.responseText)}
    });
    this.enableMsg(true, 'search');
    PhotoDeck.hideAllPhoto();
},
getSelected:function(){ return this.uiSelect.value; },
enableMsg:function(bool, id){
    if(bool){
        this.uiMsg.className = '';
        Y.addClass(this.uiMsg, id);
        Y.setStyle(this.uiMsg, 'display', 'block');
    }else{
        Y.setStyle(this.uiMsg, 'display', 'none');
    }
},
// Event Handler {{{2
onclicksearch:function(){ if(this.uiText.value != '') this.search(this.uiText.value); },
onclickclear:function(){
    this.uiText.value = '';
    this.uiSelect.value = this.ALL;
    this.enableMsg(false);
    PhotoDeck.updateDeck();
},
onkeyuptext:function(e){
    switch(e.keyCode){
        case e.DOM_VK_RETURN: this.onclicksearch(); break;
        case e.DOM_VK_ESCAPE:
            this.uiText.value = '';
            PhotoDeck.updateDeck();
            break;
    }
},
onchangeselect:function(){ PhotoDeck.updateDeck(); },
oncommonfavset:function(){ this.updateList(); },
onclickselall:function(){
    var urls = $x('id("favoriteThumbs")/span[not(contains(concat(" ",@class," ")," ffs_hidden "))]').map(function(span){
        Y.addClass(span.lastChild, 'ffs_selected');
        return span.firstChild.href;
    });
    var name = FavSetSelector.selectedFavSet;
    if(name != '' && urls.length != 0){
        Storage.addPhoto(name, urls);
    }
    this.updateCounter();
},
onclickclrall:function(){
    var urls = $x('id("favoriteThumbs")/span[not(contains(concat(" ",@class," ")," ffs_hidden "))]').map(function(span){
        Y.removeClass(span.lastChild, 'ffs_selected');
        return span.firstChild.href;
    });
    var name = FavSetSelector.selectedFavSet;
    if(name != '' && urls.length != 0){
        Storage.removePhoto(name, urls);
    }
    this.updateCounter();
}
};

//// Main {{{1
Storage.init();
I18N.init();
Event.init();
Toolbar.init();
FavSetSelector.init();
FavSetInfo.init();
PhotoSelector.init();
PhotoDeck.init();

GM_registerMenuCommand('Flickr Fav Set - Clear Data', $b(Storage, Storage.clear));

//// Utils {{{1
function $x(x,c){c=c||document;var d=c.ownerDocument||c;
var r=d.evaluate(x,c,null,4,null);for(var i,n=[];i=r.iterateNext();n.push(i));return n}
function $X(x,c){var e=$x(x,c)[0];e&&(arguments.callee.$=e);return e}
function $n(t,o,c){var e = document.createElement(t);if(o){for(var k in o)
{e[k]=o[k]}}if(c){c.forEach(function(ch){e.appendChild(ch)})}return e}
function $t(t){return document.createTextNode(t)}
function $(id){return document.getElementById(id)}
function $b(o,f){return function(){return f.apply(o,arguments)}}
function $e(t,o){t=t.toString();for(var k in o){t=t.replace(new RegExp('#{'+k+'}','g'),o[k])}return t}
function $h(o,a){if(a.length==0)return false;return a.every(function(p){return o.hasOwnProperty(p)})}
function isO(o){return o.constructor.toString()==Object.toString()}
function isA(a){return a.constructor.toString()==Array.toString()}
function isS(s){return (typeof s)=='string'}
function diffA(a,b){b.forEach(function(i){var p=a.indexOf(i);
if(p!=-1){a=a.slice(0,p).concat(a.slice(p+1))}});return a}
function interA(a,b){return a.filter(function(i){return b.indexOf(i)!=-1})}
function unionA(a,b){(a.length<b.length)&&([a,b]=[b,a]);
b.forEach(function(i){(a.indexOf(i)==-1)&&(a.push(i))});return a}
function toA(o){for(var i=0,a=[];i<o.length;a.push(o[i]),i++);return a}
function pdg(s){s=s.toString();if(s.length<2){s='0'+s}return s}
function fmtD(d){d=new Date(parseInt(d));return d.getFullYear()
+'.'+pdg(d.getMonth()+1)+'.'+pdg(d.getDate())}
// }}}1

})();
// vim:set foldmethod=marker: