aboutsummaryrefslogtreecommitdiff
path: root/ext/js
diff options
context:
space:
mode:
authorKuuuube <61125188+Kuuuube@users.noreply.github.com>2024-05-21 13:48:29 -0400
committerGitHub <noreply@github.com>2024-05-21 17:48:29 +0000
commit6301ba6b33fc763872d9c500cd257ac5c7ffbb44 (patch)
tree894d0bfa12ac8dcd56f5d4dd50ce9885adccea08 /ext/js
parent02c60eea797b9548328bd160b453dd95a54b1641 (diff)
Rework dictionary import UX (#937)24.5.21.0
* Add option to import from URL * Remove some debug code * Improve import ui * Add drag and drop option * Add basic-only setting css * Better sizing of import elements * Hide import from url if advanced is not enabled * Improve file drag and drop box look * Remove redundant css * Allow clicking on drag and drop box to open file picker * Allow drag and drop for folders * Prevent welcome page from breaking due to unnecessary imports * Note that the drop zone can be clicked on * Reject directories with item counts requiring more than 1000 processing steps (roughly 500 items) * Improve import modal styling * Fix typing * Add book icon to drag zone * Remove drag-over class on drop * Filter only for .zip files in drag and drop * Drop zone text rename Files to Dictionaries and add (.zip) * Clarify not using instanceof in ts-expect-error * Only show drag-over styling when file is zip or directory
Diffstat (limited to 'ext/js')
-rw-r--r--ext/js/pages/settings/dictionary-import-controller.js170
-rw-r--r--ext/js/pages/welcome-main.js8
2 files changed, 165 insertions, 13 deletions
diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js
index 7ed61fc3..7090e296 100644
--- a/ext/js/pages/settings/dictionary-import-controller.js
+++ b/ext/js/pages/settings/dictionary-import-controller.js
@@ -43,9 +43,17 @@ export class DictionaryImportController {
/** @type {HTMLButtonElement} */
this._purgeConfirmButton = querySelectorNotNull(document, '#dictionary-confirm-delete-all-button');
/** @type {HTMLButtonElement} */
- this._importFileButton = querySelectorNotNull(document, '#dictionary-import-file-button');
- /** @type {HTMLInputElement} */
this._importFileInput = querySelectorNotNull(document, '#dictionary-import-file-input');
+ /** @type {HTMLButtonElement} */
+ this._importFileDrop = querySelectorNotNull(document, '#dictionary-drop-file-zone');
+ /** @type {number} */
+ this._importFileDropItemCount = 0;
+ /** @type {HTMLInputElement} */
+ this._importButton = querySelectorNotNull(document, '#dictionary-import-button');
+ /** @type {HTMLInputElement} */
+ this._importURLButton = querySelectorNotNull(document, '#dictionary-import-url-button');
+ /** @type {HTMLInputElement} */
+ this._importURLText = querySelectorNotNull(document, '#dictionary-import-url-text');
/** @type {?import('./modal.js').Modal} */
this._purgeConfirmModal = null;
/** @type {HTMLElement} */
@@ -65,22 +73,153 @@ export class DictionaryImportController {
/** */
prepare() {
+ this._importModal = this._modalController.getModal('dictionary-import');
this._purgeConfirmModal = this._modalController.getModal('dictionary-confirm-delete-all');
this._purgeButton.addEventListener('click', this._onPurgeButtonClick.bind(this), false);
this._purgeConfirmButton.addEventListener('click', this._onPurgeConfirmButtonClick.bind(this), false);
- this._importFileButton.addEventListener('click', this._onImportButtonClick.bind(this), false);
+ this._importButton.addEventListener('click', this._onImportButtonClick.bind(this), false);
+ this._importURLButton.addEventListener('click', this._onImportFromURL.bind(this), false);
this._importFileInput.addEventListener('change', this._onImportFileChange.bind(this), false);
+
+ this._importFileDrop.addEventListener('click', this._onImportFileButtonClick.bind(this), false);
+ this._importFileDrop.addEventListener('dragenter', this._onFileDropEnter.bind(this), false);
+ this._importFileDrop.addEventListener('dragover', this._onFileDropOver.bind(this), false);
+ this._importFileDrop.addEventListener('dragleave', this._onFileDropLeave.bind(this), false);
+ this._importFileDrop.addEventListener('drop', this._onFileDrop.bind(this), false);
}
// Private
/** */
- _onImportButtonClick() {
+ _onImportFileButtonClick() {
/** @type {HTMLInputElement} */ (this._importFileInput).click();
}
/**
+ * @param {DragEvent} e
+ */
+ _onFileDropEnter(e) {
+ e.preventDefault();
+ if (!e.dataTransfer) { return; }
+ for (const item of e.dataTransfer.items) {
+ // Directories and files with no extension both show as ''
+ if (item.type === '' || item.type === 'application/zip') {
+ this._importFileDrop.classList.add('drag-over');
+ break;
+ }
+ }
+ }
+
+ /**
+ * @param {DragEvent} e
+ */
+ _onFileDropOver(e) {
+ e.preventDefault();
+ }
+
+ /**
+ * @param {DragEvent} e
+ */
+ _onFileDropLeave(e) {
+ e.preventDefault();
+ this._importFileDrop.classList.remove('drag-over');
+ }
+
+ /**
+ * @param {DragEvent} e
+ */
+ async _onFileDrop(e) {
+ e.preventDefault();
+ this._importFileDrop.classList.remove('drag-over');
+ if (e.dataTransfer === null) { return; }
+ /** @type {import('./modal.js').Modal} */ (this._importModal).setVisible(false);
+ /** @type {File[]} */
+ const fileArray = [];
+ for (const fileEntry of await this._getAllFileEntries(e.dataTransfer.items)) {
+ if (!fileEntry) { return; }
+ try {
+ fileArray.push(await new Promise((resolve, reject) => { fileEntry.file(resolve, reject); }));
+ } catch (error) {
+ log.error(error);
+ }
+ }
+ void this._importDictionaries(fileArray);
+ }
+
+ /**
+ * @param {DataTransferItemList} dataTransferItemList
+ * @returns {Promise<FileSystemFileEntry[]>}
+ */
+ async _getAllFileEntries(dataTransferItemList) {
+ /** @type {(FileSystemFileEntry)[]} */
+ const fileEntries = [];
+ /** @type {(FileSystemEntry | null)[]} */
+ const entries = [];
+ for (let i = 0; i < dataTransferItemList.length; i++) {
+ entries.push(dataTransferItemList[i].webkitGetAsEntry());
+ }
+ this._importFileDropItemCount = entries.length - 1;
+ while (entries.length > 0) {
+ this._importFileDropItemCount += 1;
+ this._validateDirectoryItemCount();
+
+ /** @type {(FileSystemEntry | null) | undefined} */
+ const entry = entries.shift();
+ if (!entry) { continue; }
+ if (entry.isFile) {
+ if (entry.name.substring(entry.name.lastIndexOf('.'), entry.name.length) === '.zip') {
+ // @ts-expect-error - ts does not recognize `if (entry.isFile)` as verifying `entry` is type `FileSystemFileEntry` and instanceof does not work
+ fileEntries.push(entry);
+ }
+ } else if (entry.isDirectory) {
+ // @ts-expect-error - ts does not recognize `if (entry.isDirectory)` as verifying `entry` is type `FileSystemDirectoryEntry` and instanceof does not work
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ entries.push(...await this._readAllDirectoryEntries(entry.createReader()));
+ }
+ }
+ return fileEntries;
+ }
+
+ /**
+ * @param {FileSystemDirectoryReader} directoryReader
+ * @returns {Promise<(FileSystemEntry)[]>}
+ */
+ async _readAllDirectoryEntries(directoryReader) {
+ const entries = [];
+ /** @type {(FileSystemEntry)[]} */
+ let readEntries = await new Promise((resolve) => { directoryReader.readEntries(resolve); });
+ while (readEntries.length > 0) {
+ this._importFileDropItemCount += readEntries.length;
+ this._validateDirectoryItemCount();
+
+ entries.push(...readEntries);
+ readEntries = await new Promise((resolve) => { directoryReader.readEntries(resolve); });
+ }
+ return entries;
+ }
+
+ /**
+ * @throws
+ */
+ _validateDirectoryItemCount() {
+ if (this._importFileDropItemCount > 1000) {
+ this._importFileDropItemCount = 0;
+ const errorText = 'Directory upload item count too large';
+ this._showErrors([new Error(errorText)]);
+ throw new Error(errorText);
+ }
+ }
+
+ /**
+ * @param {MouseEvent} e
+ */
+ _onImportButtonClick(e) {
+ e.preventDefault();
+ /** @type {import('./modal.js').Modal} */ (this._importModal).setVisible(true);
+ }
+
+ /**
* @param {MouseEvent} e
*/
_onPurgeButtonClick(e) {
@@ -100,7 +239,8 @@ export class DictionaryImportController {
/**
* @param {Event} e
*/
- _onImportFileChange(e) {
+ async _onImportFileChange(e) {
+ /** @type {import('./modal.js').Modal} */ (this._importModal).setVisible(false);
const node = /** @type {HTMLInputElement} */ (e.currentTarget);
const {files} = node;
if (files === null) { return; }
@@ -110,6 +250,26 @@ export class DictionaryImportController {
}
/** */
+ async _onImportFromURL() {
+ const text = this._importURLText.value.trim();
+ if (!text) { return; }
+ const urls = text.split('\n');
+ const files = [];
+ for (const url of urls) {
+ try {
+ files.push(await fetch(url.trim())
+ .then((res) => res.blob())
+ .then((blob) => {
+ return new File([blob], 'fileFromURL');
+ }));
+ } catch (error) {
+ log.error(error);
+ }
+ }
+ void this._importDictionaries(files);
+ }
+
+ /** */
async _purgeDatabase() {
if (this._modifying) { return; }
diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js
index 7cb28cda..10a84a59 100644
--- a/ext/js/pages/welcome-main.js
+++ b/ext/js/pages/welcome-main.js
@@ -20,8 +20,6 @@ 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';
@@ -82,12 +80,6 @@ await Application.main(true, async (application) => {
const settingsController = new SettingsController(application);
await settingsController.prepare();
- const dictionaryController = new DictionaryController(settingsController, modalController, statusFooter);
- preparePromises.push(dictionaryController.prepare());
-
- const dictionaryImportController = new DictionaryImportController(settingsController, modalController, statusFooter);
- preparePromises.push(dictionaryImportController.prepare());
-
const genericSettingController = new GenericSettingController(settingsController);
preparePromises.push(setupGenericSettingsController(genericSettingController));