// ==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×tamp=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"> </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> <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!'));
});
});
}
})();