Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow queueing dictionary update & delete together #1561

Merged
merged 16 commits into from
Nov 6, 2024
Merged
102 changes: 73 additions & 29 deletions ext/js/pages/settings/dictionary-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ class DictionaryEntry {
this._enabledCheckbox.checked = value;
}

/** */
hideUpdatesAvailableButton() {
this._updatesAvailable.hidden = true;
}

/**
* @returns {Promise<boolean>}
*/
Expand Down Expand Up @@ -161,7 +166,8 @@ class DictionaryEntry {
const bodyNode = e.detail.menu.bodyNode;
const count = this._dictionaryController.dictionaryOptionCount;
this._setMenuActionEnabled(bodyNode, 'moveTo', count > 1);
this._setMenuActionEnabled(bodyNode, 'delete', !this._dictionaryController.isDictionaryInDeleteQueue(this.dictionaryTitle));
const deleteDisabled = this._dictionaryController.isDictionaryInTaskQueue(this.dictionaryTitle);
this._setMenuActionEnabled(bodyNode, 'delete', !deleteDisabled);
}

/**
Expand Down Expand Up @@ -504,10 +510,12 @@ export class DictionaryController {
this._allCheckbox = querySelectorNotNull(document, '#all-dictionaries-enabled');
/** @type {?DictionaryExtraInfo} */
this._extraInfo = null;
/** @type {import('dictionary-controller.js').DictionaryTask[]} */
this._dictionaryTaskQueue = [];
/** @type {boolean} */
this._isDeleting = false;
/** @type {string[]} */
this._dictionaryDeleteQueue = [];
this._isTaskQueueRunning = false;
/** @type {(() => void) | null} */
this._onDictionariesUpdate = null;
}

/** @type {import('./modal-controller.js').ModalController} */
Expand Down Expand Up @@ -738,6 +746,10 @@ export class DictionaryController {
this._dictionaries = dictionaries;

await this._updateEntries();

if (this._onDictionariesUpdate) {
this._onDictionariesUpdate();
}
}

/** */
Expand Down Expand Up @@ -842,11 +854,12 @@ export class DictionaryController {
const modal = /** @type {import('./modal.js').Modal} */ (this._deleteDictionaryModal);
modal.setVisible(false);

const title = modal.node.dataset.dictionaryTitle;
if (typeof title !== 'string') { return; }
const dictionaryTitle = modal.node.dataset.dictionaryTitle;
if (typeof dictionaryTitle !== 'string') { return; }
delete modal.node.dataset.dictionaryTitle;

void this._enqueueDictionaryDelete(title);
void this._enqueueTask({type: 'delete', dictionaryTitle});
this._hideUpdatesAvailableButton(dictionaryTitle);
}

/**
Expand All @@ -858,12 +871,24 @@ export class DictionaryController {
const modal = /** @type {import('./modal.js').Modal} */ (this._updateDictionaryModal);
modal.setVisible(false);

const title = modal.node.dataset.dictionaryTitle;
const dictionaryTitle = modal.node.dataset.dictionaryTitle;
const downloadUrl = modal.node.dataset.downloadUrl;
if (typeof title !== 'string') { return; }
if (typeof dictionaryTitle !== 'string') { return; }
delete modal.node.dataset.dictionaryTitle;

void this._updateDictionary(title, downloadUrl);
void this._enqueueTask({type: 'update', dictionaryTitle, downloadUrl});
this._hideUpdatesAvailableButton(dictionaryTitle);
}

/**
* @param {string} dictionaryTitle
*/
_hideUpdatesAvailableButton(dictionaryTitle) {
for (const entry of this._dictionaryEntries) {
if (entry.dictionaryTitle === dictionaryTitle) {
entry.hideUpdatesAvailableButton();
}
}
}

