Repository: yomidevs/yomitan
Pull request #1978: Change view notes button color based on card state
Author: login: KolbyML
Labels: name: kind/enhancement
description: The issue or PR is a new feature or request
name: area/ui-ux
description: The issue or PR is related to UI/UX/Design
name: area/anki
description: The issue or PR is related to Anki integration
Created: 2025-05-01T20:19:58Z | Merged: 2025-05-02T16:12:07Z

I want this change, because I only would want to overwrite a card if it is suspended, as if it is suspended I know the card is no good, and perhaps be remaking the card will help me learn the word. As the new card would contain fresh context

![image](https://github.com/user-attachments/assets/2e9e1513-657c-4534-994c-f46005d43f2f)




Diff:
diff --git a/ext/css/display.css b/ext/css/display.css
index 3aa646525a..cbbb8f3737 100644
--- a/ext/css/display.css
+++ b/ext/css/display.css
@@ -741,7 +741,12 @@ button.action-button:active {
     background-color: var(--action-button-active-color);
     box-shadow: none;
 }
-.icon[data-icon=view-note]      { background-image: url('/images/view-note.svg'); }
+
+.icon[data-icon=view-note] { background-image: url('/images/view-note.svg'); }
+.icon[data-icon=view-note-learning] { background-image: url('/images/view-note-learning.svg'); }
+.icon[data-icon=view-note-review] { background-image: url('/images/view-note-review.svg'); }
+.icon[data-icon=view-note-suspended] { background-image: url('/images/view-note-suspended.svg'); }
+.icon[data-icon=view-note-buried] { background-image: url('/images/view-note-buried.svg'); }
 .icon[data-icon=big-circle] { background-image: url('/images/big-circle.svg'); }
 .icon[data-icon=small-circle]  { background-image: url('/images/small-circle.svg'); }
 .icon[data-icon=big-square] { background-image: url('/images/big-square.svg'); }
diff --git a/ext/images/view-note-buried.svg b/ext/images/view-note-buried.svg
new file mode 100644
index 0000000000..354398afca
--- /dev/null
+++ b/ext/images/view-note-buried.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="16"
+   height="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg9"
+   sodipodi:docname="view-note-buried.svg"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview9"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="49"
+     inkscape:cx="7.9897959"
+     inkscape:cy="8"
+     inkscape:window-width="1920"
+     inkscape:window-height="1007"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g9" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient5010"
+       x2="-3.9688"
+       y1="3.7042"
+       y2="1.0583"
+       gradientTransform="matrix(3.7795 0 0 3.7795 15.5 -.5)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop1"
+         style="stop-color:#5b3f01;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop2"
+         style="stop-color:#c6a263;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5018-3"
+       x1="3.8365"
+       x2=".39687"
+       y1="3.0427"
+       y2=".59531"
+       gradientTransform="scale(3.7795 3.7795)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop3"
+         style="stop-color:#5b3f01;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop4"
+         style="stop-color:#c6a263;stop-opacity:1;" />
+    </linearGradient>
+  </defs>
+  <g
+     stroke-width="1px"
+     id="g9">
+    <path
+       d="m 9.75,13.5 -1,1 h -1.5 l -1,-1 H 0.5 v -10 h 6 l 1,1 h 1 l 1,-1 h 6 v 10 h -1.479592 z"
+       fill="#5d8bb3"
+       stroke="url(#linearGradient5010)"
+       id="path4"
+       sodipodi:nodetypes="cccccccccccccc"
+       style="fill:#b3955d;fill-opacity:1" />
+    <path
+       d="m 14.5,11.5 h -5 l -1,1 h -1 l -1,-1 h -5 V 2.25 h 5 l 1,1 h 1 l 1,-1 h 5 v 9.25"
+       fill="#f8f8f8"
+       stroke="url(#linearGradient5018-3)"
+       id="path5"
+       sodipodi:nodetypes="ccccccccccccc" />
+    <g
+       fill="none"
+       id="g8">
+      <path
+         d="m10 8.75h3m-3-2h3m-3-2h3m-10 4h3m-3-2h3m-3-2h3"
+         stroke="#bdbdbd"
+         id="path6" />
+      <path
+         d="m7.5 10.25v-4.75"
+         stroke="#a6a6a6"
+         id="path7" />
+      <path
+         d="m8.5 5.75v4.25"
+         stroke="#d8d8d8"
+         id="path8" />
+    </g>
+  </g>
+</svg>
diff --git a/ext/images/view-note-learning.svg b/ext/images/view-note-learning.svg
new file mode 100644
index 0000000000..4c58e88950
--- /dev/null
+++ b/ext/images/view-note-learning.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="16"
+   height="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg9"
+   sodipodi:docname="view-note-learning.svg"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview9"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="49"
+     inkscape:cx="7.9897959"
+     inkscape:cy="8"
+     inkscape:window-width="1920"
+     inkscape:window-height="1007"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g9" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient5010"
+       x2="-3.9688"
+       y1="3.7042"
+       y2="1.0583"
+       gradientTransform="matrix(3.7795 0 0 3.7795 15.5 -.5)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop1"
+         style="stop-color:#5b0101;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop2"
+         style="stop-color:#c66368;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5018-3"
+       x1="3.8365"
+       x2=".39687"
+       y1="3.0427"
+       y2=".59531"
+       gradientTransform="scale(3.7795 3.7795)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop3"
+         style="stop-color:#5b0101;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop4"
+         style="stop-color:#c66368;stop-opacity:1;" />
+    </linearGradient>
+  </defs>
+  <g
+     stroke-width="1px"
+     id="g9">
+    <path
+       d="m 9.75,13.5 -1,1 h -1.5 l -1,-1 H 0.5 v -10 h 6 l 1,1 h 1 l 1,-1 h 6 v 10 z"
+       fill="#5d8bb3"
+       stroke="url(#linearGradient5010)"
+       id="path4"
+       sodipodi:nodetypes="ccccccccccccc"
+       style="fill:#b35d60;fill-opacity:1" />
+    <path
+       d="m 14.5,11.5 h -5 l -1,1 h -1 l -1,-1 h -5 V 2.25 h 5 l 1,1 h 1 l 1,-1 h 5 v 9.25"
+       fill="#f8f8f8"
+       stroke="url(#linearGradient5018-3)"
+       id="path5"
+       sodipodi:nodetypes="ccccccccccccc" />
+    <g
+       fill="none"
+       id="g8">
+      <path
+         d="m10 8.75h3m-3-2h3m-3-2h3m-10 4h3m-3-2h3m-3-2h3"
+         stroke="#bdbdbd"
+         id="path6" />
+      <path
+         d="m7.5 10.25v-4.75"
+         stroke="#a6a6a6"
+         id="path7" />
+      <path
+         d="m8.5 5.75v4.25"
+         stroke="#d8d8d8"
+         id="path8" />
+    </g>
+  </g>
+</svg>
diff --git a/ext/images/view-note-review.svg b/ext/images/view-note-review.svg
new file mode 100644
index 0000000000..8c62f9f097
--- /dev/null
+++ b/ext/images/view-note-review.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="16"
+   height="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg9"
+   sodipodi:docname="view-note-review.svg"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview9"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="49"
+     inkscape:cx="7.9897959"
+     inkscape:cy="8"
+     inkscape:window-width="1920"
+     inkscape:window-height="1007"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g9" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient5010"
+       x2="-3.9688"
+       y1="3.7042"
+       y2="1.0583"
+       gradientTransform="matrix(3.7795 0 0 3.7795 15.5 -.5)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop1"
+         style="stop-color:#015b12;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop2"
+         style="stop-color:#63c670;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5018-3"
+       x1="3.8365"
+       x2=".39687"
+       y1="3.0427"
+       y2=".59531"
+       gradientTransform="scale(3.7795 3.7795)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop3"
+         style="stop-color:#015b12;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop4"
+         style="stop-color:#63c670;stop-opacity:1;" />
+    </linearGradient>
+  </defs>
+  <g
+     stroke-width="1px"
+     id="g9">
+    <path
+       d="m 9.75,13.5 -1,1 h -1.5 l -1,-1 H 0.5 v -10 h 6 l 1,1 h 1 l 1,-1 h 6 v 10 z"
+       fill="#5d8bb3"
+       stroke="url(#linearGradient5010)"
+       id="path4"
+       sodipodi:nodetypes="ccccccccccccc"
+       style="fill:#5db36a;fill-opacity:1" />
+    <path
+       d="m 14.5,11.5 h -5 l -1,1 h -1 l -1,-1 h -5 V 2.25 h 5 l 1,1 h 1 l 1,-1 h 5 v 9.25"
+       fill="#f8f8f8"
+       stroke="url(#linearGradient5018-3)"
+       id="path5"
+       sodipodi:nodetypes="ccccccccccccc" />
+    <g
+       fill="none"
+       id="g8">
+      <path
+         d="m10 8.75h3m-3-2h3m-3-2h3m-10 4h3m-3-2h3m-3-2h3"
+         stroke="#bdbdbd"
+         id="path6" />
+      <path
+         d="m7.5 10.25v-4.75"
+         stroke="#a6a6a6"
+         id="path7" />
+      <path
+         d="m8.5 5.75v4.25"
+         stroke="#d8d8d8"
+         id="path8" />
+    </g>
+  </g>
+</svg>
diff --git a/ext/images/view-note-suspended.svg b/ext/images/view-note-suspended.svg
new file mode 100644
index 0000000000..2b70b53134
--- /dev/null
+++ b/ext/images/view-note-suspended.svg
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="16"
+   height="16"
+   version="1.1"
+   viewBox="0 0 16 16"
+   id="svg9"
+   sodipodi:docname="view-note-suspended.svg"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview9"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="49"
+     inkscape:cx="7.9897959"
+     inkscape:cy="8"
+     inkscape:window-width="1920"
+     inkscape:window-height="1007"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g9" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient5010"
+       x2="-3.9688"
+       y1="3.7042"
+       y2="1.0583"
+       gradientTransform="matrix(3.7795 0 0 3.7795 15.5 -.5)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop1"
+         style="stop-color:#545b01;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop2"
+         style="stop-color:#c3c663;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5018-3"
+       x1="3.8365"
+       x2=".39687"
+       y1="3.0427"
+       y2=".59531"
+       gradientTransform="scale(3.7795 3.7795)"
+       gradientUnits="userSpaceOnUse">
+      <stop
+         stop-color="#012e5b"
+         offset="0"
+         id="stop3"
+         style="stop-color:#545b01;stop-opacity:1;" />
+      <stop
+         stop-color="#6399c6"
+         offset="1"
+         id="stop4"
+         style="stop-color:#c1c663;stop-opacity:1;" />
+    </linearGradient>
+  </defs>
+  <g
+     stroke-width="1px"
+     id="g9">
+    <path
+       d="m9.75 13.5-1 1h-1.5l-1-1h-5.75v-10h6l1 1h1l1-1h6v10z"
+       fill="#5d8bb3"
+       stroke="url(#linearGradient5010)"
+       id="path4"
+       style="fill:#adb35d;fill-opacity:1" />
+    <path
+       d="m14.5 11.5h-5l-1 1h-1l-1-1h-5v-9.25h5l1 1h1l1-1h5v9.25"
+       fill="#f8f8f8"
+       stroke="url(#linearGradient5018-3)"
+       id="path5" />
+    <g
+       fill="none"
+       id="g8">
+      <path
+         d="m10 8.75h3m-3-2h3m-3-2h3m-10 4h3m-3-2h3m-3-2h3"
+         stroke="#bdbdbd"
+         id="path6" />
+      <path
+         d="m7.5 10.25v-4.75"
+         stroke="#a6a6a6"
+         id="path7" />
+      <path
+         d="m8.5 5.75v4.25"
+         stroke="#d8d8d8"
+         id="path8" />
+    </g>
+  </g>
+</svg>
diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js
index 2793864c4f..19797e8a84 100644
--- a/ext/js/comm/anki-connect.js
+++ b/ext/js/comm/anki-connect.js
@@ -697,7 +697,7 @@ export class AnkiConnect {
                 result2.push(null);
                 continue;
             }
-            const {note, flags} = /** @type {{[key: string]: unknown}} */ (item);
+            const {note, flags, queue} = /** @type {{[key: string]: unknown}} */ (item);
             if (typeof note !== 'number') {
                 result2.push(null);
                 continue;
@@ -708,6 +708,7 @@ export class AnkiConnect {
                 noteId: note,
                 cardId,
                 flags: typeof flags === 'number' ? flags : 0,
+                cardState: typeof queue === 'number' ? queue : 0,
             };
             result2.push(item2);
         }
diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js
index 5d1abd7908..fb7af725fe 100644
--- a/ext/js/display/display-anki.js
+++ b/ext/js/display/display-anki.js
@@ -468,7 +468,7 @@ export class DisplayAnki {
 
                 const validNoteIds = noteIds?.filter((id) => id !== INVALID_NOTE_ID) ?? [];
 
-                this._createViewNoteButton(entryIndex, cardFormatIndex, validNoteIds);
+                this._createViewNoteButton(entryIndex, cardFormatIndex, validNoteIds, Array.isArray(noteInfos) ? noteInfos : []);
 
                 if (displayTagsAndFlags !== 'never' && Array.isArray(noteInfos)) {
                     this._setupTagsIndicator(entryIndex, cardFormatIndex, noteInfos);
@@ -791,7 +791,7 @@ export class DisplayAnki {
         /** @type {HTMLButtonElement | null} */
         let viewNoteButton = singleNoteActions.querySelector('.action-button[data-action=view-note]');
         if (viewNoteButton === null) {
-            viewNoteButton = this._createViewNoteButton(dictionaryEntryIndex, cardFormatIndex, noteIds);
+            viewNoteButton = this._createViewNoteButton(dictionaryEntryIndex, cardFormatIndex, noteIds, []);
         }
         if (viewNoteButton === null) { return; }
         const newNoteIds = new Set([...this._getNodeNoteIds(viewNoteButton), ...noteIds]);
@@ -913,13 +913,20 @@ export class DisplayAnki {
         return templates;
     }
 
+    /**
+     * Checks whether fetching additional information (e.g. tags and flags, or overwrite) is enabled
+     * based on the current instance's display settings and duplicate handling behavior.
+     * @returns {boolean} - True if additional info fetching is enabled, false otherwise.
+     */
+    _isAdditionalInfoEnabled() {
+        return this._displayTagsAndFlags !== 'never' || this._duplicateBehavior === 'overwrite';
+    }
+
     /**
      * @param {import('dictionary').DictionaryEntry[]} dictionaryEntries
      * @returns {Promise<import('display-anki').DictionaryEntryDetails[]>}
      */
     async _getDictionaryEntryDetails(dictionaryEntries) {
-        const fetchAdditionalInfo = (this._displayTagsAndFlags !== 'never') || this._duplicateBehavior === 'overwrite';
-
         const notePromises = [];
         const noteTargets = [];
         for (let i = 0, ii = dictionaryEntries.length; i < ii; ++i) {
@@ -944,7 +951,7 @@ export class DisplayAnki {
             }
 
             infos = this._checkForDuplicates ?
-                await this._display.application.api.getAnkiNoteInfo(notes, fetchAdditionalInfo) :
+                await this._display.application.api.getAnkiNoteInfo(notes, this._isAdditionalInfoEnabled()) :
                 this._getAnkiNoteInfoForceValue(notes, true);
         } catch (e) {
             infos = this._getAnkiNoteInfoForceValue(notes, false);
@@ -1110,17 +1117,20 @@ export class DisplayAnki {
      * @param {number} index
      * @param {number} cardFormatIndex
      * @param {number[]} noteIds
+     * @param {(?import('anki').NoteInfo)[]} noteInfos
      * @returns {?HTMLButtonElement}
      */
-    _createViewNoteButton(index, cardFormatIndex, noteIds) {
+    _createViewNoteButton(index, cardFormatIndex, noteIds, noteInfos) {
         if (noteIds.length === 0) { return null; }
-        const viewNoteButton = /** @type {HTMLButtonElement} */ (this._display.displayGenerator.instantiateTemplate('note-action-button-view-note'));
+        let viewNoteButton = /** @type {HTMLButtonElement} */ (this._display.displayGenerator.instantiateTemplate('note-action-button-view-note'));
         if (viewNoteButton === null) { return null; }
         const disabled = (noteIds.length === 0);
         viewNoteButton.disabled = disabled;
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        viewNoteButton = this._setViewNoteButtonCardState(noteInfos, viewNoteButton);
+
         this._setViewButtonBadge(viewNoteButton);
 
         const entry = this._getEntry(index);
@@ -1139,6 +1149,32 @@ export class DisplayAnki {
         return viewNoteButton;
     }
 
+    /**
+     * @param {(?import('anki').NoteInfo)[]} noteInfos
+     * @param {HTMLButtonElement} viewNoteButton
+     * @returns {HTMLButtonElement}
+     */
+    _setViewNoteButtonCardState(noteInfos, viewNoteButton) {
+        if (this._isAdditionalInfoEnabled() === false || noteInfos.length === 0) { return viewNoteButton; }
+
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (viewNoteButton.querySelector('.icon[data-icon^="view-note"]'));
+        dataIcon.dataset.icon = highestState !== 'new' ? `view-note-${highestState}` : 'view-note';
+
+        const label = `View added note (${highestState})`;
+        viewNoteButton.title = label;
+        viewNoteButton.dataset.hotkey = JSON.stringify(['viewNotes', 'title', `${label} ({0})`]);
+        return viewNoteButton;
+    }
+
     /**
      * @param {HTMLButtonElement} viewNoteButton
      */
@@ -1303,6 +1339,35 @@ export class DisplayAnki {
 
         return 'linear-gradient(to right,' + gradientSlices.join(',') + ')';
     }
+
+    /**
+     * Get the highest priority state from a list of Anki queue states.
+     * Source: https://github.com/ankidroid/Anki-Android/wiki/Database-Structure#cards
+     *
+     * Priority order:
+     *   - -3, -2 → "buried"
+     *   - -1 → "suspended"
+     *   -  2 → "review"
+     *   -  1, 3 → "learning"
+     *   -  0 → "new" (default fallback)
+     * @param {number[]} cardStates Array of queue state integers.
+     * @returns {"buried" | "suspended" | "review" | "learning" | "new" } - The highest priority state found.
+     */
+    _getHighestPriorityCardState(cardStates) {
+        if (cardStates.includes(-3) || cardStates.includes(-2)) {
+            return 'buried';
+        }
+        if (cardStates.includes(-1)) {
+            return 'suspended';
+        }
+        if (cardStates.includes(2)) {
+            return 'review';
+        }
+        if (cardStates.includes(1) || cardStates.includes(3)) {
+            return 'learning';
+        }
+        return 'new';
+    }
 }
 
 class DisplayAnkiError extends Error {
diff --git a/types/ext/anki.d.ts b/types/ext/anki.d.ts
index 0d9ea8d339..77fe54d28b 100644
--- a/types/ext/anki.d.ts
+++ b/types/ext/anki.d.ts
@@ -69,6 +69,7 @@ export type CardInfo = {
     noteId: NoteId;
     cardId: CardId;
     flags: number;
+    cardState: number;
 };
 
 export type ApiReflectResult = {


Reviews:
user: login: Kuuuube
body: A few things from a quick look.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-01T20:31:17Z
user: login: KolbyML
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-01T20:45:05Z
user: login: Kuuuube
body: A few small things.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-01T23:21:02Z
user: login: KolbyML
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T00:53:16Z
user: login: KolbyML
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T01:08:28Z
user: login: Kuuuube
body: Looks good. Since there was some debate about implementation on discord I'll leave this open for a bit incase anyone else wants to test and comment on anything.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-02T01:13:32Z
user: login: Kuuuube
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-02T15:55:30Z
user: login: KolbyML
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T15:58:24Z
user: login: Kuuuube
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T16:03:11Z
user: login: KolbyML
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T16:07:06Z
user: login: Kuuuube
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-02T16:09:33Z

Review comments:
diff_hunk: @@ -60,6 +60,6 @@
 }
 
 </style>
-<table><thead><tr><th class="string">name</th><th class="string">installed version</th><th class="string">license type</th><th class="string">link</th></tr></thead><tbody><tr><td class="string">@resvg/resvg-wasm</td><td class="string">2.6.2</td><td class="string">MPL-2.0</td><td class="string">git+ssh://git@github.com/yisibl/resvg-js.git</td></tr><tr><td class="string">@zip.js/zip.js</td><td class="string">2.7.54</td><td class="string">BSD-3-Clause</td><td class="string">git+https://github.com/gildas-lormeau/zip.js.git</td></tr><tr><td class="string">dexie</td><td class="string">3.2.5</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dfahlander/Dexie.js.git</td></tr><tr><td class="string">dexie-export-import</td><td class="string">4.1.4</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dexie/Dexie.js.git</td></tr><tr><td class="string">hangul-js</td><td class="string">0.2.6</td><td class="string">MIT</td><td class="string">git://github.com/e-/Hangul.js.git</td></tr><tr><td class="string">parse5</td><td class="string">7.2.1</td><td class="string">MIT</td><td class="string">git://github.com/inikulin/parse5.git</td></tr><tr><td class="string">yomitan-handlebars</td><td class="string">1.0.0</td><td class="string">MIT</td><td class="string">n/a</td></tr></tbody></table>
+<table><thead><tr><th class="string">name</th><th class="string">installed version</th><th class="string">license type</th><th class="string">link</th></tr></thead><tbody><tr><td class="string">@resvg/resvg-wasm</td><td class="string">2.6.2</td><td class="string">MPL-2.0</td><td class="string">git+ssh://git@github.com/yisibl/resvg-js.git</td></tr><tr><td class="string">@zip.js/zip.js</td><td class="string">2.7.54</td><td class="string">BSD-3-Clause</td><td class="string">git+https://github.com/gildas-lormeau/zip.js.git</td></tr><tr><td class="string">dexie</td><td class="string">4.0.11</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dexie/Dexie.js.git</td></tr><tr><td class="string">dexie-export-import</td><td class="string">4.1.4</td><td class="string">Apache-2.0</td><td class="string">git+https://github.com/dexie/Dexie.js.git</td></tr><tr><td class="string">hangul-js</td><td class="string">0.2.6</td><td class="string">MIT</td><td class="string">git://github.com/e-/Hangul.js.git</td></tr><tr><td class="string">parse5</td><td class="string">7.2.1</td><td class="string">MIT</td><td class="string">git://github.com/inikulin/parse5.git</td></tr><tr><td class="string">yomitan-handlebars</td><td class="string">1.0.0</td><td class="string">MIT</td><td class="string">n/a</td></tr></tbody></table>
path: ext/legal-npm.html
user: login: Kuuuube
body: Even if this is correct to update, I'd rather this not happen in this pr.
created_at: 2025-05-01T20:28:15Z
author_association: MEMBER
diff_hunk: @@ -1121,6 +1122,20 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        let isSuspended = false;
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                if (cardInfo.queue === -1) {
+                    isSuspended = true;
+                    break;
+                }
+            }
+        }
+        if (isSuspended) {
+            viewNoteButton.style.setProperty('background-color', 'yellow', 'important');
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: This should be a class defined in the popup css.
created_at: 2025-05-01T20:29:39Z
author_association: MEMBER
diff_hunk: @@ -708,6 +708,7 @@ export class AnkiConnect {
                 noteId: note,
                 cardId,
                 flags: typeof flags === 'number' ? flags : 0,
+                queue: typeof queue === 'number' ? queue : 0,
path: ext/js/comm/anki-connect.js
user: login: Kuuuube
body: Even though it's what Anki calls it, `queue` is a terrible name for this. Maybe `cardState`.
created_at: 2025-05-01T20:31:13Z
author_association: MEMBER
diff_hunk: @@ -1121,6 +1122,20 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        let isSuspended = false;
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                if (cardInfo.queue === -1) {
+                    isSuspended = true;
+                    break;
+                }
+            }
+        }
+        if (isSuspended) {
+            viewNoteButton.style.setProperty('background-color', 'yellow', 'important');
path: ext/js/display/display-anki.js
user: login: KolbyML
body: I wasn't sure how it get it to work in `action-popup.css` so I put it in `display.css` as that is where the other css classes were for the buttons
created_at: 2025-05-01T20:45:04Z
author_association: NONE
diff_hunk: @@ -1121,6 +1122,28 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        // Apply priority-based data-icon
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (
+            viewNoteButton.querySelector('.icon[data-icon^="view-note-"]')
+        );
+        dataIcon.dataset.icon = `view-note-${highestState}`;
+
+        // Build consistent label
+        const label = `View added note (${highestState} state)`;
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: Telling the user it's a `state` here seems redundant.
created_at: 2025-05-01T23:04:41Z
author_association: MEMBER
diff_hunk: @@ -1121,6 +1122,28 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        // Apply priority-based data-icon
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (
+            viewNoteButton.querySelector('.icon[data-icon^="view-note-"]')
+        );
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: Why is this wrapped in extra parentheses and taking up three lines?
created_at: 2025-05-01T23:05:05Z
author_association: MEMBER
diff_hunk: @@ -1303,6 +1326,34 @@ export class DisplayAnki {
 
         return 'linear-gradient(to right,' + gradientSlices.join(',') + ')';
     }
+
+    /**
+     * Get the highest priority state from a list of Anki queue states.
+     *
+     * Priority order:
+     *   - -3, -2 → "buried"
+     *   - -1 → "suspended"
+     *   -  2 → "review"
+     *   -  1, 3 → "learning"
+     *   -  0 → "new" (default fallback)
+     * @param {number[]} cardStates Array of queue state integers.
+     * @returns {"buried" | "suspended" | "review" | "learning" | "new"} - The highest priority state found.
+     */
+    _getHighestPriorityCardState(cardStates) {
+        if (cardStates.includes(-3) || cardStates.includes(-2)) {
+            return 'buried';
+        }
+        if (cardStates.includes(-1)) {
+            return 'suspended';
+        }
+        if (cardStates.includes(2)) {
+            return 'review';
+        }
+        if (cardStates.includes(1) || cardStates.includes(3)) {
+            return 'learning';
+        }
+        return 'new';
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: This needs to not default to `new`. If `noteInfos` is not searched for, Yomitan will always say `new` on the view notes button. Maybe make this null or empty string and add a case where the state isnt shown in the tooltip. Maybe add back `view-note.svg` so it can be a clear default.
created_at: 2025-05-01T23:13:01Z
author_association: MEMBER
diff_hunk: @@ -1303,6 +1326,34 @@ export class DisplayAnki {
 
         return 'linear-gradient(to right,' + gradientSlices.join(',') + ')';
     }
+
+    /**
+     * Get the highest priority state from a list of Anki queue states.
+     *
+     * Priority order:
+     *   - -3, -2 → "buried"
+     *   - -1 → "suspended"
+     *   -  2 → "review"
+     *   -  1, 3 → "learning"
+     *   -  0 → "new" (default fallback)
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: It's worth mentioning the source of this (https://github.com/ankidroid/Anki-Android/wiki/Database-Structure).
created_at: 2025-05-01T23:15:05Z
author_association: MEMBER
diff_hunk: @@ -1121,6 +1122,28 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        // Apply priority-based data-icon
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (
+            viewNoteButton.querySelector('.icon[data-icon^="view-note-"]')
+        );
+        dataIcon.dataset.icon = `view-note-${highestState}`;
+
+        // Build consistent label
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: How would this comment ever help anyone in the future?
created_at: 2025-05-01T23:16:53Z
author_association: MEMBER
diff_hunk: @@ -1121,6 +1122,28 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        // Apply priority-based data-icon
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (
+            viewNoteButton.querySelector('.icon[data-icon^="view-note-"]')
+        );
+        dataIcon.dataset.icon = `view-note-${highestState}`;
+
+        // Build consistent label
+        const label = `View added note (${highestState} state)`;
+
+        // Set both title and data-hotkey to match
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: Same as above, it's obvious what this is doing. This doesn't need a comment.
created_at: 2025-05-01T23:17:25Z
author_association: MEMBER
diff_hunk: @@ -1121,6 +1122,28 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        // Apply priority-based data-icon
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (
+            viewNoteButton.querySelector('.icon[data-icon^="view-note-"]')
+        );
path: ext/js/display/display-anki.js
user: login: KolbyML
body: I made it take up one line, the extra parenthese is the only way I get get jsdocs to work for me, without throwing an warning and claiming dataIcon is a different type then I am declaring it as
created_at: 2025-05-02T00:53:16Z
author_association: NONE
diff_hunk: @@ -1121,6 +1122,28 @@ export class DisplayAnki {
         viewNoteButton.hidden = disabled;
         viewNoteButton.dataset.noteIds = noteIds.join(' ');
 
+        const cardStates = [];
+        for (const item of noteInfos) {
+            if (item === null) { continue; }
+            for (const cardInfo of item.cardsInfo) {
+                cardStates.push(cardInfo.cardState);
+            }
+        }
+
+        // Apply priority-based data-icon
+        const highestState = this._getHighestPriorityCardState(cardStates);
+        const dataIcon = /** @type {HTMLElement} */ (
+            viewNoteButton.querySelector('.icon[data-icon^="view-note-"]')
+        );
path: ext/js/display/display-anki.js
user: login: KolbyML
body: I seen this done in other parts of the codebase for reference
created_at: 2025-05-02T01:08:28Z
author_association: NONE
diff_hunk: @@ -1139,6 +1149,32 @@ export class DisplayAnki {
         return viewNoteButton;
     }
 
+    /**
+     * @param {(?import('anki').NoteInfo)[]} noteInfos
+     * @param {HTMLButtonElement} viewNoteButton
+     * @returns {HTMLButtonElement}
+     */
+    _setViewNoteButtonCardState(noteInfos, viewNoteButton) {
+        if (this._isAdditionalInfoEnabled() === false) { return viewNoteButton; }
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: While we know it will be true in this case, there no guarantee that `this._isAdditionalInfoEnabled()` being false will always mean `noteInfos` is empty. Just check `noteInfos` here.
created_at: 2025-05-02T15:53:27Z
author_association: MEMBER
diff_hunk: @@ -1139,6 +1149,32 @@ export class DisplayAnki {
         return viewNoteButton;
     }
 
+    /**
+     * @param {(?import('anki').NoteInfo)[]} noteInfos
+     * @param {HTMLButtonElement} viewNoteButton
+     * @returns {HTMLButtonElement}
+     */
+    _setViewNoteButtonCardState(noteInfos, viewNoteButton) {
+        if (this._isAdditionalInfoEnabled() === false) { return viewNoteButton; }
path: ext/js/display/display-anki.js
user: login: KolbyML
body: `this._isAdditionalInfoEnabled()` being false means this feature isn't enable.

Although I am not sure how `noteInfos` could have items if `this._isAdditionalInfoEnabled()` was false, from reading the code.
created_at: 2025-05-02T15:58:24Z
author_association: NONE
diff_hunk: @@ -1139,6 +1149,32 @@ export class DisplayAnki {
         return viewNoteButton;
     }
 
+    /**
+     * @param {(?import('anki').NoteInfo)[]} noteInfos
+     * @param {HTMLButtonElement} viewNoteButton
+     * @returns {HTMLButtonElement}
+     */
+    _setViewNoteButtonCardState(noteInfos, viewNoteButton) {
+        if (this._isAdditionalInfoEnabled() === false) { return viewNoteButton; }
path: ext/js/display/display-anki.js
user: login: Kuuuube
body: I'm asking for this change not because the code doesn't work but because it could be messier in the future.

For example, if there is a change later that has a case where `this._isAdditionalInfoEnabled()` evaluates true but `noteInfos` is empty. You are checking whether things *should* be correct not whether they actually are correct.

How about this:

```suggestion
        if (this._isAdditionalInfoEnabled() === false || noteInfos.length === 0) { return viewNoteButton; }
```
created_at: 2025-05-02T16:03:11Z
author_association: MEMBER
diff_hunk: @@ -1139,6 +1149,32 @@ export class DisplayAnki {
         return viewNoteButton;
     }
 
+    /**
+     * @param {(?import('anki').NoteInfo)[]} noteInfos
+     * @param {HTMLButtonElement} viewNoteButton
+     * @returns {HTMLButtonElement}
+     */
+    _setViewNoteButtonCardState(noteInfos, viewNoteButton) {
+        if (this._isAdditionalInfoEnabled() === false) { return viewNoteButton; }
path: ext/js/display/display-anki.js
user: login: KolbyML
body: > I'm asking for this change not because the code doesn't work but because it could be messier in the future.
> 
> For example, if there is a change later that has a case where `this._isAdditionalInfoEnabled()` evaluates true but `noteInfos` is empty. You are checking whether things _should_ be correct not whether they actually are correct.
> 
> How about this:

done
created_at: 2025-05-02T16:07:05Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-01T20:26:14Z
body: <!-- add-pr-comment:add-pr-comment -->

:heavy_check_mark: <a href="https://github.com/yomidevs/yomitan/actions/runs/14798872646/artifacts/3051815704">No visual changes detected by playwright.</a>
author_association: NONE
user: login: KolbyML
created_at: 2025-05-01T20:28:48Z
body: ```
[403] https://foosoft.net/projects/anki-connect | Network error: Forbidden
```
ci is failing due to this? It seems like some AI crawler blocker?
author_association: NONE
user: login: Kuuuube
created_at: 2025-05-01T20:33:32Z
body: > ci is failing due to this? It seems like some AI crawler blocker?

Yeah no need to worry about that. This was supposed to be handled with https://github.com/yomidevs/yomitan/pull/1972 but it looks like that link appears somewhere without the slash on the end so it broke the ignore.
author_association: MEMBER
user: login: KolbyML
created_at: 2025-05-01T20:45:16Z
body: @Kuuuube ready for another review
author_association: NONE
user: login: KolbyML
created_at: 2025-05-01T22:33:44Z
body: ![image](https://github.com/user-attachments/assets/7dd7ea7c-15e1-499d-b40e-bd1aad44b582)
![image](https://github.com/user-attachments/assets/58b9502c-f895-4860-abc3-7ff579d34e7b)
![image](https://github.com/user-attachments/assets/c0b77a28-1528-46fa-aeb1-21382cdba610)
![image](https://github.com/user-attachments/assets/ae16724f-8ae7-4c69-9d22-6dcc9dbb471e)


Here are all the colors, and some examples of the tooltip in action
author_association: NONE
user: login: KolbyML
created_at: 2025-05-01T22:40:09Z
body: @Kuuuube ready for another look
author_association: NONE
user: login: KolbyML
created_at: 2025-05-02T01:08:48Z
body: @Kuuuube ready for another look
author_association: NONE
user: login: KolbyML
created_at: 2025-05-02T01:14:27Z
body: Thank you for the reviews and feedback
author_association: NONE
user: login: YukiNagat0
created_at: 2025-05-02T03:34:31Z
body: Is it correct that the newly added logic **only works** when the `Show card tags and flags` option is **not** set to `Never`?
If this option is disabled in my setup, does that mean I **won't observe any behavioral changes**, such as:
1. There are only two states:
     a. Expression not in deck -> `Add expression` button
     b. Expression in deck -> `View added note` and `Add duplicate expression` buttons
2. The asset for the book **remains unchanged** (old image for the book for all card states)
3. **No additional data transfer** occurs between Yomitan-AnkiConnect, and no extra computations (e.g., iterating through card states or getting priority)

In other words, if the option is set to `Never`, there should be:
- **No visual differences** (book color, labels, etc.)
- **No performance impact**

This is because `noteInfos` is an empty array in this case, correct?
author_association: NONE
user: login: Kuuuube
created_at: 2025-05-02T03:36:05Z
body: Yep, tags and flags or overwrite duplicate action being enabled are currently the only two ways Yomitan will request the extra data for `noteInfos`.
author_association: MEMBER
user: login: YukiNagat0
created_at: 2025-05-02T03:38:41Z
body: > tags and flags or overwrite duplicate action

What if I have:
- `Show card tags and flags` - `Never`
- `Check for card duplicates` - `Enabled`
author_association: NONE
user: login: KolbyML
created_at: 2025-05-02T03:39:35Z
body: > > tags and flags or overwrite duplicate action
> 
> What if I have:
> 
> * `Show card tags and flags` - `Never`
> * `Check for card duplicates` - `Enabled`

it will be enabled if 
- `Show card tags and flags`
or 
- `Check for card duplicates, allow overwriting
is true

author_association: NONE
user: login: KolbyML
created_at: 2025-05-02T03:48:34Z
body: To be clear this is the conditions that will enable it ``const fetchAdditionalInfo = (this._displayTagsAndFlags !== 'never') || this._duplicateBehavior === 'overwrite';
``
author_association: NONE