Wanikani: Review Cache

Stores reviews for other scripts

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/410909/1470754/Wanikani%3A%20Review%20Cache.js

  1. // ==UserScript==
  2. // @name Wanikani: Review Cache
  3. // @version 1.2.10
  4. // @description Manages a cache of all the user's reviews
  5. // @author Kumirei
  6. // @include *wanikani.com*
  7. // @grant none
  8. // ==/UserScript==
  9.  
  10. ;(function (wkof) {
  11. // Manually increment to initiate reload for all users
  12. const cache_version = 1
  13.  
  14. // Script version. Starts with q to make it larger than numerical versions
  15. const version = 'q1.2.10'
  16.  
  17. // Update interval for subscriptions
  18. const update_interval = 10 // minutes
  19.  
  20. // Reveal functions to window
  21. if (!window.review_cache?.version || window.review_cache.version < version) {
  22. const _subscribers = window.review_cache?._subscribers ? window.review_cache?._subscribers : new Set()
  23. const _fetching = window.review_cache?._fetching ? window.review_cache?._fetching : null
  24. if (window.review_cache?._reviewListener)
  25. window.removeEventListener('didCompleteSubject', window.review_cache?._reviewListener)
  26. window.review_cache = {
  27. get_reviews,
  28. reload,
  29. subscribe,
  30. unsubscribe,
  31. insert,
  32. silent: true, // Hide popup_delay messages in the console
  33. version: version,
  34. _subscribers,
  35. _fetching,
  36. _reviewListener: null,
  37. }
  38. }
  39.  
  40. // Listens for completed reviews. Temporary solution while the reviews API is not available
  41. const item_srs = {}
  42.  
  43. wkof.include('ItemData')
  44. wkof.ready('ItemData').then(async () => {
  45. const items = await wkof.ItemData.get_items('assignments')
  46. for (let item of items) item_srs[item.id] = item.assignments?.srs_stage
  47. })
  48.  
  49. set_update_interval()
  50. set_review_listener()
  51.  
  52. function set_review_listener() {
  53. const callback = async (event) => {
  54. if (window.location.pathname !== '/subjects/review') return // Only count reviews, not lessons or extra study
  55. await wkof.ready('ItemData')
  56. const { stats, subject } = event.detail.subjectWithStats
  57. window.review_cache.insert([
  58. [Date.now(), subject.id, item_srs[subject.id], stats.meaning.incorrect, stats.reading.incorrect],
  59. ])
  60. }
  61. window.addEventListener('didCompleteSubject', callback)
  62. window.review_cache._reviewListener = callback
  63. }
  64.  
  65. // Add a subscriber
  66. async function subscribe(subscriber) {
  67. window.review_cache._subscribers.add(subscriber)
  68. const cached = await load_data()
  69. subscriber?.(cached.reviews)
  70. const reviews = await get_reviews()
  71. if (cached.reviews.length !== reviews.length) subscriber?.(reviews)
  72. }
  73.  
  74. // Remove a subscriber
  75. function unsubscribe(subscriber) {
  76. return window.review_cache._subscribers.delete(subscriber)
  77. }
  78.  
  79. // Automatically update every 10 minutes
  80. function set_update_interval() {
  81. get_reviews(false)
  82. setInterval(() => {
  83. if (window.review_cache._subscribers.size) get_reviews(true)
  84. }, update_interval * 60_000)
  85. }
  86.  
  87. // Fetch reviews from storage
  88. async function get_reviews(disable_popup = false) {
  89. wkof.include('Apiv2')
  90. if (!window.review_cache._fetching) {
  91. window.review_cache._fetching = wkof
  92. .ready('Apiv2')
  93. .then(load_data)
  94. .then((data) => update_data_after_session(data, disable_popup))
  95. }
  96. const data = await window.review_cache._fetching
  97. if (data.changed) {
  98. for (let subscriber of window.review_cache._subscribers) subscriber?.(data.reviews)
  99. }
  100. window.review_cache._fetching = null
  101. return data.reviews
  102. }
  103.  
  104. // Deletes cache and re-fetches reviews
  105. function reload() {
  106. return wkof.file_cache.delete('review_cache').then(get_reviews)
  107. }
  108.  
  109. async function insert(reviews) {
  110. const cached = await load_data()
  111. const newestDate = reviews.reduce((max, cur) => Math.max(cur[0], max), 0)
  112. const updated = {
  113. cache_version,
  114. date: new Date(newestDate).toISOString(),
  115. reviews: cached.reviews.concat(reviews).sort((a, b) => a[0] - b[0]),
  116. }
  117. for (let subscriber of window.review_cache._subscribers) subscriber?.(updated.reviews)
  118. await save(updated)
  119. }
  120.  
  121. // Loads data from cache
  122. function load_data() {
  123. return wkof.file_cache.load('review_cache').then(decompress, (_) => {
  124. return { cache_version, date: '1970-01-01T00:00:00.000Z', reviews: [] }
  125. })
  126. }
  127.  
  128. // Save cache
  129. function save(data) {
  130. return wkof.file_cache.save('review_cache', compress(data)).then((_) => data)
  131. }
  132.  
  133. // Compress and decompress the dates for better use of storage space.
  134. // Dates are stored as time elapsed between items, but are returned as absolute dates
  135. function compress(data) {
  136. return press(true, data)
  137. }
  138. function decompress(data) {
  139. return press(false, data)
  140. }
  141. function press(com, data) {
  142. let last = 0
  143. let pressed = data.reviews.map((item) => {
  144. let map = [com ? item[0] - last : last + item[0], ...item.slice(1)]
  145. last = com ? item[0] : last + item[0]
  146. return map
  147. })
  148. return { cache_version: data.cache_version, date: data.date, reviews: pressed }
  149. }
  150.  
  151. async function update_data_after_session(data, disable_popup = false) {
  152. let [date, new_reviews] = await fetch_new_reviews(data.date, disable_popup)
  153. if (new_reviews.length) {
  154. for (let new_review of new_reviews) data.reviews.push(new_review)
  155. data.reviews.sort((a, b) => (a[0] < b[0] ? -1 : 1))
  156. data.date = date
  157. save(data)
  158. }
  159. return { reviews: data.reviews, changed: !!new_reviews.length }
  160. }
  161.  
  162. // Fetches any new reviews from the API
  163. async function fetch_new_reviews(last_fetch, disable_popup = false) {
  164. if (disable_popup) wkof.Progress.popup_delay(-1, window.review_cache.silent === true)
  165. let updated_reviews = await wkof.Apiv2.fetch_endpoint('reviews', {
  166. filters: { updated_after: last_fetch },
  167. disable_progress_dialog: true,
  168. }).catch(fetch_error)
  169. if (disable_popup) wkof.Progress.popup_delay('default', window.review_cache.silent === true)
  170. if (updated_reviews.error) return [null, []] // no new reviews
  171. let new_reviews = updated_reviews.data.filter((item) => last_fetch < item.data.created_at)
  172. new_reviews = new_reviews.map((item) => [
  173. Date.parse(item.data.created_at),
  174. item.data.subject_id,
  175. item.data.starting_srs_stage,
  176. item.data.incorrect_meaning_answers,
  177. item.data.incorrect_reading_answers,
  178. ])
  179. return [updated_reviews.data_updated_at, new_reviews]
  180. }
  181.  
  182. function fetch_error(error) {
  183. if (error.status !== 304) // skip logging for 304 Not Modified
  184. console.warn('Review Cache: Error fetching reviews', error)
  185. return { error }
  186. }
  187. })(window.wkof)