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.
This commit is contained in:
parent
69a739f00a
commit
ba3f7b3e96
@ -644,6 +644,9 @@ button.action-button[hidden] {
|
|||||||
-webkit-filter var(--animation-duration) linear,
|
-webkit-filter var(--animation-duration) linear,
|
||||||
background-color var(--animation-duration) linear;
|
background-color var(--animation-duration) linear;
|
||||||
}
|
}
|
||||||
|
button.collapsible-action-button[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
button.action-button:disabled {
|
button.action-button:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@ -667,7 +670,7 @@ button.action-button:active {
|
|||||||
background-color: var(--action-button-active-color);
|
background-color: var(--action-button-active-color);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
button.action-button::before {
|
button.action-button[data-icon]::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: var(--action-button-size);
|
width: var(--action-button-size);
|
||||||
height: 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 {
|
button.action-button[data-icon=source-term]::before {
|
||||||
background-image: url('/images/source-term.svg');
|
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 {
|
:root[data-result-output-mode=merge] .entry[data-type=term] .actions>button.action-button.action-play-audio {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -806,7 +806,8 @@
|
|||||||
"duplicateScope",
|
"duplicateScope",
|
||||||
"checkForDuplicates",
|
"checkForDuplicates",
|
||||||
"fieldTemplates",
|
"fieldTemplates",
|
||||||
"suspendNewCards"
|
"suspendNewCards",
|
||||||
|
"displayTags"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"enable": {
|
"enable": {
|
||||||
@ -912,6 +913,11 @@
|
|||||||
"suspendNewCards": {
|
"suspendNewCards": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
|
},
|
||||||
|
"displayTags": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["never", "always", "non-standard"],
|
||||||
|
"default": "never"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
<div class="entry-current-indicator" title="Current entry"><span class="entry-current-indicator-inner"></span></div>
|
<div class="entry-current-indicator" title="Current entry"><span class="entry-current-indicator-inner"></span></div>
|
||||||
<div class="entry-header">
|
<div class="entry-header">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
<button class="action-button collapsible-action-button action-view-tags" hidden disabled><span class="icon" data-icon="tag"></span></button>
|
||||||
<button class="action-button action-view-note" hidden disabled data-icon="view-note" title="View added note" data-hotkey='["viewNote","title","View added note ({0})"]'></button>
|
<button class="action-button action-view-note" hidden disabled data-icon="view-note" title="View added note" data-hotkey='["viewNote","title","View added note ({0})"]'></button>
|
||||||
<button class="action-button action-add-note" hidden disabled data-icon="add-term-kanji" data-mode="term-kanji" title="Add expression" data-hotkey='["addNoteTermKanji","title","Add expression ({0})"]'></button>
|
<button class="action-button action-add-note" hidden disabled data-icon="add-term-kanji" data-mode="term-kanji" title="Add expression" data-hotkey='["addNoteTermKanji","title","Add expression ({0})"]'></button>
|
||||||
<button class="action-button action-add-note" hidden disabled data-icon="add-term-kana" data-mode="term-kana" title="Add reading" data-hotkey='["addNoteTermKana","title","Add reading ({0})"]'></button>
|
<button class="action-button action-add-note" hidden disabled data-icon="add-term-kana" data-mode="term-kana" title="Add reading" data-hotkey='["addNoteTermKana","title","Add reading ({0})"]'></button>
|
||||||
|
@ -458,7 +458,7 @@ class Backend {
|
|||||||
return await this._anki.addNote(note);
|
return await this._anki.addNote(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onApiGetAnkiNoteInfo({notes}) {
|
async _onApiGetAnkiNoteInfo({notes, fetchAdditionalInfo}) {
|
||||||
const results = [];
|
const results = [];
|
||||||
const cannotAdd = [];
|
const cannotAdd = [];
|
||||||
const canAddArray = await this._anki.canAddNotes(notes);
|
const canAddArray = await this._anki.canAddNotes(notes);
|
||||||
@ -482,6 +482,9 @@ class Backend {
|
|||||||
const noteIds = noteIdsArray[i];
|
const noteIds = noteIdsArray[i];
|
||||||
if (noteIds.length > 0) {
|
if (noteIds.length > 0) {
|
||||||
cannotAdd[i].info.noteIds = noteIds;
|
cannotAdd[i].info.noteIds = noteIds;
|
||||||
|
if (fetchAdditionalInfo) {
|
||||||
|
cannotAdd[i].info.noteInfos = await this._anki.notesInfo(noteIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,12 @@ class AnkiConnect {
|
|||||||
return await this._invoke('canAddNotes', {notes});
|
return await this._invoke('canAddNotes', {notes});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async notesInfo(notes) {
|
||||||
|
if (!this._enabled) { return []; }
|
||||||
|
await this._checkVersion();
|
||||||
|
return await this._invoke('notesInfo', {notes});
|
||||||
|
}
|
||||||
|
|
||||||
async getDeckNames() {
|
async getDeckNames() {
|
||||||
if (!this._enabled) { return []; }
|
if (!this._enabled) { return []; }
|
||||||
await this._checkVersion();
|
await this._checkVersion();
|
||||||
|
@ -52,8 +52,8 @@ class API {
|
|||||||
return this._invoke('addAnkiNote', {note});
|
return this._invoke('addAnkiNote', {note});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnkiNoteInfo(notes) {
|
getAnkiNoteInfo(notes, fetchAdditionalInfo) {
|
||||||
return this._invoke('getAnkiNoteInfo', {notes});
|
return this._invoke('getAnkiNoteInfo', {notes, fetchAdditionalInfo});
|
||||||
}
|
}
|
||||||
|
|
||||||
injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) {
|
injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) {
|
||||||
|
@ -792,6 +792,7 @@ class OptionsUtil {
|
|||||||
// Version 11 changes:
|
// Version 11 changes:
|
||||||
// Changed dictionaries to an array.
|
// Changed dictionaries to an array.
|
||||||
// Changed audio.customSourceUrl's {expression} marker to {term}.
|
// Changed audio.customSourceUrl's {expression} marker to {term}.
|
||||||
|
// Added anki.displayTags.
|
||||||
const customSourceUrlPattern = /\{expression\}/g;
|
const customSourceUrlPattern = /\{expression\}/g;
|
||||||
for (const profile of options.profiles) {
|
for (const profile of options.profiles) {
|
||||||
const dictionariesNew = [];
|
const dictionariesNew = [];
|
||||||
@ -805,6 +806,8 @@ class OptionsUtil {
|
|||||||
customSourceUrl = customSourceUrl.replace(customSourceUrlPattern, '{term}');
|
customSourceUrl = customSourceUrl.replace(customSourceUrlPattern, '{term}');
|
||||||
}
|
}
|
||||||
profile.options.audio.customSourceUrl = customSourceUrl;
|
profile.options.audio.customSourceUrl = customSourceUrl;
|
||||||
|
|
||||||
|
profile.options.anki.displayTags = 'never';
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,7 @@ class Display extends EventDispatcher {
|
|||||||
this._displayAudio = new DisplayAudio(this);
|
this._displayAudio = new DisplayAudio(this);
|
||||||
this._ankiNoteNotification = null;
|
this._ankiNoteNotification = null;
|
||||||
this._ankiNoteNotificationEventListeners = null;
|
this._ankiNoteNotificationEventListeners = null;
|
||||||
|
this._ankiTagNotification = null;
|
||||||
this._queryPostProcessor = null;
|
this._queryPostProcessor = null;
|
||||||
this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);
|
this._optionToggleHotkeyHandler = new OptionToggleHotkeyHandler(this);
|
||||||
this._elementOverflowController = new ElementOverflowController();
|
this._elementOverflowController = new ElementOverflowController();
|
||||||
@ -1081,8 +1082,8 @@ class Display extends EventDispatcher {
|
|||||||
let states;
|
let states;
|
||||||
try {
|
try {
|
||||||
const noteContext = this._getNoteContext();
|
const noteContext = this._getNoteContext();
|
||||||
const {checkForDuplicates} = this._options.anki;
|
const {checkForDuplicates, displayTags} = this._options.anki;
|
||||||
states = await this._areDictionaryEntriesAddable(dictionaryEntries, modes, noteContext, checkForDuplicates ? null : true);
|
states = await this._areDictionaryEntriesAddable(dictionaryEntries, modes, noteContext, checkForDuplicates ? null : true, displayTags !== 'never');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1096,11 +1097,12 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateAdderButtons2(states, modes) {
|
_updateAdderButtons2(states, modes) {
|
||||||
|
const {displayTags} = this._options.anki;
|
||||||
for (let i = 0, ii = states.length; i < ii; ++i) {
|
for (let i = 0, ii = states.length; i < ii; ++i) {
|
||||||
const infos = states[i];
|
const infos = states[i];
|
||||||
let noteId = null;
|
let noteId = null;
|
||||||
for (let j = 0, jj = infos.length; j < jj; ++j) {
|
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 mode = modes[j];
|
||||||
const button = this._adderButtonFind(i, mode);
|
const button = this._adderButtonFind(i, mode);
|
||||||
if (button === null) {
|
if (button === null) {
|
||||||
@ -1112,6 +1114,10 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
button.disabled = !canAdd;
|
button.disabled = !canAdd;
|
||||||
button.hidden = false;
|
button.hidden = false;
|
||||||
|
|
||||||
|
if (displayTags !== 'never' && Array.isArray(noteInfos)) {
|
||||||
|
this._setupTagsIndicator(i, noteInfos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (noteId !== null) {
|
if (noteId !== null) {
|
||||||
this._viewerButtonShow(i, noteId);
|
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) {
|
_entrySetCurrent(index) {
|
||||||
const entryPre = this._getEntry(this._index);
|
const entryPre = this._getEntry(this._index);
|
||||||
if (entryPre !== null) {
|
if (entryPre !== null) {
|
||||||
@ -1320,6 +1369,11 @@ class Display extends EventDispatcher {
|
|||||||
return entry !== null ? entry.querySelector(`.action-add-note[data-mode="${mode}"]`) : null;
|
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) {
|
_viewerButtonFind(index) {
|
||||||
const entry = this._getEntry(index);
|
const entry = this._getEntry(index);
|
||||||
return entry !== null ? entry.querySelector('.action-view-note') : null;
|
return entry !== null ? entry.querySelector('.action-view-note') : null;
|
||||||
@ -1424,7 +1478,7 @@ class Display extends EventDispatcher {
|
|||||||
return templates;
|
return templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue) {
|
async _areDictionaryEntriesAddable(dictionaryEntries, modes, context, forceCanAddValue, fetchAdditionalInfo) {
|
||||||
const modeCount = modes.length;
|
const modeCount = modes.length;
|
||||||
const notePromises = [];
|
const notePromises = [];
|
||||||
for (const dictionaryEntry of dictionaryEntries) {
|
for (const dictionaryEntry of dictionaryEntries) {
|
||||||
@ -1442,7 +1496,7 @@ class Display extends EventDispatcher {
|
|||||||
}
|
}
|
||||||
infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue);
|
infos = this._getAnkiNoteInfoForceValue(notes, forceCanAddValue);
|
||||||
} else {
|
} else {
|
||||||
infos = await yomichan.api.getAnkiNoteInfo(notes);
|
infos = await yomichan.api.getAnkiNoteInfo(notes, fetchAdditionalInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
@ -1703,6 +1757,7 @@ class Display extends EventDispatcher {
|
|||||||
|
|
||||||
_addEntryEventListeners(entry) {
|
_addEntryEventListeners(entry) {
|
||||||
this._eventListeners.addEventListener(entry, 'click', this._onEntryClick.bind(this));
|
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-add-note', 'click', this._onNoteAdd.bind(this));
|
||||||
this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteView.bind(this));
|
this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteView.bind(this));
|
||||||
this._addMultipleEventListeners(entry, '.headword-kanji-link', 'click', this._onKanjiLookup.bind(this));
|
this._addMultipleEventListeners(entry, '.headword-kanji-link', 'click', this._onKanjiLookup.bind(this));
|
||||||
|
@ -1565,6 +1565,34 @@
|
|||||||
<label class="toggle"><input type="checkbox" data-setting="anki.suspendNewCards"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label>
|
<label class="toggle"><input type="checkbox" data-setting="anki.suspendNewCards"><span class="toggle-body"><span class="toggle-track"></span><span class="toggle-knob"></span></span></label>
|
||||||
</div>
|
</div>
|
||||||
</div></div>
|
</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 class="more-toggle more-only" data-parent-distance="4">(?)</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item-right">
|
||||||
|
<select data-setting="anki.displayTags">
|
||||||
|
<option value="never">Never</option>
|
||||||
|
<option value="always">Always</option>
|
||||||
|
<option value="non-standard">Non-Standard</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-item-children more" hidden>
|
||||||
|
<p>
|
||||||
|
When coming across a word that is already in an Anki deck, a button will appear that shows
|
||||||
|
the tags the card has. If set to <em>Non-Standard</em>, all tags that are included in the
|
||||||
|
<em>Card tags</em> option will be filtered out from the list. If no tags remain after filtering,
|
||||||
|
then the button will not be shown.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a class="more-toggle" data-parent-distance="3">Less…</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="settings-item settings-item-button" data-modal-action="show,anki-cards"><div class="settings-item-inner">
|
<div class="settings-item settings-item-button" data-modal-action="show,anki-cards"><div class="settings-item-inner">
|
||||||
<div class="settings-item-left">
|
<div class="settings-item-left">
|
||||||
<div class="settings-item-label">Configure Anki card format…</div>
|
<div class="settings-item-label">Configure Anki card format…</div>
|
||||||
|
@ -431,6 +431,7 @@ function createProfileOptionsUpdatedTestData1() {
|
|||||||
terms: {deck: '', model: '', fields: {}},
|
terms: {deck: '', model: '', fields: {}},
|
||||||
kanji: {deck: '', model: '', fields: {}},
|
kanji: {deck: '', model: '', fields: {}},
|
||||||
duplicateScope: 'collection',
|
duplicateScope: 'collection',
|
||||||
|
displayTags: 'never',
|
||||||
checkForDuplicates: true,
|
checkForDuplicates: true,
|
||||||
fieldTemplates: null,
|
fieldTemplates: null,
|
||||||
suspendNewCards: false
|
suspendNewCards: false
|
||||||
|
Loading…
Reference in New Issue
Block a user