diff options
-rw-r--r-- | ext/css/settings.css | 22 | ||||
-rw-r--r-- | ext/data/schemas/options-schema.json | 8 | ||||
-rw-r--r-- | ext/display-templates.html | 4 | ||||
-rw-r--r-- | ext/js/background/backend.js | 18 | ||||
-rw-r--r-- | ext/js/comm/anki.js | 24 | ||||
-rw-r--r-- | ext/js/comm/api.js | 4 | ||||
-rw-r--r-- | ext/js/data/options-util.js | 12 | ||||
-rw-r--r-- | ext/js/display/display-anki.js | 24 | ||||
-rw-r--r-- | ext/js/display/display-generator.js | 38 | ||||
-rw-r--r-- | ext/js/pages/settings/anki-controller.js | 58 | ||||
-rw-r--r-- | ext/settings.html | 33 | ||||
-rw-r--r-- | test/test-options-util.js | 5 |
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> › <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…</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"> <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 |