Add support for guiEditNote to view notes (#2143)
* Add AnkiConnect.guiEditNote * Update _onApiNoteView to first try guiEditNote * Add setting * Update noteView API * Use setting * Return which mode was used * Update DisplayGenerator * Handle errors in DisplayAnki * Update docs * Add isErrorUnsupportedAction function * Add an allowFallback option to noteView * Disambiguate * Simplify now that preferredMode isn't used * Update settings info * Implement test buttons * Update styles * Update status visibility * Wrap layout * Update description * Update date
This commit is contained in:
parent
f3024c5018
commit
331a2e6294
@ -2147,6 +2147,28 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.test-anki-note-viewer-container {
|
||||
margin-top: 0.85em;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.test-anki-note-viewer-container>:nth-child(n+2) {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.test-anki-note-viewer-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.test-anki-note-viewer-results {
|
||||
align-self: center;
|
||||
}
|
||||
.test-anki-note-viewer-results[data-success=true] {
|
||||
color: var(--success-color);
|
||||
}
|
||||
.test-anki-note-viewer-results[data-success=false] {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
|
||||
/* Dictionary settings */
|
||||
.dictionary-list {
|
||||
|
@ -844,7 +844,8 @@
|
||||
"checkForDuplicates",
|
||||
"fieldTemplates",
|
||||
"suspendNewCards",
|
||||
"displayTags"
|
||||
"displayTags",
|
||||
"noteGuiMode"
|
||||
],
|
||||
"properties": {
|
||||
"enable": {
|
||||
@ -959,6 +960,11 @@
|
||||
"type": "string",
|
||||
"enum": ["never", "always", "non-standard"],
|
||||
"default": "never"
|
||||
},
|
||||
"noteGuiMode": {
|
||||
"type": "string",
|
||||
"enum": ["browse", "edit"],
|
||||
"default": "browse"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -179,6 +179,10 @@
|
||||
<ul class="anki-note-error-list"></ul>
|
||||
<div class="anki-note-error-log-container"><a tabindex="0" class="anki-note-error-log-link">Log debug info to console</a></div>
|
||||
</div></template>
|
||||
<template id="footer-notification-anki-view-note-error-template" data-remove-whitespace-text="true">
|
||||
Note viewer window could not be opened.<br>
|
||||
Check the <a href="/settings.html#!anki" target="_blank" rel="noopener"><em>Anki</em> › <em>Note viewer window</em></a> setting.
|
||||
</template>
|
||||
<template id="profile-list-item-template"><label class="profile-list-item">
|
||||
<div class="profile-list-item-selection"><label class="radio"><input type="radio" class="profile-entry-is-default-radio" name="profile-entry-default-radio"><span class="radio-body"><span class="radio-border"></span><span class="radio-dot"></span></span></label></div>
|
||||
<div class="profile-list-item-name"></div>
|
||||
|
@ -512,8 +512,22 @@ class Backend {
|
||||
);
|
||||
}
|
||||
|
||||
async _onApiNoteView({noteId}) {
|
||||
return await this._anki.guiBrowseNote(noteId);
|
||||
async _onApiNoteView({noteId, mode, allowFallback}) {
|
||||
if (mode === 'edit') {
|
||||
try {
|
||||
await this._anki.guiEditNote(noteId);
|
||||
return 'edit';
|
||||
} catch (e) {
|
||||
if (!this._anki.isErrorUnsupportedAction(e)) {
|
||||
throw e;
|
||||
} else if (!allowFallback) {
|
||||
throw new Error('Mode not supported');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
await this._anki.guiBrowseNote(noteId);
|
||||
return 'browse';
|
||||
}
|
||||
|
||||
async _onApiSuspendAnkiCardsForNote({noteId}) {
|
||||
|
@ -105,6 +105,15 @@ class AnkiConnect {
|
||||
return await this.guiBrowse(`nid:${noteId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the note editor GUI.
|
||||
* @param {number} noteId The ID of the note.
|
||||
* @returns {Promise<null>} Nothing is returned.
|
||||
*/
|
||||
async guiEditNote(noteId) {
|
||||
return await this._invoke('guiEditNote', {note: noteId});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a file with the specified base64-encoded content inside Anki's media folder.
|
||||
* @param {string} fileName The name of the file.
|
||||
@ -187,6 +196,21 @@ class AnkiConnect {
|
||||
return actions.includes(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific error object corresponds to an unsupported action.
|
||||
* @param {Error} error An error object generated by an API call.
|
||||
* @returns {boolean} Whether or not the error indicates the action is not supported.
|
||||
*/
|
||||
isErrorUnsupportedAction(error) {
|
||||
if (error instanceof Error) {
|
||||
const {data} = error;
|
||||
if (isObject(data) && data.apiError === 'unsupported action') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
async _checkVersion() {
|
||||
|
@ -60,8 +60,8 @@ class API {
|
||||
return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails});
|
||||
}
|
||||
|
||||
noteView(noteId) {
|
||||
return this._invoke('noteView', {noteId});
|
||||
noteView(noteId, mode, allowFallback) {
|
||||
return this._invoke('noteView', {noteId, mode, allowFallback});
|
||||
}
|
||||
|
||||
suspendAnkiCardsForNote(noteId) {
|
||||
|
@ -467,7 +467,8 @@ class OptionsUtil {
|
||||
{async: false, update: this._updateVersion15.bind(this)},
|
||||
{async: false, update: this._updateVersion16.bind(this)},
|
||||
{async: false, update: this._updateVersion17.bind(this)},
|
||||
{async: false, update: this._updateVersion18.bind(this)}
|
||||
{async: false, update: this._updateVersion18.bind(this)},
|
||||
{async: false, update: this._updateVersion19.bind(this)}
|
||||
];
|
||||
if (typeof targetVersion === 'number' && targetVersion < result.length) {
|
||||
result.splice(targetVersion);
|
||||
@ -947,4 +948,13 @@ class OptionsUtil {
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
_updateVersion19(options) {
|
||||
// Version 19 changes:
|
||||
// Added anki.noteGuiMode.
|
||||
for (const profile of options.profiles) {
|
||||
profile.options.anki.noteGuiMode = 'browse';
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ class DisplayAnki {
|
||||
this._screenshotFormat = 'png';
|
||||
this._screenshotQuality = 100;
|
||||
this._scanLength = 10;
|
||||
this._noteGuiMode = 'browse';
|
||||
this._noteTags = [];
|
||||
this._modeOptions = new Map();
|
||||
this._dictionaryEntryTypeModeMap = new Map([
|
||||
@ -132,7 +133,7 @@ class DisplayAnki {
|
||||
_onOptionsUpdated({options}) {
|
||||
const {
|
||||
general: {resultOutputMode, glossaryLayoutMode, compactTags},
|
||||
anki: {tags, duplicateScope, duplicateScopeCheckAllModels, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, screenshot: {format, quality}},
|
||||
anki: {tags, duplicateScope, duplicateScopeCheckAllModels, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, noteGuiMode, screenshot: {format, quality}},
|
||||
scanning: {length: scanLength}
|
||||
} = options;
|
||||
|
||||
@ -147,6 +148,7 @@ class DisplayAnki {
|
||||
this._screenshotFormat = format;
|
||||
this._screenshotQuality = quality;
|
||||
this._scanLength = scanLength;
|
||||
this._noteGuiMode = noteGuiMode;
|
||||
this._noteTags = [...tags];
|
||||
this._modeOptions.clear();
|
||||
this._modeOptions.set('kanji', kanji);
|
||||
@ -418,7 +420,9 @@ class DisplayAnki {
|
||||
return error;
|
||||
}
|
||||
|
||||
_showErrorNotification(errors) {
|
||||
_showErrorNotification(errors, displayErrors) {
|
||||
if (typeof displayErrors === 'undefined') { displayErrors = errors; }
|
||||
|
||||
if (this._errorNotificationEventListeners !== null) {
|
||||
this._errorNotificationEventListeners.removeAllEventListeners();
|
||||
}
|
||||
@ -428,7 +432,7 @@ class DisplayAnki {
|
||||
this._errorNotificationEventListeners = new EventListenerCollection();
|
||||
}
|
||||
|
||||
const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
|
||||
const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(displayErrors);
|
||||
for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
|
||||
this._errorNotificationEventListeners.addEventListener(node, 'click', () => {
|
||||
console.log({ankiNoteErrors: errors});
|
||||
@ -634,10 +638,20 @@ class DisplayAnki {
|
||||
}
|
||||
}
|
||||
|
||||
_viewNote(node) {
|
||||
async _viewNote(node) {
|
||||
const noteIds = this._getNodeNoteIds(node);
|
||||
if (noteIds.length === 0) { return; }
|
||||
yomichan.api.noteView(noteIds[0]);
|
||||
try {
|
||||
await yomichan.api.noteView(noteIds[0], this._noteGuiMode, false);
|
||||
} catch (e) {
|
||||
const displayErrors = (
|
||||
e.message === 'Mode not supported' ?
|
||||
[this._display.displayGenerator.instantiateTemplateFragment('footer-notification-anki-view-note-error')] :
|
||||
void 0
|
||||
);
|
||||
this._showErrorNotification([e], displayErrors);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_showViewNoteMenu(node) {
|
||||
|
@ -220,6 +220,9 @@ class DisplayGenerator {
|
||||
for (const error of errors) {
|
||||
const div = document.createElement('li');
|
||||
div.className = 'anki-note-error-message';
|
||||
if (error instanceof DocumentFragment || error instanceof Node) {
|
||||
div.appendChild(error);
|
||||
} else {
|
||||
let message = isObject(error) && typeof error.message === 'string' ? error.message : `${error}`;
|
||||
let link = null;
|
||||
if (isObject(error) && isObject(error.data)) {
|
||||
@ -237,6 +240,7 @@ class DisplayGenerator {
|
||||
}
|
||||
this._setTextContent(div, message);
|
||||
if (link !== null) { div.appendChild(link); }
|
||||
}
|
||||
list.appendChild(div);
|
||||
}
|
||||
|
||||
@ -251,6 +255,10 @@ class DisplayGenerator {
|
||||
return this._templates.instantiate(name);
|
||||
}
|
||||
|
||||
instantiateTemplateFragment(name) {
|
||||
return this._templates.instantiateFragment(name);
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_createTermHeadword(headword, headwordIndex, pronunciations) {
|
||||
|
@ -71,6 +71,12 @@ class AnkiController {
|
||||
input.addEventListener('change', this._onAnkiCardPrimaryTypeRadioChange.bind(this), false);
|
||||
}
|
||||
|
||||
const testAnkiNoteViewerButtons = document.querySelectorAll('.test-anki-note-viewer-button');
|
||||
const onTestAnkiNoteViewerButtonClick = this._onTestAnkiNoteViewerButtonClick.bind(this);
|
||||
for (const button of testAnkiNoteViewerButtons) {
|
||||
button.addEventListener('click', onTestAnkiNoteViewerButtonClick, false);
|
||||
}
|
||||
|
||||
document.querySelector('#anki-error-log').addEventListener('click', this._onAnkiErrorLogLinkClick.bind(this));
|
||||
|
||||
const options = await this._settingsController.getOptions();
|
||||
@ -192,6 +198,10 @@ class AnkiController {
|
||||
console.log({error: this._ankiError});
|
||||
}
|
||||
|
||||
_onTestAnkiNoteViewerButtonClick(e) {
|
||||
this._testAnkiNoteViewerSafe(e.currentTarget.dataset.mode);
|
||||
}
|
||||
|
||||
_setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) {
|
||||
if (this._ankiCardPrimary === null) { return; }
|
||||
this._ankiCardPrimary.dataset.ankiCardType = ankiCardType;
|
||||
@ -336,6 +346,54 @@ class AnkiController {
|
||||
const stringComparer = this._stringComparer;
|
||||
array.sort((a, b) => stringComparer.compare(a, b));
|
||||
}
|
||||
|
||||
async _testAnkiNoteViewerSafe(mode) {
|
||||
this._setAnkiNoteViewerStatus(false, null);
|
||||
try {
|
||||
await this._testAnkiNoteViewer(mode);
|
||||
} catch (e) {
|
||||
this._setAnkiNoteViewerStatus(true, e);
|
||||
return;
|
||||
}
|
||||
this._setAnkiNoteViewerStatus(true, null);
|
||||
}
|
||||
|
||||
async _testAnkiNoteViewer(mode) {
|
||||
const queries = [
|
||||
'"よむ" deck:current',
|
||||
'"よむ"',
|
||||
'deck:current',
|
||||
''
|
||||
];
|
||||
|
||||
let noteId = null;
|
||||
for (const query of queries) {
|
||||
const notes = await yomichan.api.findAnkiNotes(query);
|
||||
if (notes.length > 0) {
|
||||
noteId = notes[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (noteId === null) {
|
||||
throw new Error('Could not find a note to test with');
|
||||
}
|
||||
|
||||
await yomichan.api.noteView(noteId, mode, false);
|
||||
}
|
||||
|
||||
_setAnkiNoteViewerStatus(visible, error) {
|
||||
const node = document.querySelector('#test-anki-note-viewer-results');
|
||||
if (visible) {
|
||||
const success = (error === null);
|
||||
node.textContent = success ? 'Success!' : error.message;
|
||||
node.dataset.success = `${success}`;
|
||||
} else {
|
||||
node.textContent = '';
|
||||
delete node.dataset.success;
|
||||
}
|
||||
node.hidden = !visible;
|
||||
}
|
||||
}
|
||||
|
||||
class AnkiCardController {
|
||||
|
@ -1764,6 +1764,39 @@
|
||||
<label class="toggle"><input type="checkbox" data-setting="anki.suspendNewCards"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label>
|
||||
</div>
|
||||
</div></div>
|
||||
<div class="settings-item advanced-only">
|
||||
<div class="settings-item-inner">
|
||||
<div class="settings-item-left">
|
||||
<div class="settings-item-label">Note viewer window</div>
|
||||
<div class="settings-item-description">
|
||||
Clicking the <em>View added note</em> button shows this window.
|
||||
<a tabindex="0" class="more-toggle more-only" data-parent-distance="4">More…</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item-right">
|
||||
<select data-setting="anki.noteGuiMode">
|
||||
<option value="browse">Card browser</option>
|
||||
<option value="edit">Note editor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item-children more" hidden>
|
||||
<p>
|
||||
AnkiConnect releases after around 2022-05-29 support a new note editor window
|
||||
which can be shown when clicking the <em>View added note</em> button.
|
||||
This can be tested using the buttons below.
|
||||
If an error occurs, Anki and/or AnkiConnect may need to be updated.
|
||||
</p>
|
||||
<div class="test-anki-note-viewer-container">
|
||||
<button class="test-anki-note-viewer-button" data-mode="browse">Test <em>Card browser</em></button>
|
||||
<button class="test-anki-note-viewer-button" data-mode="edit">Test <em>Note editor</em></button>
|
||||
<div class="test-anki-note-viewer-results" id="test-anki-note-viewer-results" hidden></div>
|
||||
</div>
|
||||
<p class="margin-above">
|
||||
<a tabindex="0" class="more-toggle" data-parent-distance="3">Less…</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-item advanced-only">
|
||||
<div class="settings-item-inner">
|
||||
<div class="settings-item-left">
|
||||
|
@ -453,7 +453,8 @@ function createProfileOptionsUpdatedTestData1() {
|
||||
displayTags: 'never',
|
||||
checkForDuplicates: true,
|
||||
fieldTemplates: null,
|
||||
suspendNewCards: false
|
||||
suspendNewCards: false,
|
||||
noteGuiMode: 'browse'
|
||||
},
|
||||
sentenceParsing: {
|
||||
scanExtent: 200,
|
||||
@ -602,7 +603,7 @@ function createOptionsUpdatedTestData1() {
|
||||
}
|
||||
],
|
||||
profileCurrent: 0,
|
||||
version: 18,
|
||||
version: 19,
|
||||
global: {
|
||||
database: {
|
||||
prefixWildcardsSupported: false
|
||||
|
Loading…
Reference in New Issue
Block a user