text: // ==UserScript==
  // @name         Wanikani Forum: Regular Tracker
  // @namespace    http://tampermonkey.net/
  // @version      1.1.4
  // @description  Tracks how regular you are
  // @author       Kumirei
  // @include      *community.wanikani.com*
  // @grant        none
  // ==/UserScript==
  
  ;(function () {
      // Settings object. Configure your settings here
      let settings = {
          update_interval: 10, // Minutes between fetches of the summary page data
      }
  
      // Global tracker object. Keeps track of your efforts of meeting the regularity criteria in the last 100 days
      let tracker = {
          last_fetch: 0, // Timestamp of when summary and category data was last fetched
          history: [], // Contains all fetches of the summary and category data going back 100 days
          streak: [0, 0], // Last date visited and the current streak
          visited: [], // List of visited dates [date1, date2, ...]
          unique_topics: 0, // Not interested in going through the trouble of tracking this right now
          flags_received: 0, // No way to detect this as far as I am aware
          suspended: false, // No way to detect this as far as I am aware
          regular: false, // Boolean indicating whether you have regular status
      }
  
      const msday = 1000 * 60 * 60 * 24 // Number of ms in a day
      init()
  
      // First run setup
      function init() {
          add_display()
          add_css()
          fetch_data()
          setInterval(fetch_data, (1000 * 60 * settings.update_interval) / 10)
      }
  
      // Fetches the needed data
      function fetch_data() {
          update_tracker()
          if (tracker.last_fetch < Date.now() - 1000 * 60 * settings.update_interval) {
              tracker.last_fetch = Date.now()
              save()
              let username = $('#current-user a').attr('href').split('/u/')[1]
              let summary_url = 'https://community.wanikani.com/u/' + username + '/summary'
              let stats_url = 'https://community.wanikani.com/about'
              Promise.all([fetch(summary_url), fetch(stats_url)]).then(process_data)
          }
      }
  
      function fetch(url) {
          return $.ajax({ url: url, type: 'GET', dataType: 'json', cache: false })
      }
  
      // Updates the global variable with the new data
      function process_data(data) {
          let [summary_data, about] = data
          let s = summary_data.user_summary
          let stats = about.about.stats
          let history = tracker.history
          if (history.length > 1 && history[history.length - 2].date > Date.now() - 60 * 60 * 1000) history.pop() // Limit to hourly updates
          tracker.history.push({
              date: Date.now(),
              likes_given: s.likes_given,
              likes_received: s.likes_received,
              topics_viewed: s.topics_entered,
              posts_read: s.posts_read_count,
              days_visited: s.days_visited,
              total_topics: stats.topic_count,
              total_posts: stats.post_count,
              topics_30d: stats.topics_30_days,
              posts_30d: stats.posts_30_days,
          })
          tracker.regular = summary_data.badges[0].id == 3 // Regular has badge ID 3
          const get_date = (date) => new Date(date).toISOString().slice(0, 10)
          tracker.visited.push(get_date(Date.now())) // Append today's date
          tracker.visited.sort()
          tracker.visited = tracker.visited.filter((d, i) => {
              // filter out old visits
              if (!tracker.visited[i + 1]) return true // Keep last
              const date = new Date(d) // Get next day
              date.setDate(date.getDate() + 1)
              return get_date(date.getTime()) == get_date(tracker.visited[i + 1])
          })
          tracker.streak = [null, tracker.visited.length]
          save()
          update_display()
      }
  
      // Update the tracker variable
      function update_tracker() {
          var stored = JSON.parse(localStorage.getItem('WKFRT'))
          if (stored) {
              for (let key in stored) tracker[key] = stored[key]
          }
      }
  
      // Stores the data in localStorage so other tabs can access it
      function save() {
          localStorage.setItem('WKFRT', JSON.stringify(tracker))
      }
  
      // Adds the bubbles to the header
      function add_display() {
          // START code by rfindley
          if (is_dark_theme()) {
              $('body').attr('theme', 'dark')
          } else {
              $('body').attr('theme', 'light')
          }
          var 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.
          var top_menu = $('.d-header')
          var main_content = $('#main-outlet')
          $('body').addClass('float_wkappnav')
          wk_app_nav.addClass('wanikani-app-nav-container')
          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.
          var main_content_toppad = Number(main_content.css('padding-top').match(/[0-9]*/)[0])
          main_content.css('padding-top', main_content_toppad + 25 + 'px')
          // Insert CSS.
          var css =
              '.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 {color:#ddd; 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;}'
          $('head').append('<style type="text/css">' + css + '</style>')
          // END code by rfindley
          if (!$('#regular_status').length) {
              $('.wanikani-app-nav ul').append(
                  '<li id="regular_status" data-highlight="false">Regular Status:<span class="dashboard_bubble">0%</span></li>',
              )
              update_display()
          }
      }
  
      // Function made by rfindley
      function 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
          )
      }
  
      // Updates the display with the current numbers
      function update_display() {
          fetch_data()
          update_tracker()
          prune_history()
          save()
          let elem = $('#regular_status')[0]
  
          if (tracker.history.length != 0 && elem) {
              let days_visited = last100('days_visited') + 1
              let likes_given = last100('likes_given')
              let likes_received = last100('likes_received')
              let topics_viewed = last100('topics_viewed')
              let days_tracked = Math.floor((Date.now() - tracker.history.firstObject.date) / msday)
              let topics_goal = Math.round(last100('total_topics') * 0.25)
              let posts_read = last100('posts_read')
              let post_goal = Math.round(last100('total_posts') * 0.25)
              if (days_tracked < 100) {
                  topics_goal = Math.round((topics_goal / days_tracked) * 100) || 0
                  post_goal = Math.round((post_goal / days_tracked) * 100) || 0
                  if (days_tracked < 30) {
                      topics_goal = Math.round((tracker.history.lastObject.topics_30d / 0.3) * 0.25)
                      post_goal = Math.round((tracker.history.lastObject.posts_30d / 0.3) * 0.25)
                  }
              }
              if (topics_goal > 500) topics_goal = 500
              if (post_goal > 20000) post_goal = 20000
              let total_days_visited = tracker.history.lastObject.days_visited
              let visit_streak = tracker.streak[1]
              let title = `
  In the last 100 days
  –––––––––––––––––––––––––––––
  Days Visited:            ${days_visited} / 50 (${Math.round((100 * days_visited) / 50)}%)
  Posts Read:                ${posts_read.toLocaleString()} / ${post_goal.toLocaleString()} (${Math.round(
                  (100 * posts_read) / post_goal,
              )}%)
  Topics Viewed:         ${topics_viewed.toLocaleString()} / ${topics_goal} (${Math.round(
                  (100 * topics_viewed) / topics_goal,
              )}%)
  Likes Given:              ${likes_given.toLocaleString()} / 30
  Likes Received:        ${likes_received.toLocaleString()} / 20
  –––––––––––––––––––––––––––––
  Topics Posted In:      ??? / 10
  Flags Not Received: ??? / 5
  Not Suspended:       ???
  –––––––––––––––––––––––––––––
  Visit Streak:               ${visit_streak}`
              elem.title = title
  
              let goals = [
                  days_visited / 50,
                  likes_given / 30,
                  likes_received / 20,
                  topics_viewed / topics_goal,
                  posts_read / post_goal,
              ].reduce((a, b) => {
                  return b < a ? b : a
              })
              elem.children[0].innerText = Math.floor(goals * 100) + '%'
              elem.setAttribute('data-highlight', tracker.regular)
          }
      }
  
      // Deletes any data older than 24 hours
      function prune_history() {
          $(tracker.history).each((i, item) => {
              if (item.date < Date.now() - msday * 100) tracker.history.splice(0, 1)
              else return false
          })
      }
  
      // Add CSS which makes space in the header
      function add_css() {
          $('head').append(`
  <style id="WKFRTCSS">
  .wanikani-app-nav .wanikani-app-nav-list-header {display: none;}
  .wanikani-app-nav > ul {display: flex;}
  .wanikani-app-nav #regular_status {order: 4;}
  .wanikani-app-nav > ul > li:last-child {margin-right: 1em;}
  </style>`)
      }
  
      function last100(key) {
          return dict_key_diff(tracker.history.lastObject, tracker.history.firstObject, key)
      }
  
      // Returns the difference between entries in two dicts
      function dict_key_diff(d1, d2, key) {
          return d1[key] - d2[key]
      }
  
      // Waits until a certain element is created then passes it to the callback function
      async function wait_for_selector(selector, callback, interval = 1000, max_wait = 0, count = 0) {
          let wait_id = Math.random().toString().slice(2)
          let waited = 0
          let found = 0
          let result
          let result_handler = function (e) {
              if (found == count && count != 0) return
              found++
              e.setAttribute('wait_id_' + wait_id, '')
              callback(e)
          }
          while ((waited < max_wait || max_wait == 0) && (found < count || count == 0)) {
              result = document.querySelectorAll(selector + ':not([wait_id_' + wait_id + '])')
              result.forEach(result_handler)
              await sleep(interval)
          }
      }
  
      // Waits for a specified number of ms before returning
      function sleep(ms) {
          return new Promise((r) => setTimeout(r, ms))
      }
  })()
  