From ba3f7b3e96df7f034b56132d8c2c90289e16c288 Mon Sep 17 00:00:00 2001
From: soriac <soriac@users.noreply.github.com>
Date: Fri, 30 Apr 2021 18:57:53 -0300
Subject: Show any custom tags on words that have anki cards created (#1628)

* Proof-of-concept for showing card tags (#1626)

* Resolved most PR comments:
- Added a snackbar notification when clicking tag button
- Replaced magnifying glass icon with new tag icon
- Button now contains a span w/icon, to use text color
- Removed unnecessary attributes from button
- Backend now returns full noteInfos object
- Frontend now handles filtering tags

* Add options to show/hide tag button & filter tags

* Do not show tags button if, after filtering, we have zero tags.

* Change tags option to enums, optimize tags intersection check & fix code style.

* Update options-util.js to include new tag options.

* Fix wording on new tag setting.

* Add CSS to remove hidden buttons from the display layout.

* getAnkiNoteInfo extra parameter for additional info.

* Add new tag option to tests.

* Remove unnecessary changes related to anki tags option.

* Code style fixes.
---
 ext/js/background/backend.js |  5 +++-
 ext/js/comm/anki.js          |  6 ++++
 ext/js/comm/api.js           |  4 +--
 ext/js/data/options-util.js  |  3 ++
 ext/js/display/display.js    | 65 ++++++++++++++++++++++++++++++++++++++++----
 5 files changed, 75 insertions(+), 8 deletions(-)

(limited to 'ext/js')

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));
-- 
cgit v1.2.3