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

support updatable dictionaries #1174

Merged
merged 22 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/css/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -2323,7 +2323,7 @@ input[type=number].dictionary-priority {
margin-top: 0;
margin-right: 0.5em;
}
.dictionary-outdated-button,
.dictionary-update-available,
.dictionary-integrity-button {
--button-content-color: transparent;
--button-border-color: transparent;
Expand Down
22 changes: 19 additions & 3 deletions ext/data/schemas/dictionary-index-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"revision": {
"type": "string",
"description": "Revision of the dictionary. This value is only used for displaying information."
"description": "Revision of the dictionary. This value is displayed, and used to check for dictionary updates."
StefanVukovic99 marked this conversation as resolved.
Show resolved Hide resolved
},
"sequenced": {
"type": "boolean",
Expand All @@ -42,9 +42,22 @@
"type": "string",
"description": "Creator of the dictionary."
},
"isUpdatable": {
"type": "boolean",
"const": true,
"description": "Whether this dictionary contains links to its latest version."
},
"indexUrl": {
"type": "string",
"description": "URL for the index file of the latest revision of the dictionary, used to check for updates."
},
"downloadUrl": {
jamesmaa marked this conversation as resolved.
Show resolved Hide resolved
"type": "string",
"description": "URL for the download of the latest revision of the dictionary."
},
"url": {
"type": "string",
"description": "URL for the source of the dictionary."
"description": "URL for the source of the dictionary, displayed in the dictionary details."
},
"description": {
"type": "string",
Expand Down Expand Up @@ -101,5 +114,8 @@
{
"required": ["version"]
}
]
],
"dependencies": {
"isUpdatable": ["indexUrl", "downloadUrl"]
}
}
32 changes: 30 additions & 2 deletions ext/js/dictionary/dictionary-importer.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export class DictionaryImporter {
* @param {import('dictionary-data').Index} index
* @param {import('dictionary-importer').SummaryDetails} details
* @returns {import('dictionary-importer').Summary}
* @throws {Error}
*/
_createSummary(dictionaryTitle, version, index, details) {
const indexSequenced = index.sequenced;
Expand All @@ -303,18 +304,45 @@ export class DictionaryImporter {
styles,
};

const {author, url, description, attribution, frequencyMode, sourceLanguage, targetLanguage} = index;
const {author, url, description, attribution, frequencyMode, isUpdatable, sourceLanguage, targetLanguage} = index;
if (typeof author === 'string') { summary.author = author; }
if (typeof url === 'string') { summary.url = url; }
if (typeof description === 'string') { summary.description = description; }
if (typeof attribution === 'string') { summary.attribution = attribution; }
if (typeof frequencyMode === 'string') { summary.frequencyMode = frequencyMode; }
if (typeof sourceLanguage === 'string') { summary.sourceLanguage = sourceLanguage; }
if (typeof targetLanguage === 'string') { summary.targetLanguage = targetLanguage; }

if (typeof isUpdatable === 'boolean') {
const {indexUrl, downloadUrl} = index;
if (!isUpdatable || !this._validateUrl(indexUrl) || !this._validateUrl(downloadUrl)) {
StefanVukovic99 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('Invalid index data for updatable dictionary');
}
summary.isUpdatable = isUpdatable;
summary.indexUrl = indexUrl;
summary.downloadUrl = downloadUrl;
}
return summary;
}

/**
* @param {string|undefined} string
* @returns {boolean}
*/
_validateUrl(string) {
if (typeof string !== 'string') {
return false;
}

let url;
try {
url = new URL(string);
} catch (_) {
return false;
}

return url.protocol === 'http:' || url.protocol === 'https:';
}

/**
* @param {import('ajv').ValidateFunction} schema
* @param {string} fileName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class CollapsibleDictionaryController {
nameNode.textContent = dictionary;

/** @type {HTMLElement} */
const versionNode = querySelectorNotNull(node, '.dictionary-version');
const versionNode = querySelectorNotNull(node, '.dictionary-revision');
versionNode.textContent = version;

return querySelectorNotNull(node, '.definitions-collapsible');
Expand Down
145 changes: 142 additions & 3 deletions ext/js/pages/settings/dictionary-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import * as ajvSchemas0 from '../../../lib/validate-schemas.js';
import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {readResponseJson} from '../../core/json.js';
import {log} from '../../core/log.js';
import {DictionaryWorker} from '../../dictionary/dictionary-worker.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';

const ajvSchemas = /** @type {import('dictionary-importer').CompiledSchemaValidators} */ (/** @type {unknown} */ (ajvSchemas0));

class DictionaryEntry {
/**
* @param {DictionaryController} dictionaryController
Expand Down Expand Up @@ -55,10 +59,12 @@
this._outdatedButton = querySelectorNotNull(fragment, '.dictionary-outdated-button');
/** @type {HTMLButtonElement} */
this._integrityButton = querySelectorNotNull(fragment, '.dictionary-integrity-button');
/** @type {HTMLButtonElement} */
this._updatesAvailable = querySelectorNotNull(fragment, '.dictionary-update-available');
/** @type {HTMLElement} */
this._titleNode = querySelectorNotNull(fragment, '.dictionary-title');
/** @type {HTMLElement} */
this._versionNode = querySelectorNotNull(fragment, '.dictionary-version');
this._versionNode = querySelectorNotNull(fragment, '.dictionary-revision');
/** @type {HTMLElement} */
this._titleContainer = querySelectorNotNull(fragment, '.dictionary-item-title-container');
}
Expand All @@ -70,6 +76,7 @@

/** */
prepare() {
//
const index = this._index;
const {title, revision, version} = this._dictionaryInfo;

Expand All @@ -85,6 +92,7 @@
this._eventListeners.addEventListener(this._downButton, 'click', (() => { this._move(1); }).bind(this), false);
this._eventListeners.addEventListener(this._outdatedButton, 'click', this._onOutdatedButtonClick.bind(this), false);
this._eventListeners.addEventListener(this._integrityButton, 'click', this._onIntegrityButtonClick.bind(this), false);
this._eventListeners.addEventListener(this._updatesAvailable, 'click', this._onUpdateButtonClick.bind(this), false);
}

/** */
Expand Down Expand Up @@ -113,8 +121,60 @@
this._enabledCheckbox.checked = value;
}

