aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--ext/css/settings.css72
-rw-r--r--ext/js/pages/settings/dictionary-import-controller.js170
-rw-r--r--ext/js/pages/welcome-main.js8
-rw-r--r--ext/settings.html26
4 files changed, 262 insertions, 14 deletions
diff --git a/ext/css/settings.css b/ext/css/settings.css
index ba82ecf7..d23062b1 100644
--- a/ext/css/settings.css
+++ b/ext/css/settings.css
@@ -53,6 +53,8 @@
--modal-height: 400px;
--modal-width-small: 400px;
--modal-height-small: 200px;
+ --modal-width-medium: 600px;
+ --modal-height-medium: 400px;
--modal-transition-offset: -64px;
--badge-size: 16px;
@@ -587,6 +589,9 @@ a.heading-link-light {
:root:not([data-advanced=true]) .advanced-only {
display: none;
}
+:root:not([data-advanced=false]) .basic-only {
+ display: none;
+}
.settings-item.settings-item-button,
a.settings-item.settings-item-button {
cursor: pointer;
@@ -769,6 +774,12 @@ select.short-height {
height: auto;
max-height: 100%;
}
+.modal-content.modal-content-medium {
+ width: var(--modal-width-medium);
+ min-height: var(--modal-height-medium);
+ height: auto;
+ max-height: 100%;
+}
.modal-content.modal-content-full {
width: var(--content-width);
height: 100%;
@@ -2352,6 +2363,67 @@ input[type=number].dictionary-priority {
}
+/* Dictionary Import */
+#dictionary-import-url-text {
+ width: 100%;
+ height: 4em;
+ white-space: nowrap;
+ resize: none;
+}
+
+#dictionary-import-url-button {
+ flex: auto;
+}
+
+#dictionary-drop-file-zone {
+ transition: background-color var(--animation-duration) ease-in-out, border var(--animation-duration) ease-in-out;
+ border: 2px dashed rgb(204, 204, 204);
+ border-radius: 5px;
+ flex: auto;
+ min-height: 20em;
+ user-select: none;
+ text-align: center;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+#dictionary-drop-file-zone:hover {
+ background-color: rgba(28, 116, 233, 0.05);
+ border: 2px dashed var(--accent-color);
+}
+
+#dictionary-drop-file-zone.drag-over {
+ border: 2px solid var(--accent-color);
+ background-color: rgb(191, 209, 255);
+}
+
+#dictionary-drag-drop-text {
+ pointer-events: none;
+}
+
+#dictionary-drag-drop-text>.icon {
+ display: block;
+ margin: auto;
+ background-color: var(--button-default-icon-color);
+ width: var(--outline-item-icon-size);
+ height: var(--outline-item-icon-size);
+}
+
+#dictionary-drag-drop-text h1,
+#dictionary-drag-drop-text h5 {
+ margin: 0;
+ padding: 0;
+ font-weight: normal;
+ border-bottom: none;
+}
+
+#dictionary-import-modal .modal-body:has(#dictionary-drop-file-zone) {
+ display: flex;
+}
+
+
/* Generic layouts */
.margin-above {
margin-top: 0.85em;
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));
diff --git a/ext/settings.html b/ext/settings.html
index 3eb2fc2e..6f4857db 100644
--- a/ext/settings.html
+++ b/ext/settings.html
@@ -2415,7 +2415,31 @@
<div class="modal-footer">
<button type="button" class="low-emphasis danger dictionary-database-mutating-input" id="dictionary-delete-all-button">Delete All</button>
<button type="button" class="low-emphasis dictionary-database-mutating-input" id="dictionary-check-integrity">Check Integrity</button>
- <button type="button" class="low-emphasis dictionary-database-mutating-input" id="dictionary-import-file-button">Import</button>
+ <button type="button" class="low-emphasis dictionary-database-mutating-input" id="dictionary-import-button">Import</button>
+ <button type="button" data-modal-action="hide">Close</button>
+ </div>
+</div></div>
+
+<div id="dictionary-import-modal" class="modal" tabindex="-1" role="dialog" hidden><div class="modal-content modal-content-medium">
+ <div class="modal-header"><div class="modal-title">Import Dictionaries</div></div>
+ <div class="modal-body">
+ <div id="dictionary-drop-file-zone">
+ <div id="dictionary-drag-drop-text">
+ <span class="icon" data-icon="book"></span>
+ <h1>Drag and drop dictionaries (.zip)</h1>
+ <h5>or click here to upload</h5>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" data-modal-action="hide" class="basic-only">Close</button>
+ </div>
+ <div class="modal-body advanced-only">
+ <p>Import dictionaries from URLs:</p>
+ <textarea type="text" id="dictionary-import-url-text"></textarea>
+ </div>
+ <div class="modal-footer advanced-only">
+ <button type="button" data-modal-action="hide" class="low-emphasis dictionary-database-mutating-input" id="dictionary-import-url-button">Import from URLs</button>
<button type="button" data-modal-action="hide">Close</button>
</div>
</div></div>