返回首頁 

Greasy Fork is available in English.

WaniKani Forums: Like counter

Keeps track of the likes you've used and how many you have left... supposedly.

// ==UserScript==// @name         WaniKani Forums: Like counter// @namespace    http://tampermonkey.net/// @version      3.1.12// @description  Keeps track of the likes you've used and how many you have left... supposedly.// @author       Kumirei// @include      https://community.wanikani.com*// @grant        none// ==/UserScript==;(function ($) {// SETTINGSconst settings = {update_interval: 10, // Interval (minutes) for fetching summary page data and likeslifetime_purple: false, // Set to true for purple info bubbleshideReceived: false,}function getUsername() {return JSON.parse(JSON.parse(document.querySelector('#data-preloaded').dataset.preloaded).currentUser).username}// Global variablelet LC = {stored: {zero: [],full: [],received: [],summary: {last_update: '1970-01-01T00:00:00.000Z',likes_given: 0,likes_received: 0,days_visited: 0,max: 400, // 400 is default for *regulars*},day: {given: [],received: 0,},},elems: {received: null,given: null,next: null,},}// Update LCPromise.all([update_stored(), update_summary()]).then(update)setInterval(update_all, settings.update_interval * 60 * 1000, LC)update_all()// Update the next like timer every secondsetInterval(update_next, 1000)// Installadd_CSS()add_display()// Update every time a like is used$('body').on('click', '.post-stream .toggle-like', update)// Updates everythingasync function update(event) {// Update displayed count immediatelyif (event.type) {const old_count = Number(LC.elems.given.children().text())const new_count = old_count + ($(event.target).closest('.widget-button').hasClass('has-like') ? 1 : -1)LC.elems.given.children().text(new_count)}}// Fetches the data of LC.stored from localStoragefunction update_stored() {LC.stored = Object.assign(LC.stored, JSON.parse(localStorage.getItem('LCstored')) || {})}// Saves the LC.stored data to localStoragefunction save_stored() {localStorage.setItem('LCstored', JSON.stringify(LC.stored))}// Updates summary info and likes used/receivedasync function update_all() {update_stored()const now = new Date(Date.now() - settings.update_interval * 60 * 1000).toISOString()if (LC.stored.summary.last_update < now) {//alert('Updating' + new Date().toISOString() + '\n\n' + LC.stored.summary.last_update)await update_summary()await update_day()save_stored()update_display()}}// Updates the LC variable with info from the summary pageasync function update_summary() {const username = getUsername()const f = await fetch(`https://community.wanikani.com/u/${username}/summary`, {headers: {accept: 'application/json, text/javascript, */*; q=0.01','x-requested-with': 'XMLHttpRequest',},})if (f.status === 200) {const data = await f.json()const max = 100 * (1 + data.badges[0].id)const { likes_given, likes_received, days_visited } = data.user_summaryLC.stored.summary = {likes_given,likes_received,days_visited,max,last_update: new Date().toISOString(),}const now = Date.now()LC.stored.received.push([now, likes_received])LC.stored.received = LC.stored.received.filter((a) => a[0] >= now - 24 * 60 * 60 * 1000)} else console.warn(`[LIKE COUNTER] Error ${f.status}: There was an error fetching user summary`)}// Updates the likes given and received in the last 24 hoursasync function update_day() {const msday = 24 * 60 * 60 * 1000const now = Date.now()const username = getUsername()const summary = LC.stored.summaryconst day = LC.stored.dayday.given = (await fetch_likes(username, 1, 24)).reverse()//day.received = (await fetch_likes(username, 2, 24)).reverse()day.received = LC.stored.received[LC.stored.received.length - 1][1] - LC.stored.received[0][1] || 0if (day.given.length === summary.max && (LC.stored.zero[LC.stored.zero.length - 1] || 0) < now - msday) {LC.stored.zero.push(now)}if (day.given.length === 0 && (LC.stored.full[LC.stored.full.length - 1] || 0) < now - msday) {LC.stored.full.push(now)}}// Fetches likes from Discourse apiasync function fetch_likes(username, actionType, hoursBack) {const time = new Date(Date.now() - hoursBack * 60 * 60 * 1000).toISOString()let offset = 0let fetched = []let keep_fetching = truewhile (keep_fetching) {const f = await fetch(`https://community.wanikani.com/user_actions.json?offset=0&username=${username}&filter=${actionType}&offset=${offset}`,)if (f.status !== 200) {console.warn(`[LIKE COUNTER] Error ${f.status}: There was an error fetching user likes`)break}const actions = (await f.json()).user_actionsfor (let item of actions) {const date = item.created_atif (date >= time) {fetched.push(Date.parse(item.created_at))} else {keep_fetching = falsebreak}}offset += 30}return fetched}// Adds the bubbles to the headerfunction add_display() {// START code by rfindleyif (is_dark_theme()) $('body').attr('theme', 'dark')else $('body').attr('theme', 'light')// Wait for the navconst wk_app_nav = $('.wanikani-app-nav').closest('.container')if (wk_app_nav.length === 0) {setTimeout(add_display, 200)return}// Attach the Dashboard menu to the stay-on-top menu.const top_menu = $('.d-header')const main_content = $('#main-outlet')$('body').addClass('float_wkappnav')wk_app_nav.addClass('wanikani-app-nav-container')wk_app_nav.find('li').each((_, el) => {const $el = $(el)if (!$el.attr('data-name')) {$el.attr('data-name', 'original')}})top_menu.find('>.wrap > .contents:eq(0)').after(wk_app_nav)// Adjust the main content's top padding, so it won't be hidden under the new taller top menu.const main_content_toppad = Number(main_content.css('padding-top').match(/[0-9]*/)[0])main_content.css('padding-top', main_content_toppad + 25 + 'px')// END code by rfindleyLC.elems = {// dashboard: $(//     '<li class="show-on-small-screen">' +//     '<a href="https://www.wanikani.com" target="_blank" rel="noopener noreferrer">WaniKani</a>' +//     '</li>',// ),received: $('<li data-highlight="true" data-name="likes-received"' +(settings.hideReceived ? ' style="display:none"' : '') +'>Likes Received<span id="likes_received" class="dashboard_bubble">0</span></li>',),given: $('<li data-highlight="true" data-name="likes-left">Likes Left<span id="likes_given" class="dashboard_bubble">0</span></li>',),next: $('<li data-highlight="true" data-name="likes-next">Next Like<span id="next_like" class="dashboard_bubble">0</span></li>',),}$('.wanikani-app-nav ul').append([LC.elems.received, LC.elems.given, LC.elems.next])update_display()}// Updates the counts and the hover infofunction update_display() {const msday = 24 * 60 * 60 * 1000const msh = msday / 24const now = Date.now()const { received, given, next } = LC.elemsconst summary = LC.stored.summaryconst day = LC.stored.day// Update countsreceived.children().text(day.received)given.children().text(summary.max - day.given.length)next.children().text(time_left(day.given[0] + msday))day.given.length < summary.max ? $('body').removeClass('no-likes') : $('body').addClass('no-likes')// Update hover inforeceived.attr('title',`${day.received.toLocaleString()} likes received in past 24h` +`\n${Math.round(summary.likes_received / summary.days_visited,).toLocaleString()} likes received per day visited` +`\n${summary.likes_received.toLocaleString()} total likes received`,)given.attr('title',`${day.given.length.toLocaleString()} likes given in past 24h` +`\n${Math.round(summary.likes_given / summary.days_visited,).toLocaleString()} likes given per day visited` +`\n${summary.likes_given.toLocaleString()} total likes given` +`\n\n${LC.stored.zero.length.toLocaleString()} times have you ran out` +`\n${comma(Math.floor((now - (LC.stored.zero[LC.stored.zero.length - 1] || now)) / msday),)} days since you last ran out` +`\n\n${LC.stored.full.length.toLocaleString()} times have you had full likes` +`\n${comma(Math.floor((now - (LC.stored.full[LC.stored.full.length - 1] || now)) / msday),)} days since you last had full likes`,)let hours = Array(24).fill(0).map((_, i) =>day.given.filter((like) => like + msday > now + i * msh && like + msday < now + (i + 1) * msh).length,)const next_like = new Date(day.given[0] + msday)next.attr('title',`Next like at ${next_like.getHours()}:${(next_like.getMinutes() < 10 ? '0' : '') + next_like.getMinutes()}` + `\n\nLikes replenishing in ${hours.reduce((a, c, i) => (c == 0 ? a : `${a}\n${i + 1}h: ${c}`), ``)}`,)}function comma(n) {return n.toLocaleString('en-US')}// Update the timer for the next likefunction update_next() {const msday = 24 * 60 * 60 * 1000const yesterday = Date.now() - msdayconst day = LC.stored.dayconst given = day.given.lengthday.given = day.given.filter((t) => t > yesterday)// If likes have been used or expired update whole displayif (given !== day.given.length) update_display()// Else just update the timerelse LC.elems.next.children().text(time_left(day.given[0] + msday))}// Adds the CSSfunction add_CSS() {let bubble_color = settings.lifetime_purple ? 'rgb(213, 128, 255)' : '#6cf'$('head').append('    <style id=like_counter>' +'    body[theme="dark"] .wanikani-app-nav ul li {color:#999;}' +'    li[data-highlight="true"] span.dashboard_bubble {background-color: ' +bubble_color +' !important;}' +'    bbody.no-likes .like > .fa.d-icon-d-unliked {color: red !important}' +'    .wanikani-app-nav > ul {display: flex;}' +'    .wanikani-app-nav li[data-name="likes-received"] {order: 1;}' +'    .wanikani-app-nav li[data-name="likes-left"] {order: 2;}' +'    .wanikani-app-nav li[data-name="likes-next"] {order: 3;}' +'    .wanikani-app-nav li[data-name="lesson_count"],' +'    .wanikani-app-nav li[data-name="review_count"],' +'    .wanikani-app-nav li[data-name="next_review"] {order: 0;}' +'    .float_wkappnav .d-header {padding-bottom: 2em;}' +'    .float_wkappnav .d-header {height: 4em !important;}' +'    .float_wkappnav .d-header .title {height:4em;}' +'    .float_wkappnav .wanikani-app-nav-container {border-top:1px solid #ccc; line-height:2em;}' +'    .float_wkappnav .wanikani-app-nav ul {padding-bottom:0; margin-bottom:0; border-bottom:inherit;}' +'    .dashboard_bubble {color:#fff; background-color:#bdbdbd; font-size:0.8em; border-radius:0.5em; padding:0 6px; margin:0 0 0 4px; font-weight:bold;}' +'    li[data-highlight="true"] .dashboard_bubble {background-color:#6cf;}' +'    body[theme="dark"] .dashboard_bubble {background-color:#646464;}' +'    body[theme="dark"] li[data-highlight="true"] .dashboard_bubble {color:#000; background-color:#6cf;}' +'    body[theme="dark"] .wanikani-app-nav[data-highlight-labels="true"] li[data-highlight="true"] a {color:#6cf;}' +'    body[theme="dark"] .wanikani-app-nav ul li a {color:#999;}' +'</style>',).append('<style id="responsive-wanikani-app-nav-list-header">' +'.wanikani-app-nav .show-on-small-screen {display: none;}' +'@media screen and (max-width: 799px) {' +'    .wanikani-app-nav {margin-top: 0; float: right;}' +'    .wanikani-app-nav .hide-on-small-screen,' +'    .wanikani-app-nav li[data-name="original"] {display: none !important;}' +'    .wanikani-app-nav .show-on-small-screen {display: block !important;}' +'}' +'</style>',)}// Returns a string with the time remaining until the given datefunction time_left(date) {if (!date) return 'N/A'const seconds = (date - Date.now()) / 1000const s = Math.floor((seconds % 3600) % 60)const sr = Math.round((seconds % 3600) % 60)const m = Math.floor(((seconds - s) / 60) % 60)const mr = Math.round(((seconds - s) / 60) % 60)const h = Math.floor((seconds - s - m * 60) / 3600)const hr = Math.round((seconds - s - m * 60) / 3600)if (h != 0) return hr + 'h'if (m != 0) return mr + 'm'if (s != 0) return sr + 's'}// Checks whether a dark theme is usedfunction is_dark_theme() {// Grab the <html> background color, average the RGB.  If less than 50% bright, it's dark theme.return ($('html').css('background-color').match(/\((.*)\)/)[1].split(',').slice(0, 3).map((str) => Number(str)).reduce((a, i) => a + i) /(255 * 3) <0.5)}})(window.jQuery)