fastladder/fastladder

View on GitHub
app/assets/javascripts/lib/reader/main.js

Summary

Maintainability
F
6 days
Test Coverage
window.onload   = init;
window.onresize = function(){LDR.invoke_hook('WINDOW_RESIZE')};

var app = LDR.Application.getInstance();

// API Key
(function(){
    LDR.API.StickyQuery = { ApiKey: ApiKey };
    function getApiKey(){
        var ck = new Cookie().parse();
        for(var key in ck){
            if(/_sid/.test(key)){
                return ck[key]
            }
        }
    };
    if(/^\[/.test(ApiKey)){
        LDR.API.StickyQuery = { ApiKey: getApiKey() };
    }
}).call(LDR);


/*
 browser
*/
// TODO replace $.browser
var browser = new BrowserDetect;

/*
 Global

*/
// 記事を読み込む順番

function typecast_config(obj){
    each(obj, function(value,key){
        if(!LDR.TypeofConfig[key]) return;
        // "0" を falseに。
        switch(LDR.TypeofConfig[key]){
            case 'Boolean':
                 obj[key] =
                    (value == "1") ? true :
                    (value == "0") ? false :
                    (value == "true") ? true :
                    (value == "false") ? false :
                    Boolean(value);
                break;
            case 'Number':
                obj[key] = parseInt(value)
                break;
            default:
                obj[key] = value;
        }
    });
    return obj;
}


var TabManager = {};

function TabClick(e){
    var tab = this;
    var get_target = function(el){
        var rel = el.getAttribute("rel");
        var op  = rel.slice(4);
        var tmp = op.split(">");
        var base = tmp[0];
        var target = tmp[1];
        return [base,target];
    };
    var base   = get_target(tab)[0];
    var target = get_target(tab)[1];
    Element.show(target);
    TabManager[base] = target;
    foreach(_$(base).getElementsByTagName("*"), function(el){
        var cl = el.className;
        if(!contain(cl,"tab") || !el.getAttribute("rel")) return;
        if(el == tab){
            switchClass(el,"tab-active");
        } else {
            switchClass(el,"tab-inactive");
            Element.hide(get_target(el)[1]);
        }
    });
    update(base);
}

var Keybind;
function IME_off(msg){
    if(!browser.isFirefox || !browser.isWin) return;
    var s = $N("span");
    s.innerHTML = '<input type="password" id="ime_off" style="visibility:hidden">';
    document.body.appendChild(s);
    setTimeout(function(){
        var el = _$("ime_off");
        if(el){
            el.focus();
            document.body.removeChild(s);
            Keybind.lastInvoke = null;
            if(msg)
                message("IMEをオフにしました");
        }
    }._try(),10);
}

function getStyle(element, style){
    element = _$(element);
    var value = element.style[style.camelize()];
    if (!value) {
        if (document.defaultView && document.defaultView.getComputedStyle) {
            var css = document.defaultView.getComputedStyle(element, null);
            value = css ? css.getPropertyValue(style) : null;
        } else if (element.currentStyle) {
            value = element.currentStyle[style.camelize()];
        }
    }
    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
        if (getStyle(element, 'position') == 'static') value = 'auto';
    return value == 'auto' ? null : value;
}

/*
 Debug用関数
*/
function Dumper(obj){
    var buf = [];
    for(var i in obj){
        buf.push( i+" = "+obj[i]);
    }
    return buf.join("\n")
}

/* Global変数 */
var subs,inbox;

function message(str){
    _$("message").innerHTML = str;
}

app.state.last_items = {};

function format_keybind(){
    var help = [];
    var kbd = function(str){
        var list = str.split("|");
        if(!app.state.keyhelp_more){list = [list[0]]};
        return list.map(function(v){
            if(/\w/.test(v) && v == v.toUpperCase()){
                v = "shift+" + v.toLowerCase();
            }
            v = v.replace("+down","+↓");
            v = v.replace("+up","+↑");
            return v.aroundTag("kbd");
        }).join("<br>");
    };
    LDR.KeyHelpOrder.forEach(function(row, num){
        if(!app.state.keyhelp_more && num > 1) return;
        help.push("<tr>");
        row.forEach(function(f){
            var l  = LDR.KeyHelp[f];
            var kb = LDR.KeyConfig[f];
            if(kb){
                help.push("<th>" + l + ":</th><td>" + kbd(kb) + "</td>");
            } else {
                help.push("<td colspan='2'></td>");
            }
        });
        help.push("</tr>");
    });
    var table = "<table>" + help.join("") + "</table>";
    var button = [
        '<div class="keyhelp_desc">',
        '<div class="keyhelp_ime">',
            "If you can't use shortcut keys, try disabling Japanese input.",
        '</div>',
        '<div class="keyhelp_more">',
            '<span class="button"r onclick="Control.open_keyhelp.call(this,event)">' + "show in window" + '</span>',
            '<span class="button" onclick="Control.toggle_more_keyhelp.call(this,event)">'+
             (app.state.keyhelp_more ? "compact" : "more" + '...') + '</span>',
        '</div>',
        '</div>',
        '<div class="keyhelp_hide">',
            '<span class="button" onclick="Control.hide_keyhelp.call(this,event)">' + "close" + '</span>',
        '</div>'
    ];
    return table + button.join("");
}


function check_wait(){
    if(app.config.use_wait != 1) return false;
    var st = check_wait.state;
    var key = Keybind.lastInput;
    // 初回
    if(!st[key]){
        st[key] = new Date;
        return false;
    } else {
        var now = new Date;
        if(now - st[key] > app.config.wait){
            st[key] = new Date;
            return false;
        } else {
            return true
        }
    }
}

check_wait.state = {};
app.pin = new Pin;


//prefetch
function prefetch(sid,count){
    var max_prefetch = LDR.VARS.MaxPrefetch;
    get_unread.cache.set(sid, "prefetch");
    switchClass("subs_item_" + sid, "ps-prefetching");
    message("prefetching");
    function store_cache(json){
        get_unread.cache.set(sid, json);
        if(json.channel){
            var expr = json.channel.expires * 1000;
        }
        get_unread.cache.set_expr(sid, expr);
        switchClass("subs_item_" + sid, "ps-prefetched");
        message("prefetch_complete");
    }
    if(subs_item(sid).unread_count == 0 && app.config.show_all == true){
        var api = new LDR.API("/api/all");
        api.post({
            subscribe_id : sid,
            offset : 0,
            limit  : 1
        }, store_cache);
    } else {
        var api = new LDR.API("/api/unread?prefetch");
        api.post({ subscribe_id : sid }, store_cache);
    }
}

function get_prefetch_num(){
    var prefetch_num;
    if(app.config.use_prefetch_hack){
        prefetch_num = app.config.prefetch_num;
        if(0 <= prefetch_num && prefetch_num <= LDR.VARS.MaxPrefetch){
            return prefetch_num;
        } else {
            return LDR.VARS.DefaultPrefetch;
        }
    } else {
        return LDR.VARS.DefaultPrefetch;
    }
}
function get_next_group(){
    var prefetch_num = get_prefetch_num();
    if(prefetch_num == 0) return null;
    var sid = app.state.now_reading;
    if(!sid && Ordered.list){
        return Ordered.list[0].slice(0, prefetch_num - 1);
    }
    var list = Ordered.list;
    if(!list) return;
    var offset = list.indexOfStr(sid);
    var next_group = list.slice(offset + 1, offset + prefetch_num + 1);
    return next_group;
}


//scroll_hilight event register
var target = false;
var timer;
var count = 0;
function update_hilight(){
    // message(++count);
    var item = get_active_item(1);
    if(item){
        if (target){
            removeClass(target, "hilight");
        }
        target = _$("item_count_" + item.offset);
        addClass(target, "hilight");
    }
}
Event.observe(_$('right_container'), 'scroll', function(){
    if(app.config.use_scroll_hilight){
        clearTimeout(timer);
        timer = setTimeout(update_hilight,100);
    }
});


function writing_complete(){
    if(app.state.writer && app.state.writer.complete == false){
        return false;
    }
    if(app.state.writer2 && app.state.writer2.complete == false){
        return false;
    }
    return true;
}

/*
 Toggle Base
*/
var ToggleBase = Class.create().extend({
    initialize: function(){
        this.observe("click");
    },
    observe: function(){
        var self = this;
        Array.from(arguments).forEach(function(handle){
            self[handle] = function(event){
                self["on"+handle].call(self, this, event)
            }
        });
    },
    onclick: function(el, event){
        Event.stop(event);
        return this.toggle(el)
    },
    toggle: function(el){
        var state = (el.clicked == true) ? this.off(el) : this.on(el);
        return state;
    },
    on: function(el){
        addClass(el, "toggle_on");
        el.clicked = true;
        return "on";
    },
    off: function(el){
        removeClass(el, "toggle_on");
        el.clicked = false;
        return "off";
    }
});

var ShowFolder = Class.create().extend({
    on: function(el, e){
        var self = this;
        this.folder_menu = Control.show_folder.call(el, e);
        this.folder_menu.onhide = function(){
            self.off(el, e)
        }
    },
    off: function(el, e){
        this.folder_menu && this.hide_menu()
    },
    hide_menu: function(){
        this.folder_menu.hide();
        this.folder_menu = null;
    }
});

var FolderToggle = Class.merge(ToggleBase, ShowFolder);
FolderToggle = new FolderToggle;

var ShowViewmode = Class.create().extend({
    sw: Control.show_viewmode,
    on: function(el, e){
        var self = this;
        this.menu = this.sw.call(el, e);
        this.menu.onhide = function(){
            self.off(el, e)
        }
    },
    off: function(el, e){
        this.menu && this.hide_menu()
    },
    hide_menu: function(){
        this.menu.hide();
        this.menu = null;
    }
});
var ShowSortmode = Class.base(ShowViewmode).extend({sw:Control.show_sortmode});
var ViewmodeToggle = Class.merge(ToggleBase, ShowViewmode);
ViewmodeToggle = new ViewmodeToggle;
var SortmodeToggle = Class.merge(ToggleBase, ShowSortmode);
SortmodeToggle = new SortmodeToggle;


// 未読の記事のキャッシュ
var UnreadCache = new Cache({max : 30});

// 先頭の未読
function get_head(){
    var list = Ordered.list;
    if(!list) return;
    var i = list.indexOfA(function(sid){
        var item = subs_item(sid);
        return (item.unread_count && app.state.now_reading != item.subscribe_id);
    });
    if(i == -1){return list[0]}
    return list[i] || list[0];
}
// 最後の未読
function get_end(){
    var list = Ordered.list;
    if(!list) return;
    list = list.concat().reverse();
    var i = list.indexOfA(function(sid){
        var item = subs_item(sid);
        return (item.unread_count && app.state.now_reading != item.subscribe_id);
    });
    if(i == -1){return list[0]}
    return list[i] || list[0];
}
// 次のアイテム
function get_next(){
    var sid = app.state.now_reading;
    if(!sid && Ordered.list){
        return Ordered.list[0];
    }
    var list = Ordered.list;
    if(!list) return;
    var offset = list.indexOfStr(sid);
    var next = list[offset+1];
    return next;
}
// 前のアイテム
function get_prev(){
    var sid = app.state.now_reading;
    if(!sid && Ordered.list){
        return Ordered.list[0];
    }
    var list = Ordered.list;
    if(!list) return;
    var offset = list.indexOfStr(sid);
    var prev = list[offset-1];
    return prev;
}

/*
  MenuItem共通
 */
var MenuItem = new (Class.base(ListItem).extend({
    focus_class  : "menu-focus",
    focus_style  : { },
    normal_style : { }
}));
/*
 PinItem
*/
var PinItem = new (Class.base(ListItem).extend({
    focus_class  : "pin-focus",
    focus_style  : { },
    normal_style : { }
}));
/*
 Subscribe item
*/
var SubsItem = new (Class.base(ListItem).extend({
    focus_class  : "fs-focus",
    focus_style  : { },
    normal_style : { }
}));



/*

*/
Class.Traits["view"] = {
    initialize: function(element){
        this.element = _$(element);
    },
    print: function(v){
        isElement(v)
            ?  this.element.appendChild(v)
            : (this.element.innerHTML = v)
    },
    clear: function(){ this.print("") },
    setClass: function(v){
        this.element.className = v;
    },
    addClass: function(v){
        addClass(this.element,v)
    },
    removeClass: function(v){
        removeClass(this.element,v)
    }
};
Class.Traits["controller"] = {
    initialize: function(o){
        this.view  = o.view;
        this.model = o.model;
    }
};

function alert_once(v){
    if(alert_once.alerted) return;
    alert(v);
    alert_once.alerted = 1;
}

//**
LDR.VARS.USE_PARTIAL_LOAD = true;
Subscribe.View = Class.create("view");
Subscribe.Controller = Class.create("controller").extend({
    loaded : false,
    readyState: 0,
    filter : null,
    _update: function(reload_flag){
        var self = this;
        if(this.loaded && !reload_flag){
            this.show();
            update("total_unread_count");
        } else {
            app.state.subs_reloading = true;
            LDR.invoke_hook('BEFORE_SUBS_LOAD');
            new LDR.API("/api/subs?unread="+(app.config.show_all ? 0 : 1)).post({},
            function(list){
                self.loaded = true;
                app.state.subs_reloading = false;
                console.log(self,self.model);
                self.model.load(list);
                self.sort();
                self.update();
                LDR.invoke_hook('AFTER_SUBS_LOAD');
            });
        }
    },
    update: function(reload_flag){
        if(!LDR.VARS.USE_PARTIAL_LOAD){
            return this._update.apply(this, arguments);
        }
        var self = this;
        if((!reload_flag && this.loaded) || this.readyState >= 3){
            this.show();
            update("total_unread_count");
        } else {
            self.readyState = 0;
            if(app.state.subs_loader){
                app.state.subs_loader.cancel();
            }
            app.state.subs_reloading = true;
            app.state.load_progress = true;
            app.state.subs_loader = {
                cancel: function(){
                    message("Aborted.");
                    canceled = true;
                }
            };
            LDR.invoke_hook('BEFORE_SUBS_LOAD');
            self.model.load_start();
            var canceled = false;
            var from_id = 0;
            var is_first = 1;
            var allways_flush = 1;
            var writed = 0;
            var limit1 = LDR.VARS.SubsLimit1; // 100;
            var limit2 = LDR.VARS.SubsLimit2; // 200;
            var limit = limit1;
            var list = [];
            var count = 0;
            var load_request = function(){
                limit = is_first ? limit1 : limit2;
                is_first = 0;
                var api = new LDR.API([
                    "/api/subs?",
                    "unread=", (app.config.show_all ? 0 : 1),
                    "&from_id=", from_id,
                    "&limit=", limit
                ].join(""));
                api.post({},onload);
            };
            var onload = function(tmp){
                if(canceled) return;
                self.readyState = 3;
                list = list.concat(tmp);
                if(from_id == 0 || allways_flush){
                    flush(list);
                }
                if(tmp.length < limit){
                    self.model.load_partial_data(tmp);
                    load_complete();
                // for compatible servers (ex:PlaggerLDR)
                } else if(tmp.length > limit){
                    self.model.load_partial_data(tmp);
                    load_complete();
                } else {
                    self.model.load_partial_data(tmp);
                    from_id = tmp[tmp.length-1].subscribe_id + 1;
                    update('total_unread_count');
                    count+=limit;
                    load_request();
                    message("Loading .. " + (count+1) + " - " + (count+limit));
                }
            };
            var flush = function(list){
                self.model.load_data(list);
                self.sort();
                self.show();
                writed = 1;
            };
            var load_complete = function(){
                self.readyState = 4;
                self.loaded = true;
                app.state.load_progress = false;
                app.state.subs_reloading = false;
                app.state.subs_loader = null;
                //if(!writed){
                    self.model.load_data(list);
                    self.sort();
                    self.update();
                //} else {
                //    update("total_unread_count");
                //}
                LDR.invoke_hook('AFTER_SUBS_LOAD');
                message('Loading completed.');
                setTimeout(function(){self.readyState=0},3000);
            };
            self.readyState = 1;
            load_request();
        }
    },
    add_filter: function(q){
        var filter = function(item){
            return contain(item.title,q)
        };
        this.filter = function(model){
            return model.filter(filter)
        }
    },
    sort: function(){
        var tmp = app.config.sort_mode.split(':');
        var key = tmp[0];
        var option = tmp[1];
        this.model.list.sort_by(key);
        if(option == "reverse")
            this.model.list.reverse();
        // folderをソート
        if(key == "title"){
            this.model.folder_names.sort();
        }
    },
    show: function(){
        if(this.filter){
            var mode = "flat";
            var data = this.filter(this.model);
        } else {
            var mode = app.config.view_mode;
            var data = this.model;
        }
        this.view.clear();
        this.view.setClass(mode);
        this.view.print( SF[mode](data) );
        _$("subs_container").scrollLeft = 0;
        if(app.state.now_reading){
            set_focus(app.state.now_reading)
        }
        this.update_order()
    },
    update_order: function(array){
        if(array){
            Ordered.list = array;
            return;
        }
        var domlist = this.view.element.getElementsByTagName("span");
        Ordered.list = [];
        foreach(domlist,function(el){
            var sid = el.getAttribute("subscribe_id");
            sid && Ordered.list.push(sid)
        });
    },
    find: function(q){
        var self = this;
        if(q == ""){
            this.filter = null;
            this.show();
        } else {
            this.add_filter(q)
            this.show();
        }
    },
    get_by_id : delegator("model","get_by_id")
});

var SF = Subscribe.Formatter;
var ST = Subscribe.Template;
var SC = Subscribe.Controler;


/* Filter */
var Filter = {};
Filter.paddingLeft = function(str){ return function(v){ return v ? str + v : "" } };
Filter.sandwich    = function(l,r){ return function(v){ return v ? l+v+r : "" } };
//
Filter.author = Filter.paddingLeft("by ");
Filter.created_on = function(v){return Filter.paddingLeft("投稿: ")(Filter.toDate(v))};
Filter.modified_on = function(v){return Filter.paddingLeft(" | 更新: ")(Filter.toDate(v))};
Filter.toDate = function(v){ return v ? new DateTime(v*1000).toString() : "" };
Filter.toRelativeDate = function(v){return NowDate.rel( new DateTime(v*1000) );};
Filter.enclosure = function(v,k,tmpl){
    if(!v) return "";
    var t = tmpl.get_param("enclosure_type") || "";
    return '<a href="'+v+'">'+"DL : "+t+'</a>'
}
Filter.category = function(v,k,tmpl){
    if(!v) return "";
    return v.split(" ").join("&nbsp;");
}



var NowDate;


/****************************************************
  フィードの整形
 ***************************************************/

Feed = {};
Feed.Formatter = {
    channel : {
        image: function(src){
            if(!src){ return "" }
            return [
                '<img class="channel_image"',
                ' style="visibility:hidden"',
                ' src="/img/alpha/alpha_01.png"',
                ' onload="swap_channel_image(this,\''+src+'\');',
                'Util.style.visible(this)"',
                '>'
            ].join("")
        }
    }
};
var FF = Feed.Formatter;

/*
 画面サイズに合わせる
*/
MakeUpdater("style");
style_updater("left_container", function(){
    setStyle(this,{
        display : app.state.show_left ? "block": "none",
        width   : app.state.leftpane_width   + "px",
        height  : app.state.container_height + "px"
    });
}._try());

style_updater("subs_container", function(){
    var h = app.state.container_height - _$("subs_tools").offsetHeight;
    setStyle(this,{
        display : app.state.show_left ? "block": "none",
        width   : app.state.leftpane_width + "px",
        height  : h + "px"
    })
}._try());

style_updater("right_container", function(){
    var border_w = 2;
    setStyle(this,{
         height : app.state.container_height + "px",
         width  : document.body.offsetWidth - app.state.leftpane_width - border_w + "px"
    });
}._try());


var has_subscribe_id = has_attr("subscribe_id");


// input要素にキーバインドを効かせる。
// printableな場合は実行しない
function hotkey_filter(e){
    var el  = (e.target || e.srcElement);
    var tag = el.tagName;
    var id  = el.id;
    if(id == "finder"){
        if(HotKey.isPrintable(e)) return false;
        if(HotKey.getChar(e) == 'delete') return false;
    }
    return true;
}
function find_from(key){
    return function(v){
        return v[key].indexOf(this) != -1
    }
}

var show_tips = function(){
    show_tips.count ++;
    if(show_tips.count > 10){
        _$("loadicon").src = show_tips.icon;
        message(show_tips.text);
        show_tips.count = 0;
    }
};
show_tips.icon = "/img/icon/rest3.gif";
show_tips.text = 'なんかようか?';
show_tips.count = 0;

var LoadEffect = {
    ICON_PATH : "/img/icon/",
    LOADICON_NUM : 1,
    RESTICON_NUM : 3,
    Start : function(){
        var L = LoadEffect;
        var path = L.ICON_PATH;
        if(L.LOADICON_NUM > 1){
            var n = 1 + Math.floor(Math.rand(L.LOADICON_NUM));
            path += "loading" + n + ".gif";
        } else {
            path += "loading.gif";
        }
        _$("loadicon").src = path;
        setStyle("loading",{visibility:"visible"})
    },
    Stop : function(){
        var L = LoadEffect;
        var path = L.ICON_PATH;
        if(L.RESTICON_NUM > 1){
            var n = 1 + Math.floor(Math.rand(L.RESTICON_NUM));
            path += "rest" + n + ".gif";
        } else {
            path += "rest.gif";
        }
        _$("loadicon").src = path;
    }
};

/*
 初期化処理
*/

function init_manage(){
    if(app.state.guest_mode){
        message('この機能は使えません');
        return;
    }
    switchClass("right_container","mode-manage");
    ahah("/contents/manage","right_body",function(){
        get_folders(function(){
            update("manage_item");
            update("manage_folder");
        });
    });
}

function init_config(){
// var State = new LDR.StateClass;
    ahah("/contents/config", "right_body", function(){
        switchClass("right_container", "mode-config");
        Control.scroll_top();
        Form.fill("config_form", app.config);
        ajaxize("config_form",{
            before: function(){return true},
            after: function(res,req){
                message("Your settings have been saved");
                typecast_config(req);
                Object.extend(app.config, req);
            }
        });
        TabClick.call(_$("tab_config_basic"));
        LDR.invoke_hook('AFTER_INIT_CONFIG');
    });
}

function init_guide(){
    ahah("/contents/guide", "right_body", function(){
        switchClass("right_container", "mode-guide");
        Control.scroll_top();
        LDR.invoke_hook('AFTER_INIT_GUIDE');
    });
}


var default_right_init = init_guide;

function load_content(){
    var q = location.href;
    var o = q.indexOf("#")+1;
    if(!o){
        default_right_init();
        return
    }
    var param = q.slice(o);
    if(window["init_" + param]){
        window["init_" + param]()
    } else {
        default_right_init()
    }
}

function init(){
    app.load({}, function(){
        LDR.setup_hook();
        LDR.invoke_hook('BEFORE_INIT');
        window.onerror = function(a,b,c){
            _$("message").innerHTML = [a,b,c];
            return false;
        }

        app.state.leftpane_width = LDR.VARS.LeftpaneWidth;

        DOM.show("container");
        DOM.show("footer");
        fit_screen();
        DOM.show("right_container");

        LDR.API.registerCallback({
            Create  : LoadEffect.Start,
            Complete: LoadEffect.Stop
        });


        subs = new Subscribe.Controller({
            model : new Subscribe.Model,
            view  : new Subscribe.View("subs_body")
        })

        app.finder = new Finder("finder");
        app.finder.clear();
        app.finder.add_callback(function(q){
            if(!q){
                return subs.find("");
            }
            var query;
            if(typeof Roma == "function"){
                var roma = new Roma();
                try{
                    query = new RegExp(roma.toRegExp(q),"i");
                } catch(e){
                    query = q;
                }
            } else {
                try{
                    query = new RegExp(q, "i");
                } catch(e){
                    query = q;
                }
            }
            subs.find(query)
        });

        setup_event();
        LDR.setup_hotkey();

        // ajaxize
        ajaxize("discover_form",{
            before:function(){
                var output = _$("discover_items");
                output.innerHTML = [
                    '<div class="discover_loading">',
                    '<img src="/img/icon/loading.gif"> ',
                    "Finding a feed. Please wait.",
                    '</div>'
                ].join("");
                return true;
            },
            after: print_discover
        });

        (function(){
            load_content();
            LDR.invoke_hook('BEFORE_CONFIGLOAD');
            app.config.load(function(){
                LDR.invoke_hook('AFTER_CONFIGLOAD');
                subs.update();
            });
        }).later(10)();
        LDR.invoke_hook('AFTER_INIT');

    });
}

function print_discover(list){
    var output = _$("discover_items");
    var sub   = Template.get("discover_select_sub").compile()
    var unsub = Template.get("discover_select_unsub").compile()
    if(list.length == 0){
        output.innerHTML = [
            '<div class="discover_loading"><img src="/img/icon/orz.gif"> ',
      "Finding a feed. Please wait.",
            '</div>'
        ].join("");
    } else {
        var seen = {};
        var uniq_list = [];
        list.sort_by("subscribers_count");
        list.forEach(function(item){
            if(!seen[item.feedlink]){
                uniq_list.push(item);
                seen[item.feedlink] = true;
            }
        });
        output.innerHTML = uniq_list.map(function(item){
            var users = item.subscribers_count == 1 ? "user" : "users";
            if(item.subscribe_id){
                feedlink2id[item.feedlink] = item.subscribe_id;
                return unsub(item,{users: users});
            } else {
                return sub(item,{users: users})
            }
        }).join("");
    }
}


function set_focus(id){
    var el = _$("subs_item_"+id);
    if(app.state.last_element){
        removeClass(app.state.last_element, "fs-reading");
        touch(app.state.last_id, "onclose");
    }
    if(el){
        app.state.last_element = el;
        app.state.last_id = id;
        switchClass(el, "fs-reading");
        if(app.config.view_mode != "flat"){
            var tvroot = QueryCSS.findParent(function(){
                return /^treeview/.test(this.id)
            },el);
            tv = TreeView.get_control(tvroot.id);
            tv && tv.open()
        }
        var sc = _$("subs_container");
        sc.scrollTop = el.offsetTop - _$("subs_container").offsetTop - 64;
        sc.scrollLeft = 0;
    }
    // window.status = "sid = " + id;
}


function QueryCSS(){}
QueryCSS.findParent = function(rule,element){
    elememt = _$(element);
    if(!isFunction(rule)){
        rule = rule.isQueryCSS ? rule : new QueryCSS(rule).match;
    }
    var current = element;
    while(current = current.parentNode){
        if(rule.call(current)) return current;
    }
    return false;
}
/*
 先頭の記事を読み込む
*/
function get_first(id,callback){
    app.state.viewrange.start = 0;
    app.state.has_next = true;
    if(get_unread.cache.has(id)){
        var cached_data = get_unread.cache.get(id);
        // 読み込み中
        if(cached_data == "prefetch"){
            setTimeout(arguments.callee.curry(id,callback), 10);
            return
        }
        print_feed.next(callback)(cached_data);
        set_focus(id);
        return;
    } else {
        var api = new LDR.API("/api/all");
        set_focus(id)
        api.post({
             subscribe_id : id,
             offset : 0,
             limit  : 1
        }, function(data){
            get_unread.cache.set(id,data);
            print_feed.next(callback)(data);
        });
    }
}
/*
 未読の記事を読み込む
*/
LDR.VARS.PrefetchTimeout = 2000;
LDR.VARS.LockTimeout = 2000;
function get_unread(id,callback){
    app.state.viewrange.start = 0;
    app.state.has_next = true;
    var api_url = '/api/unread';
    function has_cache(){
        var cached_data = get_unread.cache.get(id);
        if(cached_data == "prefetch"){
            var start = new Date - 0;
            var retry = function(){
                var now = new Date - 0;
                var cached_data = get_unread.cache.get(id);
                if(cached_data != "prefetch"){
                    loaded(cached_data);
                } else if(now - start > LDR.VARS.PrefetchTimeout){
                    // console.log('prefetch_timeout');
                    prefetch_timeout();
                } else {
                    retry();
                }
            }.later(100);
            retry();
            return;
        }
        function loaded(cached_data){
            print_feed.next(callback)(cached_data);
            set_focus(id);
        }
        loaded(cached_data);
    }
    function no_cache(){
        var api = new LDR.API(api_url);
        var success = false;
        set_focus(id)
        api.post({ subscribe_id : id }, function(data){
            success = true;
            get_unread.cache.set(id,data);
            print_feed.next(callback)(data);
        });
        // release lock
        setTimeout(function(){
            if(!success){app.state.requested = false}
        }, LDR.VARS.LockTimeout);
    }
    function prefetch_timeout(){
        // unlock
        app.state.requested = false;
        api_url = '/api/unread?timeout';
        no_cache();
    }
    get_unread.cache.has(id) ? has_cache() : no_cache();
}

get_unread.cache = new Cache({max:50});

var LDRWidgets = Class.create();
LDRWidgets.extend({
    initialize: function(){
        this.widgets = [];
        this.sep = " | ";
    },
    add: function(name, generator, description){
        this.widgets.push({
            name: name,
            generator: generator,
            description: description || ''
        })
    },
    remove: function(name){
        this.widgets = this.widgets.reject(function(v){
            return v.name == name
        })
    },
    process: function(){
        var args = arguments;
        return this.widgets.map(function(w){
            var result;
            try{
                result = w.generator.apply(null, args);
            } catch(e){
                result = e;
            }
            return result ? '<span class="widget widget_'+ w.name +'" title="' + w.description + '">'+result+'</span>' : "";
        }).filter(function(v){return v}).join(this.sep);
    }
});

var entry_widgets = new LDRWidgets;
var channel_widgets = new LDRWidgets;

function fix_linktarget(el){
    el = el || _$(print_feed.target);
    foreach(el.getElementsByTagName("a"),
        function(a){
            a.target != "_self" && (a.target = base_target)
        }
    );
}

var get_action = get_attr("_action");
var Actions = {
    load: function(){}
};

/* 中止処理 */
window.stop = window.stop || function(){
    document.execCommand("Stop")
};

// delete _XMLHttpRequest;
function check_xmlhttp(){
    if(typeof _XMLHttpRequest == "undefined"){
        update("error_window");
        window.onload = null;
    }
}
check_xmlhttp();