DisplayAnki (#1791)
* Remove anki functionality from display * Rename function * Display API updates * Use DisplayAnki * TODO
This commit is contained in:
parent
82b7ebfa58
commit
ea47cb8248
582
ext/js/display/display-anki.js
Normal file
582
ext/js/display/display-anki.js
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Yomichan 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global
|
||||||
|
* AnkiNoteBuilder
|
||||||
|
* AnkiUtil
|
||||||
|
* DisplayNotification
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DisplayAnki {
|
||||||
|
constructor(display) {
|
||||||
|
this._display = display;
|
||||||
|
this._ankiFieldTemplates = null;
|
||||||
|
this._ankiFieldTemplatesDefault = null;
|
||||||
|
this._ankiNoteBuilder = new AnkiNoteBuilder();
|
||||||
|
this._ankiNoteNotification = null;
|
||||||
|
this._ankiNoteNotificationEventListeners = null;
|
||||||
|
this._ankiTagNotification = null;
|
||||||
|
this._updateAdderButtonsPromise = Promise.resolve();
|
||||||
|
this._updateAdderButtonsToken = null;
|
||||||
|
this._eventListeners = new EventListenerCollection();
|
||||||
|
this._checkForDuplicates = false;
|
||||||
|
this._suspendNewCards = false;
|
||||||
|
this._compactTags = false;
|
||||||
|
this._resultOutputMode = 'split';
|
||||||
|
this._glossaryLayoutMode = 'default';
|
||||||
|
this._displayTags = 'never';
|
||||||
|
this._duplicateScope = 'collection';
|
||||||
|
this._screenshotFormat = 'png';
|
||||||
|
this._screenshotQuality = 100;
|
||||||
|
this._noteTags = [];
|
||||||
|
this._modeOptions = new Map();
|
||||||
|
this._onShowTagsBind = this._onShowTags.bind(this);
|
||||||
|
this._onNoteAddBind = this._onNoteAdd.bind(this);
|
||||||
|
this._onNoteViewBind = this._onNoteView.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare() {
|
||||||
|
this._display.hotkeyHandler.registerActions([
|
||||||
|
['addNoteKanji', () => { this._tryAddAnkiNoteForSelectedEntry('kanji'); }],
|
||||||
|
['addNoteTermKanji', () => { this._tryAddAnkiNoteForSelectedEntry('term-kanji'); }],
|
||||||
|
['addNoteTermKana', () => { this._tryAddAnkiNoteForSelectedEntry('term-kana'); }],
|
||||||
|
['viewNote', () => { this._tryViewAnkiNoteForSelectedEntry(); }]
|
||||||
|
]);
|
||||||
|
this._display.on('optionsUpdated', this._onOptionsUpdated.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupEntries() {
|
||||||
|
this._updateAdderButtonsToken = null;
|
||||||
|
this._hideAnkiNoteErrors(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEntry(entry) {
|
||||||
|
this._addMultipleEventListeners(entry, '.action-view-tags', 'click', this._onShowTagsBind);
|
||||||
|
this._addMultipleEventListeners(entry, '.action-add-note', 'click', this._onNoteAddBind);
|
||||||
|
this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteViewBind);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEntriesComplete(isTerms, dictionaryEntries) { // TODO : Don't pass (isTerms, dictionaryEntries)
|
||||||
|
this._updateAdderButtons(isTerms, dictionaryEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLogData(dictionaryEntry) {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
// Anki note data
|
||||||
|
let ankiNoteData;
|
||||||
|
let ankiNoteDataException;
|
||||||
|
try {
|
||||||
|
const context = this._getNoteContext();
|
||||||
|
ankiNoteData = await this._ankiNoteBuilder.getRenderingData({
|
||||||
|
dictionaryEntry,
|
||||||
|
mode: 'test',
|
||||||
|
context,
|
||||||
|
resultOutputMode: this.resultOutputMode,
|
||||||
|
glossaryLayoutMode: this._glossaryLayoutMode,
|
||||||
|
compactTags: this._compactTags,
|
||||||
|
injectedMedia: null,
|
||||||
|
marker: 'test'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
ankiNoteDataException = e;
|
||||||
|
}
|
||||||
|
result.ankiNoteData = ankiNoteData;
|
||||||
|
if (typeof ankiNoteDataException !== 'undefined') {
|
||||||
|
result.ankiNoteDataException = ankiNoteDataException;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anki notes
|
||||||
|
const ankiNotes = [];
|
||||||
|
const modes = this._getModes(dictionaryEntry.type === 'term');
|
||||||
|
for (const mode of modes) {
|
||||||
|
let note;
|
||||||
|
let errors;
|
||||||
|
try {
|
||||||
|
const noteContext = this._getNoteContext();
|
||||||
|
({note: note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, false));
|
||||||
|
} catch (e) {
|
||||||
|
errors = [e];
|
||||||
|
}
|
||||||
|
const entry = {mode, note};
|
||||||
|
if (Array.isArray(errors) && errors.length > 0) {
|
||||||
|
entry.errors = errors;
|
||||||
|
}
|
||||||
|
ankiNotes.push(entry);
|
||||||
|
}
|
||||||
|
result.ankiNotes = ankiNotes;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
|
||||||
|
_onOptionsUpdated({options}) {
|
||||||
|
const {
|
||||||
|
general: {resultOutputMode, glossaryLayoutMode, compactTags},
|
||||||
|
anki: {tags, duplicateScope, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, screenshot: {format, quality}}
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
this._checkForDuplicates = checkForDuplicates;
|
||||||
|
this._suspendNewCards = suspendNewCards;
|
||||||
|
this._compactTags = compactTags;
|
||||||
|
this._resultOutputMode = resultOutputMode;
|
||||||
|
this._glossaryLayoutMode = glossaryLayoutMode;
|
||||||
|
this._displayTags = displayTags;
|
||||||
|
this._duplicateScope = duplicateScope;
|
||||||
|
this._screenshotFormat = format;
|
||||||
|
this._screenshotQuality = quality;
|
||||||
|
this._noteTags = [...tags];
|
||||||
|
this._modeOptions.clear();
|
||||||
|
this._modeOptions.set('kanji', kanji);
|
||||||
|
this._modeOptions.set('term-kanji', terms);
|
||||||
|
this._modeOptions.set('term-kana', terms);
|
||||||
|
|
||||||
|
this._updateAnkiFieldTemplates(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onNoteAdd(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const node = e.currentTarget;
|
||||||
|
const index = this._display.getElementDictionaryEntryIndex(node);
|
||||||
|
this._addAnkiNote(index, node.dataset.mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onShowTags(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const tags = e.currentTarget.title;
|
||||||
|
this._showAnkiTagsNotification(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onNoteView(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const link = e.currentTarget;
|
||||||
|
yomichan.api.noteView(link.dataset.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_addMultipleEventListeners(container, selector, ...args) {
|
||||||
|
for (const node of container.querySelectorAll(selector)) {
|
||||||
|
this._eventListeners.addEventListener(node, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_adderButtonFind(index, mode) {
|
||||||
|
const entry = this._getEntry(index);
|
||||||
|
return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tagsIndicatorFind(index) {
|
||||||
|
const entry = this._getEntry(index);
|
||||||
|
return entry !== null ? entry.querySelector('.action-view-tags') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewerButtonFind(index) {
|
||||||
|
const entry = this._getEntry(index);
|
||||||
|
return entry !== null ? entry.querySelector('.action-view-note') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getEntry(index) {
|
||||||
|
const entries = this._display.dictionaryEntryNodes;
|
||||||
|
return index >= 0 && index < entries.length ? entries[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewerButtonShow(index, noteId) {
|
||||||
|
const viewerButton = this._viewerButtonFind(index);
|
||||||
|
if (viewerButton === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
viewerButton.disabled = false;
|
||||||
|
viewerButton.hidden = false;
|
||||||
|
viewerButton.dataset.noteId = noteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getNoteContext() {
|
||||||
|
const {state} = this._display.history;
|
||||||
|
let {documentTitle, url, sentence} = (isObject(state) ? state : {});
|
||||||
|
if (typeof documentTitle !== 'string') {
|
||||||
|
documentTitle = document.title;
|
||||||
|
}
|
||||||
|
if (typeof url !== 'string') {
|
||||||
|
url = window.location.href;
|
||||||
|
}
|
||||||
|
sentence = this._getValidSentenceData(sentence);
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
sentence,
|
||||||
|
documentTitle,
|
||||||
|
query: this._display.query,
|
||||||
|
fullQuery: this._display.fullQuery
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_getDictionaryEntryDetailsForNote(dictionaryEntry) {
|
||||||
|
const {type} = dictionaryEntry;
|
||||||
|
if (type === 'kanji') {
|
||||||
|
const {character} = dictionaryEntry;
|
||||||
|
return {type, character};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {headwords} = dictionaryEntry;
|
||||||
|
let bestIndex = -1;
|
||||||
|
for (let i = 0, ii = headwords.length; i < ii; ++i) {
|
||||||
|
const {term, reading, sources} = headwords[i];
|
||||||
|
for (const {deinflectedText} of sources) {
|
||||||
|
if (term === deinflectedText) {
|
||||||
|
bestIndex = i;
|
||||||
|
i = ii;
|
||||||
|
break;
|
||||||
|
} else if (reading === deinflectedText && bestIndex < 0) {
|
||||||
|
bestIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {term, reading} = headwords[Math.max(0, bestIndex)];
|
||||||
|
return {type, term, reading};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateAdderButtons(isTerms, dictionaryEntries) {
|
||||||
|
const token = {};
|
||||||
|
this._updateAdderButtonsToken = token;
|
||||||
|
await this._updateAdderButtonsPromise;
|
||||||
|
if (this._updateAdderButtonsToken !== token) { return; }
|
||||||
|
|
||||||
|
const {promise, resolve} = deferPromise();
|
||||||
|
try {
|
||||||
|
this._updateAdderButtonsPromise = promise;
|
||||||
|
|
||||||
|
const modes = this._getModes(isTerms);
|
||||||
|
let states;
|
||||||
|
try {
|
||||||
|
const noteContext = this._getNoteContext();
|
||||||
|
states = await this._areDictionaryEntriesAddable(
|
||||||
|
dictionaryEntries,
|
||||||
|
modes,
|
||||||
|
noteContext,
|
||||||
|
this._checkForDuplicates ? null : true,
|
||||||
|
this._displayTags !== 'never'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._updateAdderButtonsToken !== token) { return; }
|
||||||
|
|
||||||
|
this._updateAdderButtons2(states, modes);
|
||||||
|
} finally {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateAdderButtons2(states, modes) {
|
||||||
|
const displayTags = this._displayTags;
|
||||||
|
for (let i = 0, ii = states.length; i < ii; ++i) {
|
||||||
|
const infos = states[i];
|
||||||
|
let noteId = null;
|
||||||
|
for (let j = 0, jj = infos.length; j < jj; ++j) {
|
||||||
|
const {canAdd, noteIds, noteInfos} = infos[j];
|
||||||
|
const mode = modes[j];
|
||||||
|
const button = this._adderButtonFind(i, mode);
|
||||||
|
if (button === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(noteIds) && noteIds.length > 0) {
|
||||||
|
noteId = noteIds[0];
|
||||||
|
}
|
||||||
|
button.disabled = !canAdd;
|
||||||
|
button.hidden = false;
|
||||||
|
|
||||||
|
if (displayTags !== 'never' && Array.isArray(noteInfos)) {
|
||||||
|
this._setupTagsIndicator(i, noteInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (noteId !== null) {
|
||||||
|
this._viewerButtonShow(i, noteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupTagsIndicator(i, noteInfos) {
|
||||||
|
const tagsIndicator = this._tagsIndicatorFind(i);
|
||||||
|
if (tagsIndicator === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayTags = new Set();
|
||||||
|
for (const {tags} of noteInfos) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
displayTags.add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._displayTags === 'non-standard') {
|
||||||
|
for (const tag of this._noteTags) {
|
||||||
|
displayTags.delete(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayTags.size > 0) {
|
||||||
|
tagsIndicator.disabled = false;
|
||||||
|
tagsIndicator.hidden = false;
|
||||||
|
tagsIndicator.title = `Card tags: ${[...displayTags].join(', ')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_showAnkiTagsNotification(message) {
|
||||||
|
if (this._ankiTagNotification === null) {
|
||||||
|
const node = this._display.displayGenerator.createEmptyFooterNotification();
|
||||||
|
node.classList.add('click-scannable');
|
||||||
|
this._ankiTagNotification = new DisplayNotification(this._display.notificationContainer, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._ankiTagNotification.setContent(message);
|
||||||
|
this._ankiTagNotification.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_tryAddAnkiNoteForSelectedEntry(mode) {
|
||||||
|
const index = this._display.selectedIndex;
|
||||||
|
this._addAnkiNote(index, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tryViewAnkiNoteForSelectedEntry() {
|
||||||
|
const index = this._display.selectedIndex;
|
||||||
|
const button = this._viewerButtonFind(index);
|
||||||
|
if (button !== null && !button.disabled) {
|
||||||
|
yomichan.api.noteView(button.dataset.noteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _addAnkiNote(dictionaryEntryIndex, mode) {
|
||||||
|
const dictionaryEntries = this._display.dictionaryEntries;
|
||||||
|
if (dictionaryEntryIndex < 0 || dictionaryEntryIndex >= dictionaryEntries.length) { return; }
|
||||||
|
const dictionaryEntry = dictionaryEntries[dictionaryEntryIndex];
|
||||||
|
|
||||||
|
const button = this._adderButtonFind(dictionaryEntryIndex, mode);
|
||||||
|
if (button === null || button.disabled) { return; }
|
||||||
|
|
||||||
|
this._hideAnkiNoteErrors(true);
|
||||||
|
|
||||||
|
const allErrors = [];
|
||||||
|
const progressIndicatorVisible = this._display.progressIndicatorVisible;
|
||||||
|
const overrideToken = progressIndicatorVisible.setOverride(true);
|
||||||
|
try {
|
||||||
|
const noteContext = this._getNoteContext();
|
||||||
|
const {note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, true);
|
||||||
|
allErrors.push(...errors);
|
||||||
|
|
||||||
|
let noteId = null;
|
||||||
|
let addNoteOkay = false;
|
||||||
|
try {
|
||||||
|
noteId = await yomichan.api.addAnkiNote(note);
|
||||||
|
addNoteOkay = true;
|
||||||
|
} catch (e) {
|
||||||
|
allErrors.length = 0;
|
||||||
|
allErrors.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addNoteOkay) {
|
||||||
|
if (noteId === null) {
|
||||||
|
allErrors.push(new Error('Note could not be added'));
|
||||||
|
} else {
|
||||||
|
if (this._suspendNewCards) {
|
||||||
|
try {
|
||||||
|
await yomichan.api.suspendAnkiCardsForNote(noteId);
|
||||||
|
} catch (e) {
|
||||||
|
allErrors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.disabled = true;
|
||||||
|
this._viewerButtonShow(dictionaryEntryIndex, noteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
allErrors.push(e);
|
||||||
|
} finally {
|
||||||
|
progressIndicatorVisible.clearOverride(overrideToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allErrors.length > 0) {
|
||||||
|
this._showAnkiNoteErrors(allErrors);
|
||||||
|
} else {
|
||||||
|
this._hideAnkiNoteErrors(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_showAnkiNoteErrors(errors) {
|
||||||
|
if (this._ankiNoteNotificationEventListeners !== null) {
|
||||||
|
this._ankiNoteNotificationEventListeners.removeAllEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._ankiNoteNotification === null) {
|
||||||
|
const node = this._display.displayGenerator.createEmptyFooterNotification();
|
||||||
|
this._ankiNoteNotification = new DisplayNotification(this._display.notificationContainer, node);
|
||||||
|
this._ankiNoteNotificationEventListeners = new EventListenerCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
|
||||||
|
for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
|
||||||
|
this._ankiNoteNotificationEventListeners.addEventListener(node, 'click', () => {
|
||||||
|
console.log({ankiNoteErrors: errors});
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._ankiNoteNotification.setContent(content);
|
||||||
|
this._ankiNoteNotification.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideAnkiNoteErrors(animate) {
|
||||||
|
if (this._ankiNoteNotification === null) { return; }
|
||||||
|
this._ankiNoteNotification.close(animate);
|
||||||
|
this._ankiNoteNotificationEventListeners.removeAllEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateAnkiFieldTemplates(options) {
|
||||||
|
this._ankiFieldTemplates = await this._getAnkiFieldTemplates(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getAnkiFieldTemplates(options) {
|
||||||
|
let templates = options.anki.fieldTemplates;
|
||||||
|
if (typeof templates === 'string') { return templates; }
|
||||||
|
|
||||||
|
templates = this._ankiFieldTemplatesDefault;
|
||||||
|
if (typeof templates === 'string') { return templates; }
|
||||||
|
|
||||||
|
templates = await yomichan.api.getDefaultAnkiFieldTemplates();
|
||||||
|
this._ankiFieldTemplatesDefault = templates;
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue, fetchAdditionalInfo) {
|
||||||
|
const modeCount = modes.length;
|
||||||
|
const notePromises = [];
|
||||||
|
for (const dictionaryEntry of dictionaryEntries) {
|
||||||
|
for (const mode of modes) {
|
||||||
|
const notePromise = this._createNote(dictionaryEntry, mode, context, false);
|
||||||
|
notePromises.push(notePromise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const notes = (await Promise.all(notePromises)).map(({note}) => note);
|
||||||
|
|
||||||
|
let infos;
|
||||||
|
if (forceCanAddValue !== null) {
|
||||||
|
if (!await yomichan.api.isAnkiConnected()) {
|
||||||
|
throw new Error('Anki not connected');
|
||||||
|
}
|
||||||
|
infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue);
|
||||||
|
} else {
|
||||||
|
infos = await yomichan.api.getAnkiNoteInfo(notes, fetchAdditionalInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0, ii = infos.length; i < ii; i += modeCount) {
|
||||||
|
results.push(infos.slice(i, i + modeCount));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAnkiNoteInfoForceValue(notes, canAdd) {
|
||||||
|
const results = [];
|
||||||
|
for (const note of notes) {
|
||||||
|
const valid = AnkiUtil.isNoteDataValid(note);
|
||||||
|
results.push({canAdd, valid, noteIds: null});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _createNote(dictionaryEntry, mode, context, injectMedia) {
|
||||||
|
const modeOptions = this._modeOptions.get(mode);
|
||||||
|
if (typeof modeOptions === 'undefined') { throw new Error(`Unsupported note type: ${mode}`); }
|
||||||
|
const template = this._ankiFieldTemplates;
|
||||||
|
const {deck: deckName, model: modelName} = modeOptions;
|
||||||
|
const fields = Object.entries(modeOptions.fields);
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
let injectedMedia = null;
|
||||||
|
if (injectMedia) {
|
||||||
|
let errors2;
|
||||||
|
({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(dictionaryEntry, fields));
|
||||||
|
for (const error of errors2) {
|
||||||
|
errors.push(deserializeError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {note, errors: createNoteErrors} = await this._ankiNoteBuilder.createNote({
|
||||||
|
dictionaryEntry,
|
||||||
|
mode,
|
||||||
|
context,
|
||||||
|
template,
|
||||||
|
deckName,
|
||||||
|
modelName,
|
||||||
|
fields,
|
||||||
|
tags: this._noteTags,
|
||||||
|
checkForDuplicates: this._checkForDuplicates,
|
||||||
|
duplicateScope: this._duplicateScope,
|
||||||
|
resultOutputMode: this.resultOutputMode,
|
||||||
|
glossaryLayoutMode: this._glossaryLayoutMode,
|
||||||
|
compactTags: this._compactTags,
|
||||||
|
injectedMedia,
|
||||||
|
errors
|
||||||
|
});
|
||||||
|
errors.push(...createNoteErrors);
|
||||||
|
return {note, errors};
|
||||||
|
}
|
||||||
|
|
||||||
|
async _injectAnkiNoteMedia(dictionaryEntry, fields) {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
const dictionaryEntryDetails = this._getDictionaryEntryDetailsForNote(dictionaryEntry);
|
||||||
|
|
||||||
|
const audioDetails = (
|
||||||
|
dictionaryEntryDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio') ?
|
||||||
|
this._display.getAnkiNoteMediaAudioDetails(dictionaryEntryDetails.term, dictionaryEntryDetails.reading) :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const {tabId, frameId} = this._display.getContentOrigin();
|
||||||
|
const screenshotDetails = (
|
||||||
|
AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') && typeof tabId === 'number' ?
|
||||||
|
{tabId, frameId, format: this._screenshotFormat, quality: this._screenshotQuality} :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const clipboardDetails = {
|
||||||
|
image: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-image'),
|
||||||
|
text: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-text')
|
||||||
|
};
|
||||||
|
|
||||||
|
return await yomichan.api.injectAnkiNoteMedia(
|
||||||
|
timestamp,
|
||||||
|
dictionaryEntryDetails,
|
||||||
|
audioDetails,
|
||||||
|
screenshotDetails,
|
||||||
|
clipboardDetails
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getModes(isTerms) {
|
||||||
|
return isTerms ? ['term-kanji', 'term-kana'] : ['kanji'];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getValidSentenceData(sentence) {
|
||||||
|
let {text, offset} = (isObject(sentence) ? sentence : {});
|
||||||
|
if (typeof text !== 'string') { text = ''; }
|
||||||
|
if (typeof offset !== 'number') { offset = 0; }
|
||||||
|
return {text, offset};
|
||||||
|
}
|
||||||
|
}
|
@ -16,8 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* global
|
/* global
|
||||||
* AnkiNoteBuilder
|
* DisplayAnki
|
||||||
* AnkiUtil
|
|
||||||
* DisplayAudio
|
* DisplayAudio
|
||||||
* DisplayGenerator
|
* DisplayGenerator
|
||||||
* DisplayHistory
|
* DisplayHistory
|
||||||
@ -86,10 +85,6 @@ class Display extends EventDispatcher {
|
|||||||
getSearchContext: this._getSearchContext.bind(this),
|
getSearchContext: this._getSearchContext.bind(this),
|
||||||
documentUtil: this._documentUtil
|
documentUtil: this._documentUtil
|
||||||
});
|
});
|
||||||
this._ankiFieldTemplates = null;
|
|
||||||
this._ankiFieldTemplatesDefault = null;
|
|
||||||
this._ankiNoteBuilder = new AnkiNoteBuilder();
|
|
||||||
this._updateAdderButtonsPromise = Promise.resolve();
|
|
||||||
this._contentScrollElement = document.querySelector('#content-scroll');
|
this._contentScrollElement = document.querySelector('#content-scroll');
|
||||||
this._contentScrollBodyElement = document.querySelector('#content-body');
|
this._contentScrollBodyElement = document.querySelector('#content-body');
|
||||||
this._windowScroll = new ScrollElement(this._contentScrollElement);
|
this._windowScroll = new ScrollElement(this._contentScrollElement);
|
||||||
@ -111,12 +106,10 @@ class Display extends EventDispatcher {
|
|||||||
this._tagNotification = null;
|
this._tagNotification = null;
|
||||||
this._footerNotificationContainer = document.querySelector('#content-footer');
|
this._footerNotificationContainer = document.querySelector('#content-footer');
|
||||||
this._displayAudio = new DisplayAudio(this);
|
this._displayAudio = new DisplayAudio(this);
|
||||||
this._ankiNoteNotification = null;
|
|
||||||
this._ankiNoteNotificationEventListeners = null;
|
|
||||||
this._ankiTagNotification = null;
|
|
||||||
this._queryPostProcessor = null;
|
this._queryPostProcessor = null;
|
||||||
this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);
|
this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);
|
||||||
this._elementOverflowController = new ElementOverflowController();
|
this._elementOverflowController = new ElementOverflowController();
|
||||||
|
this._displayAnki = new DisplayAnki(this);
|
||||||
|
|
||||||
this._hotkeyHandler.registerActions([
|
this._hotkeyHandler.registerActions([
|
||||||
['close', () => { this._onHotkeyClose(); }],
|
['close', () => { this._onHotkeyClose(); }],
|
||||||
@ -126,10 +119,6 @@ class Display extends EventDispatcher {
|
|||||||
['firstEntry', () => { this._focusEntry(0, true); }],
|
['firstEntry', () => { this._focusEntry(0, true); }],
|
||||||
['historyBackward', () => { this._sourceTermView(); }],
|
['historyBackward', () => { this._sourceTermView(); }],
|
||||||
['historyForward', () => { this._nextTermView(); }],
|
['historyForward', () => { this._nextTermView(); }],
|
||||||
['addNoteKanji', () => { this._tryAddAnkiNoteForSelectedEntry('kanji'); }],
|
|
||||||
['addNoteTermKanji', () => { this._tryAddAnkiNoteForSelectedEntry('term-kanji'); }],
|
|
||||||
['addNoteTermKana', () => { this._tryAddAnkiNoteForSelectedEntry('term-kana'); }],
|
|
||||||
['viewNote', () => { this._tryViewAnkiNoteForSelectedEntry(); }],
|
|
||||||
['playAudio', () => { this._playAudioCurrent(); }],
|
['playAudio', () => { this._playAudioCurrent(); }],
|
||||||
['playAudioFromSource', this._onHotkeyActionPlayAudioFromSource.bind(this)],
|
['playAudioFromSource', this._onHotkeyActionPlayAudioFromSource.bind(this)],
|
||||||
['copyHostSelection', () => this._copyHostSelection()],
|
['copyHostSelection', () => this._copyHostSelection()],
|
||||||
@ -202,6 +191,22 @@ class Display extends EventDispatcher {
|
|||||||
return this._footerNotificationContainer;
|
return this._footerNotificationContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedIndex() {
|
||||||
|
return this._index;
|
||||||
|
}
|
||||||
|
|
||||||
|
get history() {
|
||||||
|
return this._history;
|
||||||
|
}
|
||||||
|
|
||||||
|
get query() {
|
||||||
|
return this._query;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fullQuery() {
|
||||||
|
return this._fullQuery;
|
||||||
|
}
|
||||||
|
|
||||||
async prepare() {
|
async prepare() {
|
||||||
// State setup
|
// State setup
|
||||||
const {documentElement} = document;
|
const {documentElement} = document;
|
||||||
@ -216,6 +221,7 @@ class Display extends EventDispatcher {
|
|||||||
await this._hotkeyHelpController.prepare();
|
await this._hotkeyHelpController.prepare();
|
||||||
await this._displayGenerator.prepare();
|
await this._displayGenerator.prepare();
|
||||||
this._displayAudio.prepare();
|
this._displayAudio.prepare();
|
||||||
|
this._displayAnki.prepare();
|
||||||
this._queryParser.prepare();
|
this._queryParser.prepare();
|
||||||
this._history.prepare();
|
this._history.prepare();
|
||||||
this._optionToggleHotkeyHandler.prepare();
|
this._optionToggleHotkeyHandler.prepare();
|
||||||
@ -291,10 +297,8 @@ class Display extends EventDispatcher {
|
|||||||
|
|
||||||
async updateOptions() {
|
async updateOptions() {
|
||||||
const options = await yomichan.api.optionsGet(this.getOptionsContext());
|
const options = await yomichan.api.optionsGet(this.getOptionsContext());
|
||||||
const templates = await this._getAnkiFieldTemplates(options);
|
|
||||||
const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options;
|
const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
this._ankiFieldTemplates = templates;
|
|
||||||
|
|
||||||
this._updateHotkeys(options);
|
this._updateHotkeys(options);
|
||||||
this._updateDocumentOptions(options);
|
this._updateDocumentOptions(options);
|
||||||
@ -441,6 +445,17 @@ class Display extends EventDispatcher {
|
|||||||
return await yomichan.crossFrame.invoke(this._parentFrameId, action, params);
|
return await yomichan.crossFrame.invoke(this._parentFrameId, action, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getElementDictionaryEntryIndex(element) {
|
||||||
|
const node = element.closest('.entry');
|
||||||
|
if (node === null) { return -1; }
|
||||||
|
const index = parseInt(node.dataset.index, 10);
|
||||||
|
return Number.isFinite(index) ? index : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAnkiNoteMediaAudioDetails(term, reading) {
|
||||||
|
return this._displayAudio.getAnkiNoteMediaAudioDetails(term, reading);
|
||||||
|
}
|
||||||
|
|
||||||
// Message handlers
|
// Message handlers
|
||||||
|
|
||||||
_onDirectMessage(data) {
|
_onDirectMessage(data) {
|
||||||
@ -530,8 +545,8 @@ class Display extends EventDispatcher {
|
|||||||
this._eventListeners.removeAllEventListeners();
|
this._eventListeners.removeAllEventListeners();
|
||||||
this._mediaLoader.unloadAll();
|
this._mediaLoader.unloadAll();
|
||||||
this._displayAudio.cleanupEntries();
|
this._displayAudio.cleanupEntries();
|
||||||
|
this._displayAnki.cleanupEntries();
|
||||||
this._hideTagNotification(false);
|
this._hideTagNotification(false);
|
||||||
this._hideAnkiNoteErrors(false);
|
|
||||||
this._dictionaryEntries = [];
|
this._dictionaryEntries = [];
|
||||||
this._dictionaryEntryNodes = [];
|
this._dictionaryEntryNodes = [];
|
||||||
this._elementOverflowController.clearElements();
|
this._elementOverflowController.clearElements();
|
||||||
@ -712,19 +727,6 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNoteAdd(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const link = e.currentTarget;
|
|
||||||
const index = this._getClosestDictionaryEntryIndex(link);
|
|
||||||
this._addAnkiNote(index, link.dataset.mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onNoteView(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const link = e.currentTarget;
|
|
||||||
yomichan.api.noteView(link.dataset.noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onWheel(e) {
|
_onWheel(e) {
|
||||||
if (e.altKey) {
|
if (e.altKey) {
|
||||||
if (e.deltaY !== 0) {
|
if (e.deltaY !== 0) {
|
||||||
@ -752,7 +754,7 @@ class Display extends EventDispatcher {
|
|||||||
|
|
||||||
_onDebugLogClick(e) {
|
_onDebugLogClick(e) {
|
||||||
const link = e.currentTarget;
|
const link = e.currentTarget;
|
||||||
const index = this._getClosestDictionaryEntryIndex(link);
|
const index = this.getElementDictionaryEntryIndex(link);
|
||||||
this._logDictionaryEntryData(index);
|
this._logDictionaryEntryData(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,7 +812,7 @@ class Display extends EventDispatcher {
|
|||||||
this._tagNotification = new DisplayNotification(this._footerNotificationContainer, node);
|
this._tagNotification = new DisplayNotification(this._footerNotificationContainer, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this._getClosestDictionaryEntryIndex(tagNode);
|
const index = this.getElementDictionaryEntryIndex(tagNode);
|
||||||
const dictionaryEntry = (index >= 0 && index < this._dictionaryEntries.length ? this._dictionaryEntries[index] : null);
|
const dictionaryEntry = (index >= 0 && index < this._dictionaryEntries.length ? this._dictionaryEntries[index] : null);
|
||||||
|
|
||||||
const content = this._displayGenerator.createTagFooterNotificationDetails(tagNode, dictionaryEntry);
|
const content = this._displayGenerator.createTagFooterNotificationDetails(tagNode, dictionaryEntry);
|
||||||
@ -960,6 +962,7 @@ class Display extends EventDispatcher {
|
|||||||
this._dictionaryEntryNodes.push(entry);
|
this._dictionaryEntryNodes.push(entry);
|
||||||
this._addEntryEventListeners(entry);
|
this._addEntryEventListeners(entry);
|
||||||
this._displayAudio.setupEntry(entry, i);
|
this._displayAudio.setupEntry(entry, i);
|
||||||
|
this._displayAnki.setupEntry(entry, i);
|
||||||
container.appendChild(entry);
|
container.appendChild(entry);
|
||||||
if (focusEntry === i) {
|
if (focusEntry === i) {
|
||||||
this._focusEntry(i, false);
|
this._focusEntry(i, false);
|
||||||
@ -977,8 +980,7 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._displayAudio.setupEntriesComplete();
|
this._displayAudio.setupEntriesComplete();
|
||||||
|
this._displayAnki.setupEntriesComplete(isTerms, dictionaryEntries);
|
||||||
this._updateAdderButtons(token, isTerms, dictionaryEntries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setContentExtensionUnloaded() {
|
_setContentExtensionUnloaded() {
|
||||||
@ -1065,104 +1067,6 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateAdderButtons(token, isTerms, dictionaryEntries) {
|
|
||||||
await this._updateAdderButtonsPromise;
|
|
||||||
if (this._setContentToken !== token) { return; }
|
|
||||||
|
|
||||||
const {promise, resolve} = deferPromise();
|
|
||||||
try {
|
|
||||||
this._updateAdderButtonsPromise = promise;
|
|
||||||
|
|
||||||
const modes = this._getModes(isTerms);
|
|
||||||
let states;
|
|
||||||
try {
|
|
||||||
const noteContext = this._getNoteContext();
|
|
||||||
const {checkForDuplicates, displayTags} = this._options.anki;
|
|
||||||
states = await this._areDictionaryEntriesAddable(dictionaryEntries, modes, noteContext, checkForDuplicates ? null : true, displayTags !== 'never');
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._setContentToken !== token) { return; }
|
|
||||||
|
|
||||||
this._updateAdderButtons2(states, modes);
|
|
||||||
} finally {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateAdderButtons2(states, modes) {
|
|
||||||
const {displayTags} = this._options.anki;
|
|
||||||
for (let i = 0, ii = states.length; i < ii; ++i) {
|
|
||||||
const infos = states[i];
|
|
||||||
let noteId = null;
|
|
||||||
for (let j = 0, jj = infos.length; j < jj; ++j) {
|
|
||||||
const {canAdd, noteIds, noteInfos} = infos[j];
|
|
||||||
const mode = modes[j];
|
|
||||||
const button = this._adderButtonFind(i, mode);
|
|
||||||
if (button === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(noteIds) && noteIds.length > 0) {
|
|
||||||
noteId = noteIds[0];
|
|
||||||
}
|
|
||||||
button.disabled = !canAdd;
|
|
||||||
button.hidden = false;
|
|
||||||
|
|
||||||
if (displayTags !== 'never' && Array.isArray(noteInfos)) {
|
|
||||||
this._setupTagsIndicator(i, noteInfos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (noteId !== null) {
|
|
||||||
this._viewerButtonShow(i, noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupTagsIndicator(i, noteInfos) {
|
|
||||||
const tagsIndicator = this._tagsIndicatorFind(i);
|
|
||||||
if (tagsIndicator === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {tags: optionTags, displayTags} = this._options.anki;
|
|
||||||
const noteTags = new Set();
|
|
||||||
for (const {tags} of noteInfos) {
|
|
||||||
for (const tag of tags) {
|
|
||||||
noteTags.add(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (displayTags === 'non-standard') {
|
|
||||||
for (const tag of optionTags) {
|
|
||||||
noteTags.delete(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noteTags.size > 0) {
|
|
||||||
tagsIndicator.disabled = false;
|
|
||||||
tagsIndicator.hidden = false;
|
|
||||||
tagsIndicator.title = `Card tags: ${[...noteTags].join(', ')}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onShowTags(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const tags = e.currentTarget.title;
|
|
||||||
this._showAnkiTagsNotification(tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
_showAnkiTagsNotification(message) {
|
|
||||||
if (this._ankiTagNotification === null) {
|
|
||||||
const node = this._displayGenerator.createEmptyFooterNotification();
|
|
||||||
node.classList.add('click-scannable');
|
|
||||||
this._ankiTagNotification = new DisplayNotification(this._footerNotificationContainer, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._ankiTagNotification.setContent(message);
|
|
||||||
this._ankiTagNotification.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
_entrySetCurrent(index) {
|
_entrySetCurrent(index) {
|
||||||
const entryPre = this._getEntry(this._index);
|
const entryPre = this._getEntry(this._index);
|
||||||
if (entryPre !== null) {
|
if (entryPre !== null) {
|
||||||
@ -1239,100 +1143,6 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_tryAddAnkiNoteForSelectedEntry(mode) {
|
|
||||||
this._addAnkiNote(this._index, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
_tryViewAnkiNoteForSelectedEntry() {
|
|
||||||
const button = this._viewerButtonFind(this._index);
|
|
||||||
if (button !== null && !button.disabled) {
|
|
||||||
yomichan.api.noteView(button.dataset.noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _addAnkiNote(dictionaryEntryIndex, mode) {
|
|
||||||
if (dictionaryEntryIndex < 0 || dictionaryEntryIndex >= this._dictionaryEntries.length) { return; }
|
|
||||||
const dictionaryEntry = this._dictionaryEntries[dictionaryEntryIndex];
|
|
||||||
|
|
||||||
const button = this._adderButtonFind(dictionaryEntryIndex, mode);
|
|
||||||
if (button === null || button.disabled) { return; }
|
|
||||||
|
|
||||||
this._hideAnkiNoteErrors(true);
|
|
||||||
|
|
||||||
const allErrors = [];
|
|
||||||
const overrideToken = this._progressIndicatorVisible.setOverride(true);
|
|
||||||
try {
|
|
||||||
const {anki: {suspendNewCards}} = this._options;
|
|
||||||
const noteContext = this._getNoteContext();
|
|
||||||
const {note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, true);
|
|
||||||
allErrors.push(...errors);
|
|
||||||
|
|
||||||
let noteId = null;
|
|
||||||
let addNoteOkay = false;
|
|
||||||
try {
|
|
||||||
noteId = await yomichan.api.addAnkiNote(note);
|
|
||||||
addNoteOkay = true;
|
|
||||||
} catch (e) {
|
|
||||||
allErrors.length = 0;
|
|
||||||
allErrors.push(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addNoteOkay) {
|
|
||||||
if (noteId === null) {
|
|
||||||
allErrors.push(new Error('Note could not be added'));
|
|
||||||
} else {
|
|
||||||
if (suspendNewCards) {
|
|
||||||
try {
|
|
||||||
await yomichan.api.suspendAnkiCardsForNote(noteId);
|
|
||||||
} catch (e) {
|
|
||||||
allErrors.push(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button.disabled = true;
|
|
||||||
this._viewerButtonShow(dictionaryEntryIndex, noteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
allErrors.push(e);
|
|
||||||
} finally {
|
|
||||||
this._progressIndicatorVisible.clearOverride(overrideToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allErrors.length > 0) {
|
|
||||||
this._showAnkiNoteErrors(allErrors);
|
|
||||||
} else {
|
|
||||||
this._hideAnkiNoteErrors(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_showAnkiNoteErrors(errors) {
|
|
||||||
if (this._ankiNoteNotificationEventListeners !== null) {
|
|
||||||
this._ankiNoteNotificationEventListeners.removeAllEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._ankiNoteNotification === null) {
|
|
||||||
const node = this._displayGenerator.createEmptyFooterNotification();
|
|
||||||
this._ankiNoteNotification = new DisplayNotification(this._footerNotificationContainer, node);
|
|
||||||
this._ankiNoteNotificationEventListeners = new EventListenerCollection();
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = this._displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
|
|
||||||
for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
|
|
||||||
this._ankiNoteNotificationEventListeners.addEventListener(node, 'click', () => {
|
|
||||||
console.log({ankiNoteErrors: errors});
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._ankiNoteNotification.setContent(content);
|
|
||||||
this._ankiNoteNotification.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
_hideAnkiNoteErrors(animate) {
|
|
||||||
if (this._ankiNoteNotification === null) { return; }
|
|
||||||
this._ankiNoteNotification.close(animate);
|
|
||||||
this._ankiNoteNotificationEventListeners.removeAllEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _playAudioCurrent() {
|
async _playAudioCurrent() {
|
||||||
await this._displayAudio.playAudio(this._index, 0);
|
await this._displayAudio.playAudio(this._index, 0);
|
||||||
}
|
}
|
||||||
@ -1342,74 +1152,12 @@ class Display extends EventDispatcher {
|
|||||||
return index >= 0 && index < entries.length ? entries[index] : null;
|
return index >= 0 && index < entries.length ? entries[index] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getValidSentenceData(sentence) {
|
|
||||||
let {text, offset} = (isObject(sentence) ? sentence : {});
|
|
||||||
if (typeof text !== 'string') { text = ''; }
|
|
||||||
if (typeof offset !== 'number') { offset = 0; }
|
|
||||||
return {text, offset};
|
|
||||||
}
|
|
||||||
|
|
||||||
_getClosestDictionaryEntryIndex(element) {
|
|
||||||
return this._getClosestIndex(element, '.entry');
|
|
||||||
}
|
|
||||||
|
|
||||||
_getClosestIndex(element, selector) {
|
|
||||||
const node = element.closest(selector);
|
|
||||||
if (node === null) { return -1; }
|
|
||||||
const index = parseInt(node.dataset.index, 10);
|
|
||||||
return Number.isFinite(index) ? index : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_adderButtonFind(index, mode) {
|
|
||||||
const entry = this._getEntry(index);
|
|
||||||
return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_tagsIndicatorFind(index) {
|
|
||||||
const entry = this._getEntry(index);
|
|
||||||
return entry !== null ? entry.querySelector('.action-view-tags') : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewerButtonFind(index) {
|
|
||||||
const entry = this._getEntry(index);
|
|
||||||
return entry !== null ? entry.querySelector('.action-view-note') : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_viewerButtonShow(index, noteId) {
|
|
||||||
const viewerButton = this._viewerButtonFind(index);
|
|
||||||
if (viewerButton === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
viewerButton.disabled = false;
|
|
||||||
viewerButton.hidden = false;
|
|
||||||
viewerButton.dataset.noteId = noteId;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getElementTop(element) {
|
_getElementTop(element) {
|
||||||
const elementRect = element.getBoundingClientRect();
|
const elementRect = element.getBoundingClientRect();
|
||||||
const documentRect = this._contentScrollBodyElement.getBoundingClientRect();
|
const documentRect = this._contentScrollBodyElement.getBoundingClientRect();
|
||||||
return elementRect.top - documentRect.top;
|
return elementRect.top - documentRect.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getNoteContext() {
|
|
||||||
const {state} = this._history;
|
|
||||||
let {documentTitle, url, sentence} = (isObject(state) ? state : {});
|
|
||||||
if (typeof documentTitle !== 'string') {
|
|
||||||
documentTitle = document.title;
|
|
||||||
}
|
|
||||||
if (typeof url !== 'string') {
|
|
||||||
url = window.location.href;
|
|
||||||
}
|
|
||||||
sentence = this._getValidSentenceData(sentence);
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
sentence,
|
|
||||||
documentTitle,
|
|
||||||
query: this._query,
|
|
||||||
fullQuery: this._fullQuery
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_historyHasState() {
|
_historyHasState() {
|
||||||
return isObject(this._history.state);
|
return isObject(this._history.state);
|
||||||
}
|
}
|
||||||
@ -1464,158 +1212,6 @@ class Display extends EventDispatcher {
|
|||||||
yomichan.trigger('closePopups');
|
yomichan.trigger('closePopups');
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getAnkiFieldTemplates(options) {
|
|
||||||
let templates = options.anki.fieldTemplates;
|
|
||||||
if (typeof templates === 'string') { return templates; }
|
|
||||||
|
|
||||||
templates = this._ankiFieldTemplatesDefault;
|
|
||||||
if (typeof templates === 'string') { return templates; }
|
|
||||||
|
|
||||||
templates = await yomichan.api.getDefaultAnkiFieldTemplates();
|
|
||||||
this._ankiFieldTemplatesDefault = templates;
|
|
||||||
return templates;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue, fetchAdditionalInfo) {
|
|
||||||
const modeCount = modes.length;
|
|
||||||
const notePromises = [];
|
|
||||||
for (const dictionaryEntry of dictionaryEntries) {
|
|
||||||
for (const mode of modes) {
|
|
||||||
const notePromise = this._createNote(dictionaryEntry, mode, context, false);
|
|
||||||
notePromises.push(notePromise);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const notes = (await Promise.all(notePromises)).map(({note}) => note);
|
|
||||||
|
|
||||||
let infos;
|
|
||||||
if (forceCanAddValue !== null) {
|
|
||||||
if (!await yomichan.api.isAnkiConnected()) {
|
|
||||||
throw new Error('Anki not connected');
|
|
||||||
}
|
|
||||||
infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue);
|
|
||||||
} else {
|
|
||||||
infos = await yomichan.api.getAnkiNoteInfo(notes, fetchAdditionalInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
for (let i = 0, ii = infos.length; i < ii; i += modeCount) {
|
|
||||||
results.push(infos.slice(i, i + modeCount));
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getAnkiNoteInfoForceValue(notes, canAdd) {
|
|
||||||
const results = [];
|
|
||||||
for (const note of notes) {
|
|
||||||
const valid = AnkiUtil.isNoteDataValid(note);
|
|
||||||
results.push({canAdd, valid, noteIds: null});
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _createNote(dictionaryEntry, mode, context, injectMedia) {
|
|
||||||
const options = this._options;
|
|
||||||
const template = this._ankiFieldTemplates;
|
|
||||||
const {
|
|
||||||
general: {resultOutputMode, glossaryLayoutMode, compactTags},
|
|
||||||
anki: ankiOptions
|
|
||||||
} = options;
|
|
||||||
const {tags, checkForDuplicates, duplicateScope} = ankiOptions;
|
|
||||||
const modeOptions = (mode === 'kanji') ? ankiOptions.kanji : ankiOptions.terms;
|
|
||||||
const {deck: deckName, model: modelName} = modeOptions;
|
|
||||||
const fields = Object.entries(modeOptions.fields);
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
let injectedMedia = null;
|
|
||||||
if (injectMedia) {
|
|
||||||
let errors2;
|
|
||||||
({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(dictionaryEntry, options, fields));
|
|
||||||
for (const error of errors2) {
|
|
||||||
errors.push(deserializeError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {note, errors: createNoteErrors} = await this._ankiNoteBuilder.createNote({
|
|
||||||
dictionaryEntry,
|
|
||||||
mode,
|
|
||||||
context,
|
|
||||||
template,
|
|
||||||
deckName,
|
|
||||||
modelName,
|
|
||||||
fields,
|
|
||||||
tags,
|
|
||||||
checkForDuplicates,
|
|
||||||
duplicateScope,
|
|
||||||
resultOutputMode,
|
|
||||||
glossaryLayoutMode,
|
|
||||||
compactTags,
|
|
||||||
injectedMedia,
|
|
||||||
errors
|
|
||||||
});
|
|
||||||
errors.push(...createNoteErrors);
|
|
||||||
return {note, errors};
|
|
||||||
}
|
|
||||||
|
|
||||||
async _injectAnkiNoteMedia(dictionaryEntry, options, fields) {
|
|
||||||
const {anki: {screenshot: {format, quality}}} = options;
|
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
|
|
||||||
const dictionaryEntryDetails = this._getDictionaryEntryDetailsForNote(dictionaryEntry);
|
|
||||||
|
|
||||||
const audioDetails = (
|
|
||||||
dictionaryEntryDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio') ?
|
|
||||||
this._displayAudio.getAnkiNoteMediaAudioDetails(dictionaryEntryDetails.term, dictionaryEntryDetails.reading) :
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const screenshotDetails = (
|
|
||||||
AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') && typeof this._contentOriginTabId === 'number' ?
|
|
||||||
{tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} :
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const clipboardDetails = {
|
|
||||||
image: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-image'),
|
|
||||||
text: AnkiUtil.fieldsObjectContainsMarker(fields, 'clipboard-text')
|
|
||||||
};
|
|
||||||
|
|
||||||
return await yomichan.api.injectAnkiNoteMedia(
|
|
||||||
timestamp,
|
|
||||||
dictionaryEntryDetails,
|
|
||||||
audioDetails,
|
|
||||||
screenshotDetails,
|
|
||||||
clipboardDetails
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getDictionaryEntryDetailsForNote(dictionaryEntry) {
|
|
||||||
const {type} = dictionaryEntry;
|
|
||||||
if (type === 'kanji') {
|
|
||||||
const {character} = dictionaryEntry;
|
|
||||||
return {type, character};
|
|
||||||
}
|
|
||||||
|
|
||||||
const {headwords} = dictionaryEntry;
|
|
||||||
let bestIndex = -1;
|
|
||||||
for (let i = 0, ii = headwords.length; i < ii; ++i) {
|
|
||||||
const {term, reading, sources} = headwords[i];
|
|
||||||
for (const {deinflectedText} of sources) {
|
|
||||||
if (term === deinflectedText) {
|
|
||||||
bestIndex = i;
|
|
||||||
i = ii;
|
|
||||||
break;
|
|
||||||
} else if (reading === deinflectedText && bestIndex < 0) {
|
|
||||||
bestIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {term, reading} = headwords[Math.max(0, bestIndex)];
|
|
||||||
return {type, term, reading};
|
|
||||||
}
|
|
||||||
|
|
||||||
async _setOptionsContextIfDifferent(optionsContext) {
|
async _setOptionsContextIfDifferent(optionsContext) {
|
||||||
if (deepEqual(this._optionsContext, optionsContext)) { return; }
|
if (deepEqual(this._optionsContext, optionsContext)) { return; }
|
||||||
await this.setOptionsContext(optionsContext);
|
await this.setOptionsContext(optionsContext);
|
||||||
@ -1755,9 +1351,6 @@ class Display extends EventDispatcher {
|
|||||||
|
|
||||||
_addEntryEventListeners(entry) {
|
_addEntryEventListeners(entry) {
|
||||||
this._eventListeners.addEventListener(entry, 'click', this._onEntryClick.bind(this));
|
this._eventListeners.addEventListener(entry, 'click', this._onEntryClick.bind(this));
|
||||||
this._addMultipleEventListeners(entry, '.action-view-tags', 'click', this._onShowTags.bind(this));
|
|
||||||
this._addMultipleEventListeners(entry, '.action-add-note', 'click', this._onNoteAdd.bind(this));
|
|
||||||
this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteView.bind(this));
|
|
||||||
this._addMultipleEventListeners(entry, '.headword-kanji-link', 'click', this._onKanjiLookup.bind(this));
|
this._addMultipleEventListeners(entry, '.headword-kanji-link', 'click', this._onKanjiLookup.bind(this));
|
||||||
this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this));
|
this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this));
|
||||||
this._addMultipleEventListeners(entry, '.tag-label', 'click', this._onTagClick.bind(this));
|
this._addMultipleEventListeners(entry, '.tag-label', 'click', this._onTagClick.bind(this));
|
||||||
@ -1924,58 +1517,13 @@ class Display extends EventDispatcher {
|
|||||||
return typeof queryPostProcessor === 'function' ? queryPostProcessor(query) : query;
|
return typeof queryPostProcessor === 'function' ? queryPostProcessor(query) : query;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getModes(isTerms) {
|
|
||||||
return isTerms ? ['term-kanji', 'term-kana'] : ['kanji'];
|
|
||||||
}
|
|
||||||
|
|
||||||
async _logDictionaryEntryData(index) {
|
async _logDictionaryEntryData(index) {
|
||||||
if (index < 0 || index >= this._dictionaryEntries.length) { return; }
|
if (index < 0 || index >= this._dictionaryEntries.length) { return; }
|
||||||
const dictionaryEntry = this._dictionaryEntries[index];
|
const dictionaryEntry = this._dictionaryEntries[index];
|
||||||
const result = {dictionaryEntry};
|
const result = {dictionaryEntry};
|
||||||
|
|
||||||
// Anki note data
|
const result2 = await this._displayAnki.getLogData(dictionaryEntry);
|
||||||
let ankiNoteData;
|
Object.assign(result, result2);
|
||||||
let ankiNoteDataException;
|
|
||||||
try {
|
|
||||||
const context = this._getNoteContext();
|
|
||||||
const {general: {resultOutputMode, glossaryLayoutMode, compactTags}} = this._options;
|
|
||||||
ankiNoteData = await this._ankiNoteBuilder.getRenderingData({
|
|
||||||
dictionaryEntry,
|
|
||||||
mode: 'test',
|
|
||||||
context,
|
|
||||||
resultOutputMode,
|
|
||||||
glossaryLayoutMode,
|
|
||||||
compactTags,
|
|
||||||
injectedMedia: null,
|
|
||||||
marker: 'test'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
ankiNoteDataException = e;
|
|
||||||
}
|
|
||||||
result.ankiNoteData = ankiNoteData;
|
|
||||||
if (typeof ankiNoteDataException !== 'undefined') {
|
|
||||||
result.ankiNoteDataException = ankiNoteDataException;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anki notes
|
|
||||||
const ankiNotes = [];
|
|
||||||
const modes = this._getModes(dictionaryEntry.type === 'term');
|
|
||||||
for (const mode of modes) {
|
|
||||||
let note;
|
|
||||||
let errors;
|
|
||||||
try {
|
|
||||||
const noteContext = this._getNoteContext();
|
|
||||||
({note: note, errors} = await this._createNote(dictionaryEntry, mode, noteContext, false));
|
|
||||||
} catch (e) {
|
|
||||||
errors = [e];
|
|
||||||
}
|
|
||||||
const entry = {mode, note};
|
|
||||||
if (Array.isArray(errors) && errors.length > 0) {
|
|
||||||
entry.errors = errors;
|
|
||||||
}
|
|
||||||
ankiNotes.push(entry);
|
|
||||||
}
|
|
||||||
result.ankiNotes = ankiNotes;
|
|
||||||
|
|
||||||
console.log(result);
|
console.log(result);
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,7 @@
|
|||||||
<script src="/js/data/anki-note-builder.js"></script>
|
<script src="/js/data/anki-note-builder.js"></script>
|
||||||
<script src="/js/data/anki-util.js"></script>
|
<script src="/js/data/anki-util.js"></script>
|
||||||
<script src="/js/display/display.js"></script>
|
<script src="/js/display/display.js"></script>
|
||||||
|
<script src="/js/display/display-anki.js"></script>
|
||||||
<script src="/js/display/display-audio.js"></script>
|
<script src="/js/display/display-audio.js"></script>
|
||||||
<script src="/js/display/display-generator.js"></script>
|
<script src="/js/display/display-generator.js"></script>
|
||||||
<script src="/js/display/display-history.js"></script>
|
<script src="/js/display/display-history.js"></script>
|
||||||
|
@ -86,6 +86,7 @@
|
|||||||
<script src="/js/data/anki-note-builder.js"></script>
|
<script src="/js/data/anki-note-builder.js"></script>
|
||||||
<script src="/js/data/anki-util.js"></script>
|
<script src="/js/data/anki-util.js"></script>
|
||||||
<script src="/js/display/display.js"></script>
|
<script src="/js/display/display.js"></script>
|
||||||
|
<script src="/js/display/display-anki.js"></script>
|
||||||
<script src="/js/display/display-audio.js"></script>
|
<script src="/js/display/display-audio.js"></script>
|
||||||
<script src="/js/display/display-generator.js"></script>
|
<script src="/js/display/display-generator.js"></script>
|
||||||
<script src="/js/display/display-history.js"></script>
|
<script src="/js/display/display-history.js"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user