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:
toasted-nutbread 2022-05-29 21:24:41 -04:00 committed by GitHub
parent f3024c5018
commit 331a2e6294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 222 additions and 28 deletions

View File

@ -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 {

View File

@ -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"
}
}
},

View File

@ -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> &rsaquo; <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>

View File

@ -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}) {

View File

@ -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() {

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -220,23 +220,27 @@ class DisplayGenerator {
for (const error of errors) {
const div = document.createElement('li');
div.className = 'anki-note-error-message';
let message = isObject(error) && typeof error.message === 'string' ? error.message : `${error}`;
let link = null;
if (isObject(error) && isObject(error.data)) {
const {referenceUrl} = error.data;
if (typeof referenceUrl === 'string') {
message = message.trimEnd();
if (!/[.!?]^/.test()) { message += '.'; }
message += ' ';
link = document.createElement('a');
link.href = referenceUrl;
link.target = '_blank';
link.rel = 'noreferrer noopener';
link.textContent = 'More info';
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)) {
const {referenceUrl} = error.data;
if (typeof referenceUrl === 'string') {
message = message.trimEnd();
if (!/[.!?]^/.test()) { message += '.'; }
message += ' ';
link = document.createElement('a');
link.href = referenceUrl;
link.target = '_blank';
link.rel = 'noreferrer noopener';
link.textContent = 'More info';
}
}
this._setTextContent(div, message);
if (link !== null) { div.appendChild(link); }
}
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) {

View File

@ -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 {

View File

@ -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&hellip;</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&hellip;</a>
</p>
</div>
</div>
<div class="settings-item advanced-only">
<div class="settings-item-inner">
<div class="settings-item-left">

View File

@ -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