aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2022-05-29 21:24:41 -0400
committerGitHub <noreply@github.com>2022-05-29 21:24:41 -0400
commit331a2e62941e04a4d50a21faefed663a92ddc00a (patch)
tree1212a1e7cd57ea2331fab2101afdc325cb3a4766
parentf3024c50186344aa6a6b09500ea02540463ce5c9 (diff)
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
-rw-r--r--ext/css/settings.css22
-rw-r--r--ext/data/schemas/options-schema.json8
-rw-r--r--ext/display-templates.html4
-rw-r--r--ext/js/background/backend.js18
-rw-r--r--ext/js/comm/anki.js24
-rw-r--r--ext/js/comm/api.js4
-rw-r--r--ext/js/data/options-util.js12
-rw-r--r--ext/js/display/display-anki.js24
-rw-r--r--ext/js/display/display-generator.js38
-rw-r--r--ext/js/pages/settings/anki-controller.js58
-rw-r--r--ext/settings.html33
-rw-r--r--test/test-options-util.js5
12 files changed, 222 insertions, 28 deletions
diff --git a/ext/css/settings.css b/ext/css/settings.css
index 19f587e9..62e979e0 100644
--- a/ext/css/settings.css
+++ b/ext/css/settings.css
@@ -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 {
diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json
index ef86f8c3..46d8a32a 100644
--- a/ext/data/schemas/options-schema.json
+++ b/ext/data/schemas/options-schema.json
@@ -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"
}
}
},
diff --git a/ext/display-templates.html b/ext/display-templates.html
index 8b2e4450..8d147253 100644
--- a/ext/display-templates.html
+++ b/ext/display-templates.html
@@ -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>
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index cdbfde1e..07d6fd98 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -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}) {
diff --git a/ext/js/comm/anki.js b/ext/js/comm/anki.js
index 7ffb747b..f5dc62f2 100644
--- a/ext/js/comm/anki.js
+++ b/ext/js/comm/anki.js
@@ -106,6 +106,15 @@ class AnkiConnect {
}
/**
+ * 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.
* @param {string} content The base64-encoded content 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() {
diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js
index 75a01dd5..2ffe2d8c 100644
--- a/ext/js/comm/api.js
+++ b/ext/js/comm/api.js
@@ -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) {
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
index 593fed29..f87bfa4b 100644
--- a/ext/js/data/options-util.js
+++ b/ext/js/data/options-util.js
@@ -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;
+ }
}
diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js
index 0af8831a..12133ad0 100644
--- a/ext/js/display/display-anki.js
+++ b/ext/js/display/display-anki.js
@@ -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) {
diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js
index 95080e27..851808f2 100644
--- a/ext/js/display/display-generator.js
+++ b/ext/js/display/display-generator.js
@@ -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) {
diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js
index daf09143..d03fa535 100644
--- a/ext/js/pages/settings/anki-controller.js
+++ b/ext/js/pages/settings/anki-controller.js
@@ -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 {
diff --git a/ext/settings.html b/ext/settings.html
index ac296b8d..1ad0c37d 100644
--- a/ext/settings.html
+++ b/ext/settings.html
@@ -1767,6 +1767,39 @@
<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">
<div class="settings-item-label">
Show card tags
<a tabindex="0" class="more-toggle more-only" data-parent-distance="4">(?)</a>
diff --git a/test/test-options-util.js b/test/test-options-util.js
index e706b720..425201ce 100644
--- a/test/test-options-util.js
+++ b/test/test-options-util.js
@@ -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