/**
* @returns {Promise<void>}
*/
async checkForUpdate() {
this._updatesAvailable.hidden = true;
const {isUpdatable, indexUrl, revision} = this._dictionaryInfo;
if (!isUpdatable || !indexUrl) { return; }
const response = await fetch(indexUrl);

/** @type {unknown} */
const index = await readResponseJson(response);

if (!ajvSchemas.dictionaryIndex(index)) {
throw new Error('Invalid dictionary index');
}

const validIndex = /** @type {import('dictionary-data').Index} */ (index);

if (!this._compareRevisions(revision, validIndex.revision)) {
return;
}

this._updatesAvailable.hidden = false;
}

// Private

/**
* @param {string} current
* @param {string} latest
* @returns {boolean}
*/
_compareRevisions(current, latest) {
StefanVukovic99 marked this conversation as resolved.
Show resolved Hide resolved
const isSimpleVersion = /^(\d+.)*\d+$/.test(current) && /^(\d+.)*\d+$/.test(latest);
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
if (!isSimpleVersion) {
return current < latest;
}

const currentParts = current.split('.').map((part) => Number.parseInt(part, 10));
const latestParts = latest.split('.').map((part) => Number.parseInt(part, 10));

if (currentParts.length !== latestParts.length) {
return current < latest;
}

for (let i = 0; i < currentParts.length; i++) {
if (currentParts[i] !== latestParts[i]) {
return currentParts[i] < latestParts[i];
}
}

return false;
}

/**
* @param {import('popup-menu').MenuOpenEvent} e
*/
Expand Down Expand Up @@ -155,6 +215,11 @@
this._showDetails();
}

/** */
_onUpdateButtonClick() {
this._dictionaryController.updateDictionary(this.dictionaryTitle);
}

