mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-07 03:22:17 -08:00
feat: add AniList character dictionary sync
This commit is contained in:
@@ -36,6 +36,8 @@ export class DisplayContentManager {
|
||||
this._eventListeners = new EventListenerCollection();
|
||||
/** @type {import('display-content-manager').LoadMediaRequest[]} */
|
||||
this._loadMediaRequests = [];
|
||||
/** @type {string[]} */
|
||||
this._mediaUrls = [];
|
||||
}
|
||||
|
||||
/** @type {import('display-content-manager').LoadMediaRequest[]} */
|
||||
@@ -47,10 +49,10 @@ export class DisplayContentManager {
|
||||
* Queues loading media file from a given dictionary.
|
||||
* @param {string} path
|
||||
* @param {string} dictionary
|
||||
* @param {OffscreenCanvas} canvas
|
||||
* @param {HTMLImageElement|HTMLCanvasElement} element
|
||||
*/
|
||||
loadMedia(path, dictionary, canvas) {
|
||||
this._loadMediaRequests.push({path, dictionary, canvas});
|
||||
loadMedia(path, dictionary, element) {
|
||||
this._loadMediaRequests.push({path, dictionary, element});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +64,10 @@ export class DisplayContentManager {
|
||||
this._eventListeners.removeAllEventListeners();
|
||||
|
||||
this._loadMediaRequests = [];
|
||||
for (const mediaUrl of this._mediaUrls) {
|
||||
URL.revokeObjectURL(mediaUrl);
|
||||
}
|
||||
this._mediaUrls = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +89,70 @@ export class DisplayContentManager {
|
||||
* Execute media requests
|
||||
*/
|
||||
async executeMediaRequests() {
|
||||
this._display.application.api.drawMedia(this._loadMediaRequests, this._loadMediaRequests.map(({canvas}) => canvas));
|
||||
const token = this._token;
|
||||
for (const {path, dictionary, element} of this._loadMediaRequests) {
|
||||
try {
|
||||
const data = await this._display.application.api.getMedia([{path, dictionary}]);
|
||||
if (this._token !== token) { return; }
|
||||
|
||||
const item = data[0];
|
||||
if (
|
||||
typeof item !== 'object' ||
|
||||
item === null ||
|
||||
typeof item.content !== 'string' ||
|
||||
typeof item.mediaType !== 'string'
|
||||
) {
|
||||
this._setMediaElementState(element, 'load-error');
|
||||
continue;
|
||||
}
|
||||
|
||||
const buffer = base64ToArrayBuffer(item.content);
|
||||
const blob = new Blob([buffer], {type: item.mediaType});
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
if (element instanceof HTMLImageElement) {
|
||||
this._mediaUrls.push(blobUrl);
|
||||
element.onload = () => {
|
||||
if (this._token !== token) { return; }
|
||||
this._setMediaElementState(element, 'loaded');
|
||||
};
|
||||
element.onerror = () => {
|
||||
if (this._token !== token) { return; }
|
||||
this._setMediaElementState(element, 'load-error');
|
||||
};
|
||||
element.src = blobUrl;
|
||||
continue;
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
try {
|
||||
if (this._token !== token) { return; }
|
||||
const context = element.getContext('2d');
|
||||
if (context === null) {
|
||||
this._setMediaElementState(element, 'load-error');
|
||||
return;
|
||||
}
|
||||
element.width = image.naturalWidth || element.width;
|
||||
element.height = image.naturalHeight || element.height;
|
||||
context.drawImage(image, 0, 0, element.width, element.height);
|
||||
this._setMediaElementState(element, 'loaded');
|
||||
} finally {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
};
|
||||
image.onerror = () => {
|
||||
if (this._token === token) {
|
||||
this._setMediaElementState(element, 'load-error');
|
||||
}
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
image.src = blobUrl;
|
||||
} catch (_e) {
|
||||
if (this._token !== token) { return; }
|
||||
this._setMediaElementState(element, 'load-error');
|
||||
}
|
||||
}
|
||||
this._loadMediaRequests = [];
|
||||
}
|
||||
|
||||
@@ -127,4 +196,17 @@ export class DisplayContentManager {
|
||||
content: null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLImageElement|HTMLCanvasElement} element
|
||||
* @param {'loaded'|'load-error'} state
|
||||
*/
|
||||
_setMediaElementState(element, state) {
|
||||
const link = element.closest('.gloss-image-link');
|
||||
if (link === null) { return; }
|
||||
link.dataset.imageLoadState = state;
|
||||
if (state === 'loaded') {
|
||||
link.dataset.hasImage = 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,9 +148,7 @@ export class StructuredContentGenerator {
|
||||
}
|
||||
|
||||
if (this._contentManager !== null) {
|
||||
const image = this._contentManager instanceof DisplayContentManager ?
|
||||
/** @type {HTMLCanvasElement} */ (this._createElement('canvas', 'gloss-image')) :
|
||||
/** @type {HTMLImageElement} */ (this._createElement('img', 'gloss-image'));
|
||||
const image = /** @type {HTMLImageElement} */ (this._createElement('img', 'gloss-image'));
|
||||
if (sizeUnits === 'em' && (hasPreferredWidth || hasPreferredHeight)) {
|
||||
const emSize = 14; // We could Number.parseFloat(getComputedStyle(document.documentElement).fontSize); here for more accuracy but it would cause a layout and be extremely slow; possible improvement would be to calculate and cache the value
|
||||
const scaleFactor = 2 * this._window.devicePixelRatio;
|
||||
@@ -172,7 +170,7 @@ export class StructuredContentGenerator {
|
||||
this._contentManager.loadMedia(
|
||||
path,
|
||||
dictionary,
|
||||
(/** @type {HTMLCanvasElement} */(image)).transferControlToOffscreen(),
|
||||
image,
|
||||
);
|
||||
} else if (this._contentManager instanceof AnkiTemplateRendererContentManager) {
|
||||
this._contentManager.loadMedia(
|
||||
|
||||
@@ -720,6 +720,24 @@ export class DictionaryController {
|
||||
modal.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dictionaryTitle
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteDictionaryNow(dictionaryTitle) {
|
||||
const dictionaries = await this._settingsController.getDictionaryInfo();
|
||||
if (!dictionaries.some((dictionary) => dictionary.title === dictionaryTitle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._deleteDictionary(dictionaryTitle);
|
||||
|
||||
const remaining = await this._settingsController.getDictionaryInfo();
|
||||
if (remaining.some((dictionary) => dictionary.title === dictionaryTitle)) {
|
||||
throw new Error(`Dictionary still present after delete: ${dictionaryTitle}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dictionaryTitle
|
||||
* @returns {Promise<string[]>}
|
||||
|
||||
@@ -452,6 +452,25 @@ export class DictionaryImportController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} archiveContent
|
||||
* @param {string} fileName
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async importDictionaryArchiveContent(archiveContent, fileName='dictionary.zip') {
|
||||
const file = new File([archiveContent], fileName, {type: 'application/zip'});
|
||||
const importProgressTracker = new ImportProgressTracker(this._getFileImportSteps(), 1);
|
||||
const errors = await this._importDictionaries(
|
||||
this._arrayToAsyncGenerator([file]),
|
||||
null,
|
||||
null,
|
||||
importProgressTracker,
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
throw errors[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} urls
|
||||
* @param {import('dictionary-worker').ImportProgressCallback} onProgress
|
||||
@@ -532,9 +551,10 @@ export class DictionaryImportController {
|
||||
* @param {import('settings-controller').ProfilesDictionarySettings} profilesDictionarySettings
|
||||
* @param {import('settings-controller').ImportDictionaryDoneCallback} onImportDone
|
||||
* @param {ImportProgressTracker} importProgressTracker
|
||||
* @returns {Promise<Error[]>}
|
||||
*/
|
||||
async _importDictionaries(dictionaries, profilesDictionarySettings, onImportDone, importProgressTracker) {
|
||||
if (this._modifying) { return; }
|
||||
if (this._modifying) { return [new Error('Dictionary import already in progress.')]; }
|
||||
|
||||
const statusFooter = this._statusFooter;
|
||||
const progressSelector = '.dictionary-import-progress';
|
||||
@@ -588,6 +608,7 @@ export class DictionaryImportController {
|
||||
this._triggerStorageChanged();
|
||||
if (onImportDone) { onImportDone(); }
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,6 +113,21 @@ await Application.main(true, async (application) => {
|
||||
const dictionaryImportController = new DictionaryImportController(settingsController, modalController, statusFooter);
|
||||
dictionaryImportController.prepare();
|
||||
|
||||
globalThis.__subminerYomitanSettingsAutomation = {
|
||||
ready: false,
|
||||
importDictionaryArchiveBase64: async (archiveBase64, fileName='dictionary.zip') => {
|
||||
const binary = atob(archiveBase64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; ++i) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
await dictionaryImportController.importDictionaryArchiveContent(bytes.buffer, fileName);
|
||||
},
|
||||
deleteDictionary: async (dictionaryTitle) => {
|
||||
await dictionaryController.deleteDictionaryNow(dictionaryTitle);
|
||||
},
|
||||
};
|
||||
|
||||
const genericSettingController = new GenericSettingController(settingsController);
|
||||
preparePromises.push(setupGenericSettingController(genericSettingController));
|
||||
|
||||
@@ -184,5 +199,6 @@ await Application.main(true, async (application) => {
|
||||
|
||||
await Promise.all(preparePromises);
|
||||
|
||||
globalThis.__subminerYomitanSettingsAutomation.ready = true;
|
||||
document.documentElement.dataset.loaded = 'true';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user