您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Filter your lessons by type, while maintaining WaniKani's lesson order.
// ==UserScript== // @name WaniKani Lesson Filter // @namespace https://www.wanikani.com // @description Filter your lessons by type, while maintaining WaniKani's lesson order. // @author seanblue // @version 2.1.4 // @match https://www.wanikani.com/subjects* // @match https://preview.wanikani.com/subjects* // @grant none // ==/UserScript== (async function(global) { 'use strict'; var wkofMinimumVersion = '1.1.0'; if (!global.wkof) { var response = confirm('WaniKani Lesson Filter requires WaniKani Open Framework.\n Click "OK" to be forwarded to installation instructions.'); if (response) { window.location.href = 'https://community.wanikani.com/t/instructions-installing-wanikani-open-framework/28549'; } return; } if (!global.wkof.version || global.wkof.version.compare_to(wkofMinimumVersion) === 'older') { alert(`WaniKani Lesson Filter requires at least version ${wkofMinimumVersion} of WaniKani Open Framework.`); return; } const localStorageSettingsKey = 'lessonFilter_inputData'; const localStorageSettingsVersion = 2; const radicalSubjectType = 'radical'; const kanjiSubjectType = 'kanji'; const vocabSubjectType = 'vocabulary'; const kanaVocabSubjectType = 'kana_vocabulary'; const batchSizeInputSelector = '#lf-batch-size'; const radicalInputSelector = '#lf-radical'; const kanjiInputSelector = '#lf-kanji'; const vocabInputSelector = '#lf-vocab'; const radicalCountSelector = '.subject-statistic-counts__item-count-text[data-category="Radical"]'; const kanjiCountSelector = '.subject-statistic-counts__item-count-text[data-category="Kanji"]'; const vocabCountSelector = '.subject-statistic-counts__item-count-text[data-category="Vocabulary"]'; const pages = { lessonPage: 'lesson', quizPage: 'quiz', other: 'other' }; const style = `<style> #lf-main { width: 100%; margin: 10px auto; padding: 10px 20px; border-radius: 6px; text-align: center; background-color: #444; color: #fff; } .lf-title { font-size: 1.6em; font-weight: bold; padding-bottom: 5px; } .lf-list { margin: 0px; padding: 0px; } .lf-list-item { display: inline-block; list-style: none; text-align: center; padding: 8px; } .lf-list-item input { display: block; width: 45px; color: #fff; border-width: 2px; border-style: inset; } .lf-list-item span { display: block; padding-bottom: 3px; } #lf-batch-size { background-color: #ff5500; } #lf-radical { background-color: #0af; } #lf-kanji { background-color: #f0a; } #lf-vocab { background-color: #a0f; } .lf-filter-section { padding-top: 10px; } .lf-filter-section input { font-size: 0.9em; margin: 0px 10px; padding: 3px; border-width: 2px; border-style: outset; border-radius: 6px; } </style>`; const html = `<div id="lf-main"> <div class="lf-title">Items to Learn</div> <div class="lf-list"> <div class="lf-list-item"> <span lang="ja">バッチ</span> <input id="lf-batch-size" type="text" autocomplete="off" data-lpignore="true" maxlength="4" /> </div> <div class="lf-list-item"> <span lang="ja">部首</span> <input id="lf-radical" type="text" autocomplete="off" data-lpignore="true" maxlength="4" /> </div> <div class="lf-list-item"> <span lang="ja">漢字</span> <input id="lf-kanji" type="text" autocomplete="off" data-lpignore="true" maxlength="4" /> </div> <div class="lf-list-item"> <span lang="ja">単語</span> <input id="lf-vocab" type="text" autocomplete="off" data-lpignore="true" maxlength="4" /> </div> </div> <div class="lf-filter-section"> <input type="button" value="Filter" id="lf-apply-filter"></input> <input type="button" value="Shuffle" id="lf-apply-shuffle"></input> </div> </div>`; let queueInitializedPromise; let initialLessonQueue; let initialBatchSize; let currentLessonQueue; let currentBatchSize; async function initialize() { queueInitializedPromise = initializeLessonQueue(); await queueInitializedPromise; } async function initializeLessonQueue() { global.wkof.include('Apiv2'); await global.wkof.ready('Apiv2'); let [ unsortedLessonQueue, userPreferences ] = await Promise.all([getUnsortedLessonQueue(), getUserPreferences()]); initialBatchSize = userPreferences.batchSize; initialLessonQueue = sortInitialLessonQueue(unsortedLessonQueue, userPreferences.lessonOrder); currentLessonQueue = [...initialLessonQueue]; currentBatchSize = initialBatchSize; return Promise.resolve('done'); } async function getUnsortedLessonQueue() { let summary = await global.wkof.Apiv2.fetch_endpoint('summary'); let lessonIds = summary.data.lessons.flatMap(l => l.subject_ids); let lessonData = await global.wkof.Apiv2.fetch_endpoint('subjects', { filters: { ids: lessonIds } }); return lessonData.data.map(d => ({ id: d.id, level: d.data.level, subjectType: d.object, lessonPosition: d.data.lesson_position })); } async function getUserPreferences() { let response = await global.wkof.Apiv2.fetch_endpoint('user'); return { batchSize: response.data.preferences.lessons_batch_size, lessonOrder: response.data.preferences.lessons_presentation_order }; } function sortInitialLessonQueue(queue, lessonOrder) { let typeOrder = { [radicalSubjectType]: 0, [kanjiSubjectType]: 1, [vocabSubjectType]: 2, [kanaVocabSubjectType]: 2 }; if (lessonOrder === 'ascending_level_then_subject') { return queue.sort((a, b) => a.level - b.level || typeOrder[a.subjectType] - typeOrder[b.subjectType] || a.lessonPosition - b.lessonPosition); } shuffle(queue); if (lessonOrder === 'ascending_level_then_shuffled') { queue.sort((a, b) => a.level - b.level); } return queue; } function setupStyles(head) { head.insertAdjacentHTML('beforeend', style); } function setupUI(body) { let page = getPage(window.location); if (page === pages.lessonPage || page === pages.quizPage) { updateItemCountsInUI(body); } if (page !== pages.lessonPage) { return; } let existingLessonFilterSection = body.querySelector('#lf-main'); if (existingLessonFilterSection) { return; } let queueItemsSection = body.querySelector('.subject-queue__items'); if (!queueItemsSection) { return; } queueItemsSection.insertAdjacentHTML('beforeend', html); loadSavedInputData(body); setupEvents(body); } function getPage(location) { if ((/(\/?)subjects(\/\d+)\/lesson(\/?)/.test(location.pathname))) { return pages.lessonPage; } if ((/(\/?)subjects\/lesson\/quiz(\/?)/.test(location.pathname))) { return pages.quizPage; } return pages.other; } function loadSavedInputData(body) { let savedDataString = localStorage[localStorageSettingsKey]; if (!savedDataString) { return; } let savedData = JSON.parse(savedDataString); if (savedData.version !== localStorageSettingsVersion) { delete localStorage[localStorageSettingsKey]; return; } let data = savedData.data; body.querySelector(batchSizeInputSelector).value = data.batchSize; body.querySelector(radicalInputSelector).value = data.radicals; body.querySelector(kanjiInputSelector).value = data.kanji; body.querySelector(vocabInputSelector).value = data.vocab; } function updateItemCountsInUI(body) { var lessonQueueByType = getLessonQueueByType(currentLessonQueue); updateItemCountInUI(body, radicalCountSelector, lessonQueueByType[radicalSubjectType]); updateItemCountInUI(body, kanjiCountSelector, lessonQueueByType[kanjiSubjectType]); updateItemCountInUI(body, vocabCountSelector, lessonQueueByType[vocabSubjectType]); } function updateItemCountInUI(body, selector, queueForType) { let lessonQueueByType = getLessonQueueByType(currentLessonQueue); let el = body.querySelector(selector); if (el) { el.innerText = queueForType.length; } } function setupEvents(body) { body.querySelector('#lf-apply-filter').addEventListener('click', applyFilter); body.querySelector('#lf-apply-shuffle').addEventListener('click', applyShuffle); } async function applyFilter(e) { let rawFilterValues = getRawFilterValuesFromUI(document); let filtered = await filterLessonsInternal(rawFilterValues); if (filtered) { saveRawFilterValues(rawFilterValues); } } async function filterLessonsInternal(rawFilterValues) { await queueInitializedPromise; let newBatchSize = getCheckedBatchSize(rawFilterValues.batchSize); if (newBatchSize === null || newBatchSize < 1) { alert('Batch size must be a positive number'); return false; } let newFilteredQueue = getFilteredQueue(rawFilterValues); if (newFilteredQueue.length === 0) { alert('Cannot remove all lessons'); return false; } currentLessonQueue = newFilteredQueue; currentBatchSize = newBatchSize; visitUrlForCurrentBatch(); return true; } function getRawFilterValuesFromUI(body) { return { 'batchSize': body.querySelector(batchSizeInputSelector).value.trim(), 'radicals': body.querySelector(radicalInputSelector).value.trim(), 'kanji': body.querySelector(kanjiInputSelector).value.trim(), 'vocab': body.querySelector(vocabInputSelector).value.trim() }; } function getFilteredQueue(rawFilterValues) { let idToIndex = { }; for (let i = 0; i < initialLessonQueue.length; i++) { idToIndex[initialLessonQueue[i].id] = i; } var lessonQueueByType = getLessonQueueByType(initialLessonQueue); let filteredRadicalQueue = getFilteredQueueForType(lessonQueueByType[radicalSubjectType], rawFilterValues.radicals); let filteredKanjiQueue = getFilteredQueueForType(lessonQueueByType[kanjiSubjectType], rawFilterValues.kanji); let filteredVocabQueue = getFilteredQueueForType(lessonQueueByType[vocabSubjectType], rawFilterValues.vocab); return filteredRadicalQueue.concat(filteredKanjiQueue).concat(filteredVocabQueue).sort((a, b) => idToIndex[a.id] - idToIndex[b.id]); } function getLessonQueueByType(lessonQueue) { let vocabSubjectTypeGroup = localStorage.lessonFilter_skipKanaVocab ? [vocabSubjectType] : [vocabSubjectType, kanaVocabSubjectType]; return { [radicalSubjectType]: getQueueForType(lessonQueue, [radicalSubjectType]), [kanjiSubjectType]: getQueueForType(lessonQueue, [kanjiSubjectType]), [vocabSubjectType]: getQueueForType(lessonQueue, vocabSubjectTypeGroup) }; } function getQueueForType(lessonQueue, subjectTypes) { return lessonQueue.filter(item => subjectTypes.includes(item.subjectType)); } function getFilteredQueueForType(queueForType, rawFilterValue) { let filterValue = parseInt(rawFilterValue); if (filterValue <= 0) { return []; } if (isNaN(filterValue)) { return queueForType; } return queueForType.slice(0, filterValue); } function getCheckedBatchSize(rawValue) { if (rawValue === '') { return initialBatchSize; } let value = parseInt(rawValue); if (isNaN(value)) { return null; } return value; } function applyShuffle(e) { shuffleLessonsInternal(); } async function shuffleLessonsInternal() { await queueInitializedPromise; shuffle(currentLessonQueue); visitUrlForCurrentBatch(); } function shuffle(array) { // https://stackoverflow.com/a/12646864 // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm for (let i = array.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); let temp = array[i]; array[i] = array[j]; array[j] = temp; } } function visitUrlForCurrentBatch() { if (currentLessonQueue.length === 0) { global.Turbo.visit(`/dashboard`); } let lessonBatchQueryParam = getCurrentLessonBatchIds().join('-'); global.Turbo.visit(`/subjects/${currentLessonQueue[0].id}/lesson?queue=${lessonBatchQueryParam}`); } function getCurrentLessonBatchIds() { return currentLessonQueue.slice(0, currentBatchSize).map(item => item.id); } function saveRawFilterValues(rawFilterValues) { let settings = { 'version': localStorageSettingsVersion, 'data': rawFilterValues }; localStorage[localStorageSettingsKey] = JSON.stringify(settings); } function isNewBatchUrl(url) { return new URL(url).pathname === '/subjects/lesson'; } function setsAreEqual(set1, set2) { return set1.size === set2.size && [...set1].every(v => set2.has(v)); } window.addEventListener('turbo:before-visit', function(e) { if (isNewBatchUrl(e.detail.url)) { e.preventDefault(); let currentLessonBatchIdSet = new Set(getCurrentLessonBatchIds()); initialLessonQueue = initialLessonQueue.filter(item => !currentLessonBatchIdSet.has(item.id)); currentLessonQueue = currentLessonQueue.filter(item => !currentLessonBatchIdSet.has(item.id)); visitUrlForCurrentBatch(); } }); window.addEventListener('turbo:before-render', function(e) { e.preventDefault(); setupUI(e.detail.newBody); e.detail.resume(); }); window.lessonFilter = Object.freeze({ shuffle: () => { shuffleLessonsInternal() }, filter: (radicalCount, kanjiCount, vocabCount, batchSize) => { let rawFilterValues = { 'radicals': radicalCount, 'kanji': kanjiCount, 'vocab': vocabCount, 'batchSize': batchSize }; filterLessonsInternal(rawFilterValues); } }); await initialize(); setupStyles(document.head); setupUI(document.body); })(window);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址