/** */
_onIntegrityButtonClick() {
this._showDetails();
Expand All @@ -170,7 +235,7 @@
/** @type {HTMLElement} */
const titleElement = querySelectorNotNull(modal.node, '.dictionary-title');
/** @type {HTMLElement} */
const versionElement = querySelectorNotNull(modal.node, '.dictionary-version');
const versionElement = querySelectorNotNull(modal.node, '.dictionary-revision');
/** @type {HTMLElement} */
const outdateElement = querySelectorNotNull(modal.node, '.dictionary-outdated-notification');
/** @type {HTMLElement} */
Expand Down Expand Up @@ -393,8 +458,12 @@
/** @type {?import('core').TokenObject} */
this._databaseStateToken = null;
/** @type {boolean} */
this._checkingUpdates = false;
/** @type {boolean} */
this._checkingIntegrity = false;
/** @type {?HTMLButtonElement} */
this._checkUpdatesButton = document.querySelector('#dictionary-check-updates');
/** @type {?HTMLButtonElement} */
this._checkIntegrityButton = document.querySelector('#dictionary-check-integrity');
/** @type {HTMLElement} */
this._dictionaryEntryContainer = querySelectorNotNull(document, '#dictionary-list');
Expand All @@ -408,6 +477,8 @@
this._noDictionariesEnabledWarnings = null;
/** @type {?import('./modal.js').Modal} */
this._deleteDictionaryModal = null;
/** @type {?import('./modal.js').Modal} */
this._updateDictionaryModal = null;
/** @type {HTMLInputElement} */
this._allCheckbox = querySelectorNotNull(document, '#all-dictionaries-enabled');
/** @type {?DictionaryExtraInfo} */
Expand All @@ -431,16 +502,25 @@
this._noDictionariesInstalledWarnings = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.no-dictionaries-installed-warning'));
this._noDictionariesEnabledWarnings = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.no-dictionaries-enabled-warning'));
this._deleteDictionaryModal = this._modalController.getModal('dictionary-confirm-delete');
this._updateDictionaryModal = this._modalController.getModal('dictionary-confirm-update');
/** @type {HTMLButtonElement} */
const dictionaryDeleteButton = querySelectorNotNull(document, '#dictionary-confirm-delete-button');
/** @type {HTMLButtonElement} */
const dictionaryUpdateButton = querySelectorNotNull(document, '#dictionary-confirm-update-button');

/** @type {HTMLButtonElement} */
const dictionaryMoveButton = querySelectorNotNull(document, '#dictionary-move-button');

this._settingsController.application.on('databaseUpdated', this._onDatabaseUpdated.bind(this));
this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
this._allCheckbox.addEventListener('change', this._onAllCheckboxChange.bind(this), false);
dictionaryDeleteButton.addEventListener('click', this._onDictionaryConfirmDelete.bind(this), false);
dictionaryUpdateButton.addEventListener('click', this._onDictionaryConfirmUpdate.bind(this), false);

dictionaryMoveButton.addEventListener('click', this._onDictionaryMoveButtonClick.bind(this), false);
if (this._checkUpdatesButton !== null) {
this._checkUpdatesButton.addEventListener('click', this._onCheckUpdatesButtonClick.bind(this), false);
}
if (this._checkIntegrityButton !== null) {
this._checkIntegrityButton.addEventListener('click', this._onCheckIntegrityButtonClick.bind(this), false);
}
Expand All @@ -463,6 +543,19 @@
modal.setVisible(true);
}

/**
* @param {string} dictionaryTitle
*/
updateDictionary(dictionaryTitle) {
StefanVukovic99 marked this conversation as resolved.
Show resolved Hide resolved
const modal = this._updateDictionaryModal;
if (modal === null) { return; }
modal.node.dataset.dictionaryTitle = dictionaryTitle;
/** @type {Element} */
const nameElement = querySelectorNotNull(modal.node, '#dictionary-confirm-update-name');
nameElement.textContent = dictionaryTitle;
modal.setVisible(true);
}

/**
* @param {number} currentIndex
* @param {number} targetIndex
Expand Down Expand Up @@ -724,6 +817,22 @@
void this._deleteDictionary(title);
}

/**
* @param {MouseEvent} e
*/
_onDictionaryConfirmUpdate(e) {
e.preventDefault();

const modal = /** @type {import('./modal.js').Modal} */ (this._updateDictionaryModal);
modal.setVisible(false);

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

void this._updateDictionary(title);
}

/**
* @param {MouseEvent} e
*/
Expand All @@ -732,6 +841,14 @@
void this._checkIntegrity();
}

/**
* @param {MouseEvent} e
*/
_onCheckUpdatesButtonClick(e) {
e.preventDefault();
void this._checkForUpdates();
}

/** */
_onDictionaryMoveButtonClick() {
const modal = /** @type {import('./modal.js').Modal} */ (this._modalController.getModal('dictionary-move-location'));
Expand Down Expand Up @@ -775,9 +892,24 @@
}
}

/** */
async _checkForUpdates() {
StefanVukovic99 marked this conversation as resolved.
Show resolved Hide resolved
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isDeleting) { return; }
try {
this._checkingUpdates = true;
this._setButtonsEnabled(false);

const updateChecks = this._dictionaryEntries.map((entry) => entry.checkForUpdate());
await Promise.all(updateChecks);
} finally {
this._setButtonsEnabled(true);
this._checkingUpdates = false;
}
}

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

try {
this._checkingIntegrity = true;
Expand Down Expand Up @@ -903,6 +1035,13 @@
}
}

/**
* @param {string} dictionaryTitle
*/
async _updateDictionary(dictionaryTitle) {
console.log('Updating dictionary:', dictionaryTitle);

Check failure on line 1042 in ext/js/pages/settings/dictionary-controller.js

View workflow job for this annotation

GitHub Actions / Static Analysis

Unexpected console statement
}

/**
* @param {boolean} value
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class SecondarySearchDictionaryController {
nameNode.textContent = name;

/** @type {HTMLElement} */
const versionNode = querySelectorNotNull(node, '.dictionary-version');
const versionNode = querySelectorNotNull(node, '.dictionary-revision');
versionNode.textContent = `rev.${dictionaryInfo.revision}`;

/** @type {HTMLElement} */
Expand Down
Loading
Loading