diff --git a/ext/css/display.css b/ext/css/display.css index c7827710..6fffc5ab 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -644,6 +644,9 @@ button.action-button[hidden] { -webkit-filter var(--animation-duration) linear, background-color var(--animation-duration) linear; } +button.collapsible-action-button[hidden] { + display: none; +} button.action-button:disabled { pointer-events: none; cursor: default; @@ -667,7 +670,7 @@ button.action-button:active { background-color: var(--action-button-active-color); box-shadow: none; } -button.action-button::before { +button.action-button[data-icon]::before { content: ''; width: var(--action-button-size); height: var(--action-button-size); @@ -691,6 +694,12 @@ button.action-button[data-icon=play-audio]::before { button.action-button[data-icon=source-term]::before { background-image: url('/images/source-term.svg'); } +.action-view-tags>.icon { + display: block; + width: var(--action-button-size); + height: var(--action-button-size); + background-color: var(--text-color); +} :root[data-result-output-mode=merge] .entry[data-type=term] .actions>button.action-button.action-play-audio { display: none; } diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json index 5ec54b86..69e042ea 100644 --- a/ext/data/schemas/options-schema.json +++ b/ext/data/schemas/options-schema.json @@ -806,7 +806,8 @@ "duplicateScope", "checkForDuplicates", "fieldTemplates", - "suspendNewCards" + "suspendNewCards", + "displayTags" ], "properties": { "enable": { @@ -912,6 +913,11 @@ "suspendNewCards": { "type": "boolean", "default": false + }, + "displayTags": { + "type": "string", + "enum": ["never", "always", "non-standard"], + "default": "never" } } }, diff --git a/ext/display-templates.html b/ext/display-templates.html index 6496b73e..6637e70e 100644 --- a/ext/display-templates.html +++ b/ext/display-templates.html @@ -5,6 +5,7 @@
+ diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 5b133d79..e94ad065 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -458,7 +458,7 @@ class Backend { return await this._anki.addNote(note); } - async _onApiGetAnkiNoteInfo({notes}) { + async _onApiGetAnkiNoteInfo({notes, fetchAdditionalInfo}) { const results = []; const cannotAdd = []; const canAddArray = await this._anki.canAddNotes(notes); @@ -482,6 +482,9 @@ class Backend { const noteIds = noteIdsArray[i]; if (noteIds.length > 0) { cannotAdd[i].info.noteIds = noteIds; + if (fetchAdditionalInfo) { + cannotAdd[i].info.noteInfos = await this._anki.notesInfo(noteIds); + } } } } diff --git a/ext/js/comm/anki.js b/ext/js/comm/anki.js index da234eff..e8cf7afd 100644 --- a/ext/js/comm/anki.js +++ b/ext/js/comm/anki.js @@ -71,6 +71,12 @@ class AnkiConnect { return await this._invoke('canAddNotes', {notes}); } + async notesInfo(notes) { + if (!this._enabled) { return []; } + await this._checkVersion(); + return await this._invoke('notesInfo', {notes}); + } + async getDeckNames() { if (!this._enabled) { return []; } await this._checkVersion(); diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 137cda41..3795dcf4 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -52,8 +52,8 @@ class API { return this._invoke('addAnkiNote', {note}); } - getAnkiNoteInfo(notes) { - return this._invoke('getAnkiNoteInfo', {notes}); + getAnkiNoteInfo(notes, fetchAdditionalInfo) { + return this._invoke('getAnkiNoteInfo', {notes, fetchAdditionalInfo}); } injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) { diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 42f9a38f..cb7946f7 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -792,6 +792,7 @@ class OptionsUtil { // Version 11 changes: // Changed dictionaries to an array. // Changed audio.customSourceUrl's {expression} marker to {term}. + // Added anki.displayTags. const customSourceUrlPattern = /\{expression\}/g; for (const profile of options.profiles) { const dictionariesNew = []; @@ -805,6 +806,8 @@ class OptionsUtil { customSourceUrl = customSourceUrl.replace(customSourceUrlPattern, '{term}'); } profile.options.audio.customSourceUrl = customSourceUrl; + + profile.options.anki.displayTags = 'never'; } return options; } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index fcfa0244..720e1de5 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -113,6 +113,7 @@ class Display extends EventDispatcher { this._displayAudio = new DisplayAudio(this); this._ankiNoteNotification = null; this._ankiNoteNotificationEventListeners = null; + this._ankiTagNotification = null; this._queryPostProcessor = null; this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this); this._elementOverflowController = new ElementOverflowController(); @@ -1081,8 +1082,8 @@ class Display extends EventDispatcher { let states; try { const noteContext = this._getNoteContext(); - const {checkForDuplicates} = this._options.anki; - states = await this._areDictionaryEntriesAddable(dictionaryEntries, modes, noteContext, checkForDuplicates ? null : true); + const {checkForDuplicates, displayTags} = this._options.anki; + states = await this._areDictionaryEntriesAddable(dictionaryEntries, modes, noteContext, checkForDuplicates ? null : true, displayTags !== 'never'); } catch (e) { return; } @@ -1096,11 +1097,12 @@ class Display extends EventDispatcher { } _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} = infos[j]; + const {canAdd, noteIds, noteInfos} = infos[j]; const mode = modes[j]; const button = this._adderButtonFind(i, mode); if (button === null) { @@ -1112,6 +1114,10 @@ class Display extends EventDispatcher { } button.disabled = !canAdd; button.hidden = false; + + if (displayTags !== 'never' && Array.isArray(noteInfos)) { + this._setupTagsIndicator(i, noteInfos); + } } if (noteId !== null) { this._viewerButtonShow(i, noteId); @@ -1119,6 +1125,49 @@ class Display extends EventDispatcher { } } + _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) { const entryPre = this._getEntry(this._index); if (entryPre !== null) { @@ -1320,6 +1369,11 @@ class Display extends EventDispatcher { 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; @@ -1424,7 +1478,7 @@ class Display extends EventDispatcher { return templates; } - async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue) { + async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue, fetchAdditionalInfo) { const modeCount = modes.length; const notePromises = []; for (const dictionaryEntry of dictionaryEntries) { @@ -1442,7 +1496,7 @@ class Display extends EventDispatcher { } infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue); } else { - infos = await yomichan.api.getAnkiNoteInfo(notes); + infos = await yomichan.api.getAnkiNoteInfo(notes, fetchAdditionalInfo); } const results = []; @@ -1703,6 +1757,7 @@ class Display extends EventDispatcher { _addEntryEventListeners(entry) { 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)); diff --git a/ext/settings.html b/ext/settings.html index 7f9cc8ec..872dac4d 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -1565,6 +1565,34 @@
+
+
+
+
+ Show card tags + (?) +
+
+
+ +
+
+ +
Configure Anki card format…
diff --git a/test/test-options-util.js b/test/test-options-util.js index 4194861a..07b41635 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -431,6 +431,7 @@ function createProfileOptionsUpdatedTestData1() { terms: {deck: '', model: '', fields: {}}, kanji: {deck: '', model: '', fields: {}}, duplicateScope: 'collection', + displayTags: 'never', checkForDuplicates: true, fieldTemplates: null, suspendNewCards: false