From 7dbced4359d12c1fe64682d55d7010967ea7dc79 Mon Sep 17 00:00:00 2001 From: Kuuuube <61125188+Kuuuube@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:51:27 -0400 Subject: [PATCH] Add recommended dictionaries to welcome page (#991) * Add dictionary import to welcome page * Initial prototype of recommended dict importing * Disable button immediately * Add queue to allow clicking multiple import buttons at once * Dynamic assignment of recommended dictionaries * Add to json.json * Change kanjidic to grab latest release * Make linter happy * Add json schema * Add explicit types to dict array properties * Fix schema $id * Change types file to dts and add labeled test type * Use static jitendex url * Fix url path * Remove BCCWJ-LUW version from name * Only render recommended dictionaries on button click * Clean up dictionary rendering * Hide dictionaries when going from a language supporting recommended dictionaries to one that doesnt * Update welcome page dict modals * Fix spacing * Add back recommended dictionary modals * Fix dictionary importing * Remove unneeded progress selectors * Fix recommended dictionary modal rendering * Add pronunciation to recommended dictionary types * Fix KANJIDIC name * Fix hiding and unhiding of elements * Move language select above recommended dictionaries * Use BCCWJ combined instead of BCCWJ-LUW * show download progress --------- Signed-off-by: Kuuuube <61125188+Kuuuube@users.noreply.github.com> Co-authored-by: Stefan Vukovic --- ext/data/recommended-dictionaries.json | 15 ++ .../recommended-dictionaries-schema.json | 120 +++++++++++ .../settings/dictionary-import-controller.js | 127 +++++++++++- ext/js/pages/welcome-main.js | 8 + ext/templates-settings.html | 12 ++ ext/welcome.html | 189 ++++++++++++++++-- test/data/json.json | 11 + types/ext/dictionary-recommended.d.ts | 43 ++++ 8 files changed, 508 insertions(+), 17 deletions(-) create mode 100644 ext/data/recommended-dictionaries.json create mode 100644 ext/data/schemas/recommended-dictionaries-schema.json create mode 100644 types/ext/dictionary-recommended.d.ts diff --git a/ext/data/recommended-dictionaries.json b/ext/data/recommended-dictionaries.json new file mode 100644 index 0000000000..245f638e18 --- /dev/null +++ b/ext/data/recommended-dictionaries.json @@ -0,0 +1,15 @@ +{ + "ja": { + "terms": [ + {"name": "Jitendex", "url": "https://github.com/stephenmk/stephenmk.github.io/releases/latest/download/jitendex-yomitan.zip"} + ], + "kanji": [ + {"name": "KANJIDIC", "url": "https://github.com/themoeway/jmdict-yomitan/releases/latest/download/KANJIDIC_english.zip"} + ], + "frequency": [ + {"name": "BCCWJ", "url": "https://github.com/Kuuuube/yomitan-dictionaries/releases/download/yomitan-permalink/BCCWJ_SUW_LUW_combined.zip"} + ], + "grammar": [], + "pronunciation": [] + } +} diff --git a/ext/data/schemas/recommended-dictionaries-schema.json b/ext/data/schemas/recommended-dictionaries-schema.json new file mode 100644 index 0000000000..36d8f03fd9 --- /dev/null +++ b/ext/data/schemas/recommended-dictionaries-schema.json @@ -0,0 +1,120 @@ +{ + "$id": "recommendedDictionaries", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Contains data for recommended dictionaries on welcome page.", + "type": "object", + "patternProperties": { + "^.{2,}$": { + "type": "object", + "properties": { + "terms": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 2 + }, + "url": { + "type": "string", + "minLength": 2 + } + } + } + }, + "kanji": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 2 + }, + "url": { + "type": "string", + "minLength": 2 + } + } + } + }, + "frequency": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 2 + }, + "url": { + "type": "string", + "minLength": 2 + } + } + } + }, + "grammar": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 2 + }, + "url": { + "type": "string", + "minLength": 2 + } + } + } + }, + "pronunciation": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "name": { + "type": "string", + "minLength": 2 + }, + "url": { + "type": "string", + "minLength": 2 + } + } + } + } + }, + "required": [ + "terms", + "kanji", + "frequency", + "grammar" + ], + "additionalProperties": false + } + } +} diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index c30f4bf6dc..1c1b7720c4 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -17,6 +17,7 @@ */ import {ExtensionError} from '../../core/extension-error.js'; +import {readResponseJson} from '../../core/json.js'; import {log} from '../../core/log.js'; import {toError} from '../../core/to-error.js'; import {DictionaryWorker} from '../../dictionary/dictionary-worker.js'; @@ -69,6 +70,10 @@ export class DictionaryImportController { 'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.', ], ]; + /** @type {string[]} */ + this._recommendedDictionaryQueue = []; + /** @type {boolean} */ + this._recommendedDictionaryActiveImport = false; } /** */ @@ -89,10 +94,127 @@ export class DictionaryImportController { this._importFileDrop.addEventListener('drop', this._onFileDrop.bind(this), false); this._settingsController.on('importDictionaryFromUrl', this._onEventImportDictionaryFromUrl.bind(this)); + + // Welcome page + const recommendedDictionaryButton = document.querySelector('[data-modal-action="show,recommended-dictionaries"]'); + if (recommendedDictionaryButton) { + recommendedDictionaryButton.addEventListener('click', this._renderRecommendedDictionaries.bind(this), false); + } } // Private + /** + * @param {MouseEvent} e + */ + async _onRecommendedImportClick(e) { + if (!(e instanceof PointerEvent)) { return; } + if (!e.target || !(e.target instanceof HTMLButtonElement)) { return; } + + const import_url = e.target.attributes.getNamedItem('data-import-url'); + if (!import_url) { return; } + this._recommendedDictionaryQueue.push(import_url.value); + + e.target.disabled = true; + + if (this._recommendedDictionaryActiveImport) { return; } + + while (this._recommendedDictionaryQueue.length > 0) { + this._recommendedDictionaryActiveImport = true; + try { + const url = this._recommendedDictionaryQueue.shift(); + if (!url) { continue; } + + const importProgressTracker = new ImportProgressTracker(this._getUrlImportSteps(), 1); + const onProgress = importProgressTracker.onProgress.bind(importProgressTracker); + void this._importDictionaries( + this._generateFilesFromUrls([url], onProgress), + importProgressTracker, + ); + } catch (error) { + log.error(error); + } + } + this._recommendedDictionaryActiveImport = false; + } + + /** */ + async _renderRecommendedDictionaries() { + const url = '../../data/recommended-dictionaries.json'; + const response = await fetch(url, { + method: 'GET', + mode: 'no-cors', + cache: 'default', + credentials: 'omit', + redirect: 'follow', + referrerPolicy: 'no-referrer', + }); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status}`); + } + + /** @type {import('dictionary-recommended.js').RecommendedDictionaryElementMap[]} */ + const recommendedDictionaryCategories = [ + {property: 'terms', element: querySelectorNotNull(querySelectorNotNull(document, '#recommended-term-dictionaries'), '.recommended-dictionary-list')}, + {property: 'kanji', element: querySelectorNotNull(querySelectorNotNull(document, '#recommended-kanji-dictionaries'), '.recommended-dictionary-list')}, + {property: 'frequency', element: querySelectorNotNull(querySelectorNotNull(document, '#recommended-frequency-dictionaries'), '.recommended-dictionary-list')}, + {property: 'grammar', element: querySelectorNotNull(querySelectorNotNull(document, '#recommended-grammar-dictionaries'), '.recommended-dictionary-list')}, + {property: 'pronunciation', element: querySelectorNotNull(querySelectorNotNull(document, '#recommended-pronunciation-dictionaries'), '.recommended-dictionary-list')}, + ]; + + const language = (await this._settingsController.getOptions()).general.language; + /** @type {import('dictionary-recommended.js').RecommendedDictionaries} */ + const recommendedDictionaries = (await readResponseJson(response)); + + if (!(language in recommendedDictionaries)) { + for (const {element} of recommendedDictionaryCategories) { + const dictionaryCategoryParent = element.parentElement; + if (dictionaryCategoryParent) { + dictionaryCategoryParent.hidden = true; + } + } + return; + } + + for (const {property, element} of recommendedDictionaryCategories) { + this._renderRecommendedDictionaryGroup(recommendedDictionaries[language][property], element); + } + + /** @type {NodeListOf} */ + const buttons = document.querySelectorAll('.action-button[data-action=import-recommended-dictionary]'); + for (const button of buttons) { + button.addEventListener('click', this._onRecommendedImportClick.bind(this), false); + } + } + + /** + * + * @param {import('dictionary-recommended.js').Dictionary[]} recommendedDictionaries + * @param {HTMLElement} dictionariesList + */ + _renderRecommendedDictionaryGroup(recommendedDictionaries, dictionariesList) { + const dictionariesListParent = dictionariesList.parentElement; + dictionariesList.innerHTML = ''; + for (const dictionary of recommendedDictionaries) { + if (dictionariesList) { + if (dictionariesListParent) { + dictionariesListParent.hidden = false; + } + const template = this._settingsController.instantiateTemplate('recommended-dictionaries-list-item'); + const label = querySelectorNotNull(template, '.settings-item-label'); + const button = querySelectorNotNull(template, '.action-button[data-action=import-recommended-dictionary]'); + + const urlAttribute = document.createAttribute('data-import-url'); + urlAttribute.value = dictionary.url; + button.attributes.setNamedItem(urlAttribute); + + label.textContent = dictionary.name; + + dictionariesList.append(template); + } + } + } + /** * @param {import('settings-controller').EventArgument<'importDictionaryFromUrl'>} details */ @@ -371,6 +493,7 @@ export class DictionaryImportController { const statusFooter = this._statusFooter; const progressSelector = '.dictionary-import-progress'; const progressContainers = /** @type {NodeListOf} */ (document.querySelectorAll(`#dictionaries-modal ${progressSelector}`)); + const recommendedProgressContainers = /** @type {NodeListOf} */ (document.querySelectorAll(`#recommended-dictionaries-modal ${progressSelector}`)); const prevention = this._preventPageExit(); @@ -382,7 +505,7 @@ export class DictionaryImportController { this._setModifying(true); this._hideErrors(); - for (const progress of progressContainers) { progress.hidden = false; } + for (const progress of [...progressContainers, ...recommendedProgressContainers]) { progress.hidden = false; } const optionsFull = await this._settingsController.getOptionsFull(); const importDetails = { @@ -411,7 +534,7 @@ export class DictionaryImportController { } finally { this._showErrors(errors); prevention.end(); - for (const progress of progressContainers) { progress.hidden = true; } + for (const progress of [...progressContainers, ...recommendedProgressContainers]) { progress.hidden = true; } if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); } this._setModifying(false); this._triggerStorageChanged(); diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js index 6771b31e91..349b09ac49 100644 --- a/ext/js/pages/welcome-main.js +++ b/ext/js/pages/welcome-main.js @@ -20,6 +20,8 @@ import {Application} from '../application.js'; import {DocumentFocusController} from '../dom/document-focus-controller.js'; import {querySelectorNotNull} from '../dom/query-selector.js'; import {ExtensionContentController} from './common/extension-content-controller.js'; +import {DictionaryController} from './settings/dictionary-controller.js'; +import {DictionaryImportController} from './settings/dictionary-import-controller.js'; import {GenericSettingController} from './settings/generic-setting-controller.js'; import {LanguagesController} from './settings/languages-controller.js'; import {ModalController} from './settings/modal-controller.js'; @@ -88,6 +90,12 @@ await Application.main(true, async (application) => { const genericSettingController = new GenericSettingController(settingsController); preparePromises.push(setupGenericSettingsController(genericSettingController)); + const dictionaryController = new DictionaryController(settingsController, modalController, statusFooter); + preparePromises.push(dictionaryController.prepare()); + + const dictionaryImportController = new DictionaryImportController(settingsController, modalController, statusFooter); + preparePromises.push(dictionaryImportController.prepare()); + const simpleScanningInputController = new ScanInputsSimpleController(settingsController); preparePromises.push(simpleScanningInputController.prepare()); diff --git a/ext/templates-settings.html b/ext/templates-settings.html index d8e5c40d10..4e1bd8ef7b 100644 --- a/ext/templates-settings.html +++ b/ext/templates-settings.html @@ -118,6 +118,18 @@ +