/**
Expand Down Expand Up @@ -954,7 +979,7 @@ export class DictionaryController {

/** */
async _checkForUpdates() {
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isDeleting) { return; }
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isTaskQueueRunning) { return; }
let hasUpdates;
try {
this._checkingUpdates = true;
Expand All @@ -977,7 +1002,7 @@ export class DictionaryController {

/** */
async _checkIntegrity() {
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isDeleting) { return; }
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isTaskQueueRunning) { return; }

try {
this._checkingIntegrity = true;
Expand Down Expand Up @@ -1053,30 +1078,47 @@ export class DictionaryController {
* @param {string} dictionaryTitle
* @returns {boolean}
*/
isDictionaryInDeleteQueue(dictionaryTitle) {
return this._dictionaryDeleteQueue.includes(dictionaryTitle);
isDictionaryInTaskQueue(dictionaryTitle) {
return this._dictionaryTaskQueue.some((task) => task.dictionaryTitle === dictionaryTitle);
}

/**
* @param {string} dictionaryTitle
* @param {import('dictionary-controller.js').DictionaryTask} task
*/
async _enqueueDictionaryDelete(dictionaryTitle) {
if (this.isDictionaryInDeleteQueue(dictionaryTitle)) { return; }
this._dictionaryDeleteQueue.push(dictionaryTitle);
if (this._isDeleting) { return; }
while (this._dictionaryDeleteQueue.length > 0) {
const title = this._dictionaryDeleteQueue[0];
if (!title) { continue; }
await this._deleteDictionary(title);
void this._dictionaryDeleteQueue.shift();
_enqueueTask(task) {
if (this.isDictionaryInTaskQueue(task.dictionaryTitle)) { return; }
this._dictionaryTaskQueue.push(task);
void this._runTaskQueue();
}


/** */
async _runTaskQueue() {
if (this._isTaskQueueRunning) { return; }
this._isTaskQueueRunning = true;
while (this._dictionaryTaskQueue.length > 0) {
const task = this._dictionaryTaskQueue[0];
if (task.type === 'delete') {
await this._deleteDictionary(task.dictionaryTitle);
} else if (task.type === 'update') {
await this._updateDictionary(task.dictionaryTitle, task.downloadUrl);
}
/** @type {Promise<void>} */
const dictionariesUpdatePromise = new Promise((resolve) => {
this._onDictionariesUpdate = resolve;
});
await dictionariesUpdatePromise;
this._onDictionariesUpdate = null;
void this._dictionaryTaskQueue.shift();
}
this._isTaskQueueRunning = false;
}

/**
* @param {string} dictionaryTitle
*/
async _deleteDictionary(dictionaryTitle) {
if (this._isDeleting || this._checkingIntegrity) { return; }
if (this._checkingIntegrity) { return; }

const index = this._dictionaryEntries.findIndex((entry) => entry.dictionaryTitle === dictionaryTitle);
if (index < 0) { return; }
Expand All @@ -1089,7 +1131,6 @@ export class DictionaryController {
const statusLabels = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-status`));
const prevention = this._settingsController.preventPageExit();
try {
this._isDeleting = true;
this._setButtonsEnabled(false);

/**
Expand Down Expand Up @@ -1122,7 +1163,6 @@ export class DictionaryController {
for (const progress of progressContainers) { progress.hidden = true; }
if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); }
this._setButtonsEnabled(true);
this._isDeleting = false;
this._triggerStorageChanged();
}
}
Expand All @@ -1132,7 +1172,7 @@ export class DictionaryController {
* @param {string|undefined} downloadUrl
*/
async _updateDictionary(dictionaryTitle, downloadUrl) {
if (this._checkingIntegrity || this._checkingUpdates || this._isDeleting || this._dictionaries === null) { return; }
if (this._checkingIntegrity || this._checkingUpdates || this._dictionaries === null) { return; }

const dictionaryInfo = this._dictionaries.find((entry) => entry.title === dictionaryTitle);
if (typeof dictionaryInfo === 'undefined') { throw new Error('Dictionary not found'); }
Expand All @@ -1156,7 +1196,11 @@ export class DictionaryController {
}

await this._deleteDictionary(dictionaryTitle);
this._settingsController.trigger('importDictionaryFromUrl', {url: downloadUrl, profilesDictionarySettings});
/** @type {Promise<void>} */
const importPromise = new Promise((resolve) => {
this._settingsController.trigger('importDictionaryFromUrl', {url: downloadUrl, profilesDictionarySettings, onImportDone: resolve});
});
await importPromise;
}

/**
Expand Down
17 changes: 12 additions & 5 deletions ext/js/pages/settings/dictionary-import-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export class DictionaryImportController {
await this._importDictionaries(
this._generateFilesFromUrls([url], onProgress),
null,
null,
importProgressTracker,
);
void this._recommendedDictionaryQueue.shift();
Expand Down Expand Up @@ -252,8 +253,8 @@ export class DictionaryImportController {
/**
* @param {import('settings-controller').EventArgument<'importDictionaryFromUrl'>} details
*/
_onEventImportDictionaryFromUrl({url, profilesDictionarySettings}) {
void this.importFilesFromURLs(url, profilesDictionarySettings);
_onEventImportDictionaryFromUrl({url, profilesDictionarySettings, onImportDone}) {
void this.importFilesFromURLs(url, profilesDictionarySettings, onImportDone);
}

/** */
Expand Down Expand Up @@ -313,6 +314,7 @@ export class DictionaryImportController {
void this._importDictionaries(
this._arrayToAsyncGenerator(fileArray),
null,
null,
importProgressTracker,
);
}
Expand Down Expand Up @@ -419,6 +421,7 @@ export class DictionaryImportController {
void this._importDictionaries(
this._arrayToAsyncGenerator(files2),
null,
null,
new ImportProgressTracker(this._getFileImportSteps(), files2.length),
);
}
Expand All @@ -427,21 +430,23 @@ export class DictionaryImportController {
async _onImportFromURL() {
const text = this._importURLText.value.trim();
if (!text) { return; }
await this.importFilesFromURLs(text, null);
await this.importFilesFromURLs(text, null, null);
}

/**
* @param {string} text
* @param {import('settings-controller').ProfilesDictionarySettings} profilesDictionarySettings
* @param {import('settings-controller').ImportDictionaryDoneCallback} onImportDone
*/
async importFilesFromURLs(text, profilesDictionarySettings) {
async importFilesFromURLs(text, profilesDictionarySettings, onImportDone) {
const urls = text.split('\n');

const importProgressTracker = new ImportProgressTracker(this._getUrlImportSteps(), urls.length);
const onProgress = importProgressTracker.onProgress.bind(importProgressTracker);
void this._importDictionaries(
this._generateFilesFromUrls(urls, onProgress),
profilesDictionarySettings,
onImportDone,
importProgressTracker,
);
}
Expand Down Expand Up @@ -524,9 +529,10 @@ export class DictionaryImportController {
/**
* @param {AsyncGenerator<File, void, void>} dictionaries
* @param {import('settings-controller').ProfilesDictionarySettings} profilesDictionarySettings
* @param {import('settings-controller').ImportDictionaryDoneCallback} onImportDone
* @param {ImportProgressTracker} importProgressTracker
*/
async _importDictionaries(dictionaries, profilesDictionarySettings, importProgressTracker) {
async _importDictionaries(dictionaries, profilesDictionarySettings, onImportDone, importProgressTracker) {
if (this._modifying) { return; }

const statusFooter = this._statusFooter;
Expand Down Expand Up @@ -578,6 +584,7 @@ export class DictionaryImportController {
if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); }
this._setModifying(false);
this._triggerStorageChanged();
if (onImportDone) { onImportDone(); }
}
}

Expand Down
29 changes: 29 additions & 0 deletions types/ext/dictionary-controller.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023-2024 Yomitan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

type DictionaryDeleteTask = {
type: 'delete';
dictionaryTitle: string;
};

type DictionaryUpdateTask = {
type: 'update';
dictionaryTitle: string;
downloadUrl: string | undefined;
};

export type DictionaryTask = DictionaryDeleteTask | DictionaryUpdateTask;
3 changes: 3 additions & 0 deletions types/ext/settings-controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type ProfileDictionarySettings = Settings.DictionaryOptions & {index: number};

export type ProfilesDictionarySettings = {[profileId: string]: ProfileDictionarySettings} | null;

export type ImportDictionaryDoneCallback = (() => void) | null;

export type Events = {
optionsChanged: {
options: Settings.ProfileOptions;
Expand All @@ -47,6 +49,7 @@ export type Events = {
importDictionaryFromUrl: {
url: string;
profilesDictionarySettings: ProfilesDictionarySettings;
onImportDone: ImportDictionaryDoneCallback;
};
dictionaryEnabled: Record<string, never>;
scanInputsChanged: {
Expand Down