Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Anki card flags #1571

Merged
merged 20 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/css/material.css
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ body {
.icon[data-icon=clipboard] { --icon-image: url(/images/clipboard.svg); }
.icon[data-icon=key] { --icon-image: url(/images/key.svg); }
.icon[data-icon=tag] { --icon-image: url(/images/tag.svg); }
.icon[data-icon=flag] { --icon-image: url(/images/flag.svg); }
.icon[data-icon=accessibility] { --icon-image: url(/images/accessibility.svg); }
.icon[data-icon=connection] { --icon-image: url(/images/connection.svg); }
.icon[data-icon=external-link] { --icon-image: url(/images/external-link.svg); }
Expand Down
4 changes: 2 additions & 2 deletions ext/data/schemas/options-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@
"checkForDuplicates",
"fieldTemplates",
"suspendNewCards",
"displayTags",
"displayTagsAndFlags",
"noteGuiMode",
"apiKey",
"downloadTimeout"
Expand Down Expand Up @@ -1046,7 +1046,7 @@
"type": "boolean",
"default": false
},
"displayTags": {
"displayTagsAndFlags": {
"type": "string",
"enum": ["never", "always", "non-standard"],
"default": "never"
Expand Down
1 change: 1 addition & 0 deletions ext/images/flag.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion ext/js/background/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ export class Backend {
}

const noteIds = isDuplicate ? duplicateNoteIds[originalIndices.indexOf(i)] : null;
const noteInfos = (fetchAdditionalInfo && noteIds !== null && noteIds.length > 0) ? await this._anki.notesInfo(noteIds) : [];
const noteInfos = (fetchAdditionalInfo && noteIds !== null && noteIds.length > 0) ? await this._notesCardsInfo(noteIds) : [];

const info = {
canAdd: valid,
Expand All @@ -628,6 +628,27 @@ export class Backend {
return results;
}

/**
* @param {number[]} noteIds
* @returns {Promise<(?import('anki').NoteInfo)[]>}
*/
async _notesCardsInfo(noteIds) {
const notesInfo = await this._anki.notesInfo(noteIds);
/** @type {number[]} */
// @ts-expect-error - ts is not smart enough to realize that filtering !!x removes null and undefined
const cardIds = notesInfo.flatMap((x) => x?.cards).filter((x) => !!x);
const cardsInfo = await this._anki.cardsInfo(cardIds);
for (let i = 0; i < notesInfo.length; i++) {
if (notesInfo[i] !== null) {
const cardInfo = cardsInfo.find((x) => x?.noteId === notesInfo[i]?.noteId);
if (cardInfo) {
notesInfo[i]?.cardsInfo.push(cardInfo);
}
}
}
return notesInfo;
}

/** @type {import('api').ApiHandler<'injectAnkiNoteMedia'>} */
async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}) {
return await this._injectAnkNoteMedia(
Expand Down
51 changes: 51 additions & 0 deletions ext/js/comm/anki-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,17 @@ export class AnkiConnect {
return this._normalizeNoteInfoArray(result);
}

/**
* @param {import('anki').CardId[]} cardIds
* @returns {Promise<(?import('anki').CardInfo)[]>}
*/
async cardsInfo(cardIds) {
if (!this._enabled) { return []; }
await this._checkVersion();
const result = await this._invoke('cardsInfo', {cards: cardIds});
return this._normalizeCardInfoArray(result);
}

/**
* @returns {Promise<string[]>}
*/
Expand Down Expand Up @@ -655,6 +666,46 @@ export class AnkiConnect {
fields: fields2,
modelName,
cards: cards2,
cardsInfo: [],
};
result2.push(item2);
}
return result2;
}

/**
Kuuuube marked this conversation as resolved.
Show resolved Hide resolved
* Transforms raw AnkiConnect data into the CardInfo type.
* @param {unknown} result
* @returns {(?import('anki').CardInfo)[]}
* @throws {Error}
*/
_normalizeCardInfoArray(result) {
if (!Array.isArray(result)) {
throw this._createUnexpectedResultError('array', result, '');
}
/** @type {(?import('anki').CardInfo)[]} */
const result2 = [];
for (let i = 0, ii = result.length; i < ii; ++i) {
const item = /** @type {unknown} */ (result[i]);
if (item === null || typeof item !== 'object') {
throw this._createError(`Unexpected result type at index ${i}: expected Cards.CardInfo, received ${this._getTypeName(item)}`, result);
}
const {cardId} = /** @type {{[key: string]: unknown}} */ (item);
if (typeof cardId !== 'number') {
result2.push(null);
continue;
}
const {note, flags} = /** @type {{[key: string]: unknown}} */ (item);
if (typeof note !== 'number') {
result2.push(null);
continue;
}

/** @type {import('anki').CardInfo} */
const item2 = {
noteId: note,
cardId,
flags: typeof flags === 'number' ? flags : 0,
};
result2.push(item2);
}
Expand Down
12 changes: 12 additions & 0 deletions ext/js/data/options-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ export class OptionsUtil {
this._updateVersion51,
this._updateVersion52,
this._updateVersion53,
this._updateVersion54,
];
/* eslint-enable @typescript-eslint/unbound-method */
if (typeof targetVersion === 'number' && targetVersion < result.length) {
Expand Down Expand Up @@ -1509,6 +1510,17 @@ export class OptionsUtil {
}
}

/**
* - Renamed anki.displayTags to anki.displayTagsAndFlags
* @type {import('options-util').UpdateFunction}
*/
async _updateVersion54(options) {
for (const profile of options.profiles) {
profile.options.anki.displayTagsAndFlags = profile.options.anki.displayTags;
delete profile.options.anki.displayTags;
}
}

/**
* @param {string} url
* @returns {Promise<chrome.tabs.Tab>}
Expand Down
141 changes: 133 additions & 8 deletions ext/js/display/display-anki.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export class DisplayAnki {
this._errorNotificationEventListeners = null;
/** @type {?import('./display-notification.js').DisplayNotification} */
this._tagsNotification = null;
/** @type {?import('./display-notification.js').DisplayNotification} */
this._flagsNotification = null;
/** @type {?Promise<void>} */
this._updateSaveButtonsPromise = null;
/** @type {?import('core').TokenObject} */
Expand All @@ -69,8 +71,8 @@ export class DisplayAnki {
this._resultOutputMode = 'split';
/** @type {import('settings').GlossaryLayoutMode} */
this._glossaryLayoutMode = 'default';
/** @type {import('settings').AnkiDisplayTags} */
this._displayTags = 'never';
/** @type {import('settings').AnkiDisplayTagsAndFlags} */
this._displayTagsAndFlags = 'never';
/** @type {import('settings').AnkiDuplicateScope} */
this._duplicateScope = 'collection';
/** @type {boolean} */
Expand Down Expand Up @@ -103,6 +105,8 @@ export class DisplayAnki {
/** @type {(event: MouseEvent) => void} */
this._onShowTagsBind = this._onShowTags.bind(this);
/** @type {(event: MouseEvent) => void} */
this._onShowFlagsBind = this._onShowFlags.bind(this);
/** @type {(event: MouseEvent) => void} */
this._onNoteSaveBind = this._onNoteSave.bind(this);
/** @type {(event: MouseEvent) => void} */
this._onViewNotesButtonClickBind = this._onViewNotesButtonClick.bind(this);
Expand Down Expand Up @@ -206,7 +210,7 @@ export class DisplayAnki {
duplicateBehavior,
suspendNewCards,
checkForDuplicates,
displayTags,
displayTagsAndFlags,
kanji,
terms,
noteGuiMode,
Expand All @@ -221,7 +225,7 @@ export class DisplayAnki {
this._compactTags = compactTags;
this._resultOutputMode = resultOutputMode;
this._glossaryLayoutMode = glossaryLayoutMode;
this._displayTags = displayTags;
this._displayTagsAndFlags = displayTagsAndFlags;
this._duplicateScope = duplicateScope;
this._duplicateScopeCheckAllModels = duplicateScopeCheckAllModels;
this._duplicateBehavior = duplicateBehavior;
Expand Down Expand Up @@ -260,6 +264,9 @@ export class DisplayAnki {
for (const node of element.querySelectorAll('.action-button[data-action=view-tags]')) {
eventListeners.addEventListener(node, 'click', this._onShowTagsBind);
}
for (const node of element.querySelectorAll('.action-button[data-action=view-flags]')) {
eventListeners.addEventListener(node, 'click', this._onShowFlagsBind);
}
for (const node of element.querySelectorAll('.action-button[data-action=save-note]')) {
eventListeners.addEventListener(node, 'click', this._onNoteSaveBind);
}
Expand Down Expand Up @@ -304,6 +311,16 @@ export class DisplayAnki {
this._showTagsNotification(tags);
}

/**
* @param {MouseEvent} e
*/
_onShowFlags(e) {
e.preventDefault();
const element = /** @type {HTMLElement} */ (e.currentTarget);
const flags = element.title;
this._showFlagsNotification(flags);
}

/**
* @param {number} index
* @param {import('display-anki').CreateMode} mode
Expand All @@ -323,6 +340,15 @@ export class DisplayAnki {
return entry !== null ? entry.querySelector('.action-button[data-action=view-tags]') : null;
}

/**
* @param {number} index
* @returns {?HTMLButtonElement}
*/
_flagsIndicatorFind(index) {
const entry = this._getEntry(index);
return entry !== null ? entry.querySelector('.action-button[data-action=view-flags]') : null;
}

/**
* @param {number} index
* @returns {?HTMLElement}
Expand Down Expand Up @@ -429,7 +455,7 @@ export class DisplayAnki {
* @param {import('display-anki').DictionaryEntryDetails[]} dictionaryEntryDetails
*/
_updateSaveButtons(dictionaryEntryDetails) {
const displayTags = this._displayTags;
const displayTagsAndFlags = this._displayTagsAndFlags;
for (let i = 0, ii = dictionaryEntryDetails.length; i < ii; ++i) {
/** @type {?Set<number>} */
let allNoteIds = null;
Expand Down Expand Up @@ -457,8 +483,9 @@ export class DisplayAnki {
}
}

if (displayTags !== 'never' && Array.isArray(noteInfos)) {
if (displayTagsAndFlags !== 'never' && Array.isArray(noteInfos)) {
this._setupTagsIndicator(i, noteInfos);
this._setupFlagsIndicator(i, noteInfos);
}
}

Expand All @@ -483,7 +510,7 @@ export class DisplayAnki {
displayTags.add(tag);
}
}
if (this._displayTags === 'non-standard') {
if (this._displayTagsAndFlags === 'non-standard') {
for (const tag of this._noteTags) {
displayTags.delete(tag);
}
Expand All @@ -508,6 +535,104 @@ export class DisplayAnki {
this._tagsNotification.open();
}

/**
* @param {number} i
* @param {(?import('anki').NoteInfo)[]} noteInfos
*/
_setupFlagsIndicator(i, noteInfos) {
const flagsIndicator = this._flagsIndicatorFind(i);
if (flagsIndicator === null) {
return;
}

/** @type {Set<string>} */
const displayFlags = new Set();
for (const item of noteInfos) {
if (item === null) { continue; }
for (const cardInfo of item.cardsInfo) {
if (cardInfo.flags !== 0) {
displayFlags.add(this._getFlagName(cardInfo.flags));
}
}
}

if (displayFlags.size > 0) {
flagsIndicator.disabled = false;
flagsIndicator.hidden = false;
flagsIndicator.title = `Card flags: ${[...displayFlags].join(', ')}`;
/** @type {HTMLElement | null} */
const flagsIndicatorIcon = flagsIndicator.querySelector('.action-icon');
if (flagsIndicatorIcon !== null && flagsIndicator instanceof HTMLElement) {
flagsIndicatorIcon.style.background = this._getFlagColor(displayFlags);
}
}
}

/**
* @param {number} flag
* @returns {string}
*/
_getFlagName(flag) {
/** @type {Record<number, string>} */
const flagNamesDict = {
1: 'Red',
2: 'Orange',
3: 'Green',
4: 'Blue',
5: 'Pink',
6: 'Turquoise',
7: 'Purple',
};
if (flag in flagNamesDict) {
return flagNamesDict[flag];
}
return '';
}

/**
* @param {Set<string>} flags
* @returns {string}
*/
_getFlagColor(flags) {
/** @type {Record<string, import('display-anki').RGB>} */
const flagColorsDict = {
Red: {red: 248, green: 113, blue: 113},
Orange: {red: 253, green: 186, blue: 116},
Green: {red: 134, green: 239, blue: 172},
Blue: {red: 96, green: 165, blue: 250},
Pink: {red: 240, green: 171, blue: 252},
Turquoise: {red: 94, green: 234, blue: 212},
Purple: {red: 192, green: 132, blue: 252},
};

const gradientSliceSize = 100 / flags.size;
let currentGradientPercent = 0;

const gradientSlices = [];
for (const flag of flags) {
const flagColor = flagColorsDict[flag];
gradientSlices.push(
'rgb(' + flagColor.red + ',' + flagColor.green + ',' + flagColor.blue + ') ' + currentGradientPercent + '%',
'rgb(' + flagColor.red + ',' + flagColor.green + ',' + flagColor.blue + ') ' + (currentGradientPercent + gradientSliceSize) + '%',
);
currentGradientPercent += gradientSliceSize;
}

return 'linear-gradient(to right,' + gradientSlices.join(',') + ')';
}

/**
* @param {string} message
*/
_showFlagsNotification(message) {
if (this._flagsNotification === null) {
this._flagsNotification = this._display.createNotification(true);
}

this._flagsNotification.setContent(message);
this._flagsNotification.open();
}

/**
* @param {import('display-anki').CreateMode} mode
*/
Expand Down Expand Up @@ -733,7 +858,7 @@ export class DisplayAnki {
* @returns {Promise<import('display-anki').DictionaryEntryDetails[]>}
*/
async _getDictionaryEntryDetails(dictionaryEntries) {
const fetchAdditionalInfo = (this._displayTags !== 'never');
const fetchAdditionalInfo = (this._displayTagsAndFlags !== 'never');

const notePromises = [];
const noteTargets = [];
Expand Down
Loading