WaniKani App Store

Never miss an awesome WaniKani script again!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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!'));

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