WaniKani App Store

Never miss an awesome WaniKani script again!

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WaniKani App Store
// @version      1.3.0
// @description  Never miss an awesome WaniKani script again!
// @author       hitechbunny
// @include      https://www.wanikani.com/*
// @include      https://community.wanikani.com/*
// @run-at       document-end
// @grant        none
// @namespace https://greasyfork.org/users/149329
// ==/UserScript==

(function() {
    'use strict';

    var api_key;
    var globalVariables = {};
    var scripts;
    var uuid;

    if (window.location.pathname.indexOf('/script/appStore') === 0) {
        renderAppStore();
    } else {
        setUuid();
        renderHooks();
        recordCss();
        loadGlobalVariablesThenTrackScripts();
    }

    function loadGlobalVariablesThenTrackScripts() {
        if (localStorage.appStoreGlobalVariables) {
            globalVariables = JSON.parse(localStorage.appStoreGlobalVariables);
            setTimeout(trackScripts, 100);
        } else {
            get_api_key().then(function() {
                ajax_retry('https://wanikanitools-golang.curiousattemptbunny.com/scripts?api_key='+api_key).then(function(json) {
                    storeGlobalVariables(json);
                    globalVariables = JSON.parse(localStorage.appStoreGlobalVariables);
                    setTimeout(trackScripts, 100);
                });
            });
        }
    }

    function renderHooks() {
        // Hook into App Store
        $('<li class="app-store-menu-item-actual"><a href="/script/appStore">App Store</a></li>').insertBefore($('.navbar .dropdown-menu .nav-header:contains("Account")'));

        var css =
            '.app-store-menu-item { display: none; }'+

            '';

        $('head').append('<style type="text/css">'+css+'</style>');
    }

    function setUuid() {
        function generateUuid() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
                return v.toString(16);
            });
        }
        uuid = localStorage.appStoreUuid;
        if (!uuid) {
            uuid = generateUuid();
            localStorage.appStoreUuid = uuid;
        }
        console.log("AppStoreUUID: "+uuid);
    }

    function renderAppStore() {
        var jQuery = '<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>';
        var css = ''+
            'html#main .nav li.heading { width: initial; margin-top: 20px; }'+
            '.preview { margin-right: 10px; width: 100px; height: 80px; background-color: #ddd; background-repeat: no-repeat; background-size: 100%; border: 1px solid #ccc; min-width: 100px; min-height: 90px; background-position: center; }'+
            '.preview .missing { filter: blur(5px); background-color: #ddd; position: relative; }'+
            '.preview .missing span { font-size: 6em; position: absolute; top: 33px; left: 25px; color: #ccc;}'+
            '.scripts { display: flex; flex-wrap: wrap; max-height: 330px; overflow-y: hidden; border-top: 1px solid #ccc; padding-top: 1em; }'+
            '.scripts.more { max-height: initial; }'+
            '.script { margin-right: 20px; display: flex; flex-grow: 0; flex-shrink: 0; width: 270px; height: 110px; }'+
            '.script a { color: initial; }'+
            '.script .install { margin-top: 9px; }'+
            '.script .install div { background-color: #dde; border-radius: 9px; font-size: 0.8em; padding-left: 4px; padding-right: 4px; padding-top: 2px; padding-bottom: 2px; text-align: center; width: 6em; }'+
            '.detail { width: 170px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }'+
            '.detail a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 170px; display: inline-block; }'+
            '.name { font-weight: bold; }'+
            '.author { } '+
            '.heart { color: indianred; }'+
            '.octicon { filter:opacity(50%); width: 100%; height: 100%; background-repeat: no-repeat; background-position: center; background-size: 35%; }'+
//            '.installed .octicon { background-image: url("https://wanikanitools-golang.curiousattemptbunny.com/static/octicons/bookmark.svg"); }'+
            '.installed .octicon { background-image: url("https://wanikanitools-golang.curiousattemptbunny.com/static/octicons/repo.svg"); }'+
            '.new .octicon { background-size: 40%; background-image: url("https://wanikanitools-golang.curiousattemptbunny.com/static/octicons/radio-tower.svg"); }'+
            '.top-charts .octicon { background-image: url("https://wanikanitools-golang.curiousattemptbunny.com/static/octicons/flame.svg"); }'+
            '.categories .octicon { background-size: 45%; background-image: url("https://wanikanitools-golang.curiousattemptbunny.com/static/octicons/book.svg"); }'+
            '.search .octicon { background-image: url("https://wanikanitools-golang.curiousattemptbunny.com/static/octicons/search.svg"); }'+
            '#search { display: none; }'+
            '.active_users.undetected { color:#ccc; }'+
            '#avatar { '+(localStorage.avatarStyle ? localStorage.avatarStyle : 'background-image: url("//www.gravatar.com/avatar/734f8eaa3fb9b256f2678ddb2ef89ea5.jpg?s=200&timestamp=11202017&d=https://cdn.wanikani.com/default-avatar-300x300-20121121.png"); display: block;')+' }'+
            'h3 { margin-bottom: 0.4em; margin-top: 2.5em; }'+
//            '.installs { float: right; }'+
            '';

        var html = '<html id="main"><head>'+localStorage.wanikanicss+jQuery+'<script src="https://unpkg.com/[email protected]/dist/umd/js-search.min.js"></script>'+'<style type="text/css">'+css+'</style>'+
            '<script>'+
            '    var api_key;'+
'    var globalVariables = {};'+
'    var scripts;'+
            ' var uuid;'+
                              renderPage.toString()+"\n"+
                              init.toString()+"\n"+
                              get_api_key.toString()+"\n"+
                              ajax_retry.toString()+"\n"+
                              storeGlobalVariables.toString()+"\n"+
            setUuid.toString()+"\n"+
                              "init();\n"+
            '</script>'+
                              '</head><body>Loading ...</body></html>';
        window.document.write(html);

        function renderPage() {
            var html = ''+
                '<div class="navbar navbar-static-top">'+
                '  <div class="navbar-inner">'+
                '    <div class="container">'+
                '      <ul class="nav">'+
                '        <li class="title">'+
                '          <a href="/dashboard">'+
                '            <span></span><span lang="ja">鰐蟹</span>'+
                '          </a>'+
                '        </li>'+
                '        <li class="heading">'+
                '          <h1>App Store</h1>'+
                '        </li>'+
                '        <li class="new">'+
                '          <a href="/script/appStore/new"><span><div class="octicon"></div></span><span>New</span></a>'+
                '        </li>'+
                '        <li class="top-charts">'+
                '          <a href="/script/appStore/top"><span><div class="octicon"></div></span><span>Top Charts</span></a>'+
                '        </li>'+
                '        <li class="categories">'+
                '          <a href="/script/appStore/categories"><span><div class="octicon"></div></span><span>Categories</span></a>'+
                '        </li>'+
                '        <li class="installed">'+
                '          <a href="/script/appStore/installed"><span><div class="octicon"></div></span><span>Installs</span></a>'+
                '        </li>'+
                '        <li class="search">'+
                '          <a href="/script/appStore/search"><span><div class="octicon"></div></span><span>Search</span></a>'+
                '        </li>'+
                '      </ul>'+
                '<ul class="nav pull-right">'+
                '  <li class="dropdown account" data-dropdown="">'+
                '    <a href="#" class="dropdown-toggle" data-toggle="dropdown">'+
                '      <span id="avatar">&nbsp;</span>Menu'+
                '      <i class="icon-chevron-down"></i>'+
                '    </a>'+
                '    <ul class="dropdown-menu">'+
                '      <li class="nav-header">'+
                '        Home'+
                '      </li><li>'+
                '        <a href="/dashboard">Dashboard</a>'+
                '      </li><li>'+
                '        <a href="https://community.wanikani.com">Community</a>'+
                '      <li class="app-store-menu-item-actual"><a href="/script/appStore">App Store</a></li><li class="nav-header">'+
                '        Account'+
                '      </li><li>'+
                '        <a href="/users/hitechbunny">Profile</a>'+
                '      </li><li>'+
                '        <a href="/settings/app">Settings</a>'+
                '      </li><li>'+
                '        <a href="/account/subscription">Subscription</a>'+
                '      </li><li>'+
                '        <a rel="nofollow" data-method="delete" href="/logout">Sign Out</a>'+
                '      </li>'+
                '    </ul>'+
                '  </li><li class="top" style="display: none;">'+
                '    <a><i class="icon-circle-arrow-up"></i></a>'+
                '  </li>'+
                '</ul>'+
                '    </div>'+
                '  </div>'+
                '</div>'+
                '<div id="search">'+
                '  <div class="container">'+
                '    <div class="row">'+
                '      <div class="span3"></div>'+
                '      <div class="span6">'+
                '        <form id="search-form" accept-charset="UTF-8" style="width:100%; margin-top:50px;">'+
                '          <span id="main-ico-search"><i class="icon-search"></i></span>'+
                '          <input type="text" name="query" id="query" class="search-query" style="width:100%; height:2em;">'+
                '        </form>'+
                '      </div>'+
                '      <div class="span3"></div>'+
                '    </div>'+
                '  </div>'+
                '</div>'+
                '<div style="margin-bottom: 100px;">'+
                '  <div class="container listings"></div>'+
                '</div>'+
                '<footer>'+
                '  <div class="container">'+
                '    <div class="row">'+
                '      <div class="span12">'+
                '        <ul>'+
                '          <li>'+
                '            <a href="/about">About</a>'+
                '          </li><li>'+
                '            <a href="https://wanikani.tumblr.com/">Blog</a>'+
                '          </li><li>'+
                '            <a href="/api">API</a>'+
                '          </li><li>'+
                '            <a href="/faq">FAQ</a>'+
                '          </li><li>'+
                '            <a target="_blank" href="/terms">Terms</a>'+
                '          </li><li>'+
                '            <a target="_blank" href="/privacy">Privacy</a>'+
                '          </li><li>'+
                '            <a href="/contact">Contact</a>'+
                '          </li><li>'+
                '            Copyright © Tofugu LLC, <span lang="ja">よ</span>'+
                '          </li>'+
                '        </ul>'+
                '      </div>'+
                '    </div>'+
                '  </div>'+
                '</footer>';

            $('body').html(html);
            $('.dropdown.account').click(function() { $('.dropdown.account').toggleClass('open'); });

            get_api_key().then(function() {
                ajax_retry('https://wanikanitools-golang.curiousattemptbunny.com/scripts?api_key='+api_key).then(function(json) {
                    storeGlobalVariables(json);
                    var appStoreInstalledScripts = JSON.parse(localStorage.appStoreInstalledScripts || '{}');
                    var installedOnOtherBrowsers = {};
                    Object.keys(json.browser_installs).forEach(function(browserUuid) {
                        if (browserUuid == uuid) {
                            return;
                        }

                        json.browser_installs[browserUuid].forEach(function(script) {
                            installedOnOtherBrowsers[script.name] = script;
                        });
                    });
                    (json.browser_installs[uuid] || []).forEach(function(script) {
                        delete installedOnOtherBrowsers[script.name];
                    });

                    var alreadyRendered = {};

                    var sections = [];
                    var page = window.location.pathname.split('/');
                    page = page[page.length-1];

                    var generalRanking = function(s) { return s.likes + (s.img_url ? 300 : 0) + (s.percentage_of_users > 0 ? 100*s.percentage_of_users : 0); };
                    if (page == 'top') {
                        sections.push(["Top Active Users", function(s) { return s.percentage_of_users; }, null, '']);
                        sections.push(["Top Likes", function(s) { return s.likes; }, null, '']);
                    } else if (page == 'search') {
                        var search = new JsSearch.Search('name');
                        search.addIndex('name');
                        search.addIndex('description');
                        search.addIndex('author');
                        search.addIndex('categories');
                        search.addDocuments(json.available_scripts);
                        $('#query').keyup(function(e) {
                            var query = $('#query').val();
                            if (query.length === 0) {
                                $('.scripts .script').show();
                            } else {
                                $('.scripts .script').hide();
                                var selector = search.search(query).map(function(s) { return "#script-"+s.topic_id; }).join(",");
                                $(selector).show();
                            }
                        });

                        sections.push(["Search Results", generalRanking, null, 'more']);
                        $('#search').show();
                    } else if (page == 'installed') {
                        sections.push(['Installed', function(s) { return s.topic_id; }, appStoreInstalledScripts, 'more']);
                        sections.push(['Installed On Your Other Browser(s)', function(s) { return s.topic_id; } , installedOnOtherBrowsers, 'more']);
                    } else if (page == 'categories') {
                        var maps = {
                            'level-overview': {},
                            'lessons': {},
                            'reviews': {},
                            'dashboard': {},
                            'community': {},
                            'other': {}
                        };
                        json.available_scripts.forEach(function(script) {
                            console.log(script);
                            if (script.categories.length === 0) {
                                maps.other[script.name] = script;
                            } else {
                                script.categories.forEach(function(category) {
                                    maps[category] = maps[category] || {};
                                    maps[category][script.name] = script;
                                });
                            }
                        });
                        sections.push(['Dashboard', generalRanking, maps.dashboard, '']);
                        sections.push(['Reviews', generalRanking, maps.reviews, '']);
                        sections.push(['Lessons', generalRanking, maps.lessons, '']);
                        sections.push(['Levels Overviews', generalRanking, maps['level-overview'], '']);
                        sections.push(['Forum', generalRanking, maps.community, '']);
                        sections.push(['Other', generalRanking, maps.other, '']);
                    //} else if (page == 'new') {
                    } else {
                        sections.push(["New Releases", function(s) { return s.topic_id; }, null, '']);
                    }

                    sections.forEach(function(category) {
                        var categoryFilter = category[2];
                        var html = '<h3>'+category[0]+'</h3><div class="scripts '+category[3]+'">';

                        json.available_scripts.sort(function(a,b) { return category[1](b) - category[1](a); });
                        var i = 0;
                        var number = 0;
                        var renderCount = 0;
                        var renderMax = category[3] !== '' ? 1000 : 12;
                        while(renderCount < renderMax && i < json.available_scripts.length) {
                            var s = json.available_scripts[i];
                            i += 1;
                            if (categoryFilter && !categoryFilter[s.name]) {
                                continue;
                            }
                            number += 1;
                            renderCount += 1;
                            alreadyRendered[s.topic_id] = true;
                            html +=
                                '<div class="script" title="'+s.name.replace('"', '')+'" id="script-'+(s.topic_id)+'">';
                            html += '<a href="'+s.topic_url+'">';
                            if (s.img_url) {
                                html +=
                                    '   <div class="preview" style="background-image: url('+s.img_url+'");"/>';
                            } else {
                                html +=
                                    '   <div class="preview"><div class="missing"><span>?</span></div></div>';
                            }
                            html += '</a>';

                            html +=
                                '   <div class="detail">'+
                                '     <a href="'+s.topic_url+'">'+
                                '     <span class="name">'+(category[0] == "All..." || category[0] == 'Installed' || category[0] == 'Installed On Your Other Browser(s)' ? '' : (number+'. '))+s.name.replace('WaniKani:', '').replace('WaniKani', '').replace('WK', '').replace('Wanikani', '')+'</span><br>'+
                                '     <span class="likes">'+s.likes+'</span>&nbsp;<span class="heart">❤</span><br>';

                            if (s.percentage_of_users === 0) {
                                html += '<span class="active_users undetected">no app store hook</span><br>';
                            } else {
                                html += '     <span class="active_users">'+Math.round(s.percentage_of_users)+'% of users</span>';
                            }
                            html += '</a><br>';

                            if (s.percentage_of_users !== 0) {
                                if (appStoreInstalledScripts[s.name]) {
                                    html += '<a href="'+s.script_url+'" class="install installed"><div>INSTALLED</div></a>';
                                } else {
                                    html += '<a href="'+s.script_url+'" class="install uninstalled"><div>GET</div></a>';
                                }
                            } else {
                                html += '<a href="'+s.topic_url+'" class="install fourm"><div>FORUM</div></a>';
                            }

                            //html += '     <span class="author">'+s.author+'</span><br>';
                            html += '   </div>'+
                                '';

                            html +=
                                '</div>';

                            console.log(html);
                        }

                        html += '</div>';
                        if (html.indexOf("preview") != -1) {
                            $('.listings').append(html);
                        }
                    });
                });
            });
        }

        function init() {
          console.log("init started");
          setUuid();
          function waitForJquery() {
              if (typeof($) == 'function') {
                  renderPage();
              } else {
                  console.log('Waiting for jquery...');
                  setTimeout(waitForJquery, 100);
              }
          }

          console.log("Wait for jquery:");
          setTimeout(waitForJquery, 0);
        }
    }

    var nextTrackScripts = 200;
    function trackScripts() {
        var installedNewScript = false;
        var lastSeen = parseInt(localStorage.appStoreInstalledScriptsLastTransmission || 0+"");
        var appStoreInstalledScripts = JSON.parse(localStorage.appStoreInstalledScripts || '{}');
//        console.log("Installed:");
        Object.getOwnPropertyNames(window.appStoreRegistry || {}).forEach(function(appKey) {
//            console.log("\t"+appKey);
            var gminfo = window.appStoreRegistry[appKey];
            var fingerprint = {};
            fingerprint.version = gminfo.script.version;
            fingerprint.author = gminfo.script.author;
            fingerprint.description = gminfo.script.description;
            fingerprint.includes = gminfo.script.includes;
            fingerprint.name = gminfo.script.name;
            fingerprint.uuid = gminfo.script.uuid;
            fingerprint.lastSeenInstalled = Date.now();

            var key = fingerprint.name; //fingerprint.updateUrl ? fingerprint.updateUrl : fingerprint.author+"|"+fingerprint.name;
//            console.log(fingerprint);
//            console.log(gminfo);

            if (!appStoreInstalledScripts[key]) {
                console.log("Installed! "+key);
                installedNewScript = true;
            }
            appStoreInstalledScripts[key] = fingerprint;
        });

        function addScriptsByGlobalVariables(vars) {
            console.log("Found global variable: "+vars+"!");

            vars.forEach(function(v) {
                var name = globalVariables[v];
                globalVariables[v] = false; // avoid repeated detection
                if (!appStoreInstalledScripts[name]) {
                    scripts.forEach(function(script) {
                        if (script.name == name) {
                            console.log("Installed! "+name);
                            var fingerprint = {};
                            fingerprint.name = name;
                            fingerprint.lastSeenInstalled = Date.now();
                            appStoreInstalledScripts[name] = fingerprint;
                        }
                    });
                }
            });
        }

        var foundVariables = Object.keys(window).filter(function(key) {
            return globalVariables[key] && !appStoreInstalledScripts[globalVariables[key]];
        });

        if (foundVariables.length > 0) {
            console.log(foundVariables);

            if (scripts) {
                addScriptsByGlobalVariables(foundVariables);
                remainder();
            } else {
                get_api_key().then(function() {
                    ajax_retry('https://wanikanitools-golang.curiousattemptbunny.com/scripts?api_key='+api_key).then(function(json) {
                        scripts = json.available_scripts;
                        addScriptsByGlobalVariables(foundVariables);
                        remainder();
                    });
                });
            }
        } else {
            remainder();
        }

        function remainder() {
            localStorage.appStoreInstalledScripts = JSON.stringify(appStoreInstalledScripts);
            if (nextTrackScripts < 60000) {
                setTimeout(trackScripts, nextTrackScripts);
            }
            nextTrackScripts *= 4;

            if (installedNewScript || Date.now() - lastSeen > 1000 * 60 * 60 * 12) {
                get_api_key().then(function() {
                    ajax_retry("https://wanikanitools-golang.curiousattemptbunny.com/scripts/installed?api_key="+api_key+"&browser_uuid="+uuid, {retries: 1, method: 'POST', data: JSON.stringify({installed: appStoreInstalledScripts})}).then(function() {
                        localStorage.appStoreInstalledScriptsLastTransmission = Date.now()+"";
                    }, function(error) {
                        console.log("App Store failed to transmit installed scripts.");
                    });
                });
            }
        }
    }

    function recordCss() {
        var elements = $('head link[rel="stylesheet"]');
        var css = '';
        for(var i=0; i<elements.length; i++) {
            css += elements[i].outerHTML;
        }
        localStorage.wanikanicss = css;

        var avatarStyle = $('.dropdown.account a span').attr('style');
        if (avatarStyle) {
            localStorage.avatarStyle = avatarStyle;
        }
    }

    function storeGlobalVariables(json) {
        scripts = json.available_scripts;
        var globalVariables = {};
        json.available_scripts.forEach(function(script) {
            script.global_variables.forEach(function(k) {
                globalVariables[k] = script.name;
            });
        });
        localStorage.appStoreGlobalVariables = JSON.stringify(globalVariables);
    }

    //-------------------------------------------------------------------
    // Fetch a document from the server.
    //-------------------------------------------------------------------
    function ajax_retry(url, options) {
        //console.log(url, retries, timeout);
        options = options || {};
        var retries = options.retries || 3;
        var timeout = options.timeout || 3000;
        var headers = options.headers || {};
        var method = options.method || 'GET';
        var data = options.data || undefined;
        var cache = options.cache || false;

        function action(resolve, reject) {
            $.ajax({
                url: url,
                method: method,
                timeout: timeout,
                headers: headers,
                data: data,
                cache: cache
            })
            .done(function(data, status){
                //console.log(status, data);
                if (status === 'success') {
                    resolve(data);
                } else {
                    //console.log("done (reject)", status, data);
                    reject();
                }
            })
            .fail(function(xhr, status, error){
                //console.log(status, error);
                if ((status === 'error' || status === 'timeout') && --retries > 0) {
                    //console.log("fail", status, error);
                    action(resolve, reject);
                } else {
                    reject();
                }
            });
        }
        return new Promise(action);
    }

    function get_api_key() {
        return new Promise(function(resolve, reject) {
            api_key = localStorage.getItem('apiKey_v2');
            if (typeof api_key === 'string' && api_key.length == 36) return resolve();

            // status_div.html('Fetching API key...');
            ajax_retry('/settings/account').then(function(page) {

                // --[ SUCCESS ]----------------------
                // Make sure what we got is a web page.
                if (typeof page !== 'string') {return reject();}

                // Extract the user name.
                page = $(page);

                // Extract the API key.
                api_key = page.find('#user_api_key_v2').attr('value');
                if (typeof api_key !== 'string' || api_key.length !== 36) {
                    return reject(new Error('generate_apikey'));
                }

                localStorage.setItem('apiKey_v2', api_key);
                resolve();

            },function(result) {
                // --[ FAIL ]-------------------------
                reject(new Error('Failed to fetch API key!'));

            });
        });
    }
})();