Yes
Kind regards,
Ali
Yes
Kind regards,
Ali
I got similar reports about macOS. I donât have a Mac to test on so the issue will take a while to get fixed.
This Mac freeze problem seems to be caused by an incomplete modal âopen()â.
The add-ons manager is modal, and config of more flags is modal but it does not seem to be in front of the screen and is blocked by the add-ons manager, but the add-ons manager is also blocked and cannot be operated, so users cannot operate Anki. (I donât know why)
So using âexecâ instead should solve the problem. (or âexec_â in Qt5, or open config from tools instead of addons manager.)
# __init__.py, line 372
def on_config() -> None:
dialog = ConfigDialog(mw)
# dialog.open()
if hasattr(dialog, "exec"):
dialog.exec()
else:
dialog.exec_()
Edit 2024-12-07: I received a reply from a Mac user who read this thread and tried it and said that it opens the window but does not save the data, but on my device it seems to work fine so I am not sure about this. So maybe something still needs to be adjusted.
Started getting a crash after the Mac fix 0.0.7:
Anki 24.11 (87ccd24e) (ao)
Python 3.9.18 Qt 6.6.2 PyQt 6.6.1
Platform: Windows-10-10.0.26100
When loading More Flags - Add card flags with custom names and colors:
Traceback (most recent call last):
File "aqt.addons", line 250, in loadAddons
File "C:\Users\jormk\AppData\Roaming\Anki2\addons21\146757953\__init__.py", line 39, in <module>
from .gui.config import ConfigDialog
File "C:\Users\jormk\AppData\Roaming\Anki2\addons21\146757953\gui\config.py", line 3, in <module>
import webcolors
ModuleNotFoundError: No module named 'webcolors'
From what I can see, this import was already there since 0.0.3 so Itâs weird that itâs crashing for me now.
Should be fixed now. Thank you for the report.
Hi @abdo ,
Recently found that âMore Flagsâ addon attaches its Sentry handler to loggers of other addons.
Sometimes it leads to errors:
â Logging error â
Traceback (most recent call last):
File âloggingâ, line 1086, in emit
UnicodeEncodeError: âasciiâ codec canât encode character â\u2068â in position 192: ordinal not in range(128)
Call stack:
File ââ, line 1, in
File âaqtâ, line 571, in run
File âaqtâ, line 785, in _run
File âaqt.taskmanâ, line 144, in _on_closures_pending
File âaqt.taskmanâ, line 88, in
File âaqt.operationsâ, line 261, in wrapped_done
File âaqt.deckbrowserâ, line 171, in success
File âaqt.deckbrowserâ, line 187, in __renderPage
File â_aqt.hooksâ, line 1837, in call
File â/Users/SuhasK/Library/Application Support/Anki2/addons21/1188705668/ui/deck_browser/deck_browser_hooks.pyâ, line 43, in __on_browser_will_render_content
log.debug(f"DeckBrowserContent stats (edited): {content.stats}â)
File âloggingâ, line 1434, in debug
File âloggingâ, line 1589, in _log
File âloggingâ, line 1599, in handle
File â/Users/SuhasK/Library/Application Support/Anki2/addons21/146757953/vendor/sentry_sdk/integrations/logging.pyâ, line 96, in sentry_patched_callhandlers
return old_callhandlers(self, record)
File âloggingâ, line 1661, in callHandlers
File âloggingâ, line 952, in handle
File âloggingâ, line 1187, in emit
File âloggingâ, line 1091, in emit
Message: âDeckBrowserContent stats (edited):
Studied \u2068106\u2069 cards\n\u2068in \u20685.55\u2069 minutes\u2069 today\n(\u20683.14\u2069s/card)
What do you think is the best way to handle this issue?
CC @SuhasK
Hereâs some JS code for anyone interested in enabling the use of your More Flags on AnkiDroid (maybe it works on AnkiMobile too?)
The general idea of setting up an awaited Promise in the custom scheduler that will edit customData
with the values it receives on being resolved can be used to add any customData
values dynamically. I already set up the custom scheduler part of the code to receive an arbitrary dictionary that it will then write into customData
. The bigger part of the equation is adding the functionality (and likely UI) on the card template side to add what data values you want. In this case the only things added is one "cf" = 1/2/3...
value.
var isNightMode = document.body.classList.contains("nightMode");
var mainContainer = document.getElementById(
"custom_flag_container"
);
if (!mainContainer) {
mainContainer = document.createElement("div");
mainContainer.id = "custom_flag_container";
mainContainer.className = "inactive";
const trigger = document.createElement("button");
trigger.id = "custom_flag_trigger";
trigger.innerText = "";
mainContainer.appendChild(trigger);
// Add directly to body, this causes the container to persist between cards on desktop
document.body.appendChild(mainContainer);
mainContainer.onclick = (event) => {
event.stopPropagation();
};
trigger.onclick = function (event) {
switch (mainContainer.className) {
case "inactive":
mainContainer.className = "active";
break;
case "active":
mainContainer.className = "inactive";
break;
}
document.onclick = () => { mainContainer.className = "inactive"; };
}
}
if (!globalThis.Persistence) {
(async () => {
await import("./_ankiPersistence.js");
})();
}
// Assuming card template is including an element with is-back attribute on the backside
var isFront = !!document.querySelector("#is-front");
globalThis.selectedFlag = globalThis.selectedFlag || null;
if (globalThis.Persistence.isAvailable()) {
const storedFlag = globalThis.Persistence.getItem("selectedFlag");
if (storedFlag) {
globalThis.selectedFlag = storedFlag;
if (!isFront) {
globalThis.Persistence.removeItem("selectedFlag");
}
}
};
// Setup the current card flag setter
var flagSetter = document.getElementById("custom_flag_setter");
// Remove the previous flag setter if it exists
if (flagSetter) {
flagSetter.remove();
}
flagSetter = document.createElement("div");
flagSetter.id = "custom_flag_setter";
mainContainer.appendChild(flagSetter);
// Reset trigger on changing cards or keep same between front and back
var useStoredState = !isFront && globalThis.selectedFlag;
var trigger = document.getElementById("custom_flag_trigger");
trigger.style.color = useStoredState ? globalThis.selectedFlag.color : "inherit";
trigger.innerHTML = useStoredState ? "<span style='color: green;'>â </span>" : "";
function makeFlagRadioButton({
flagNum,
label,
color_light = '#fff',
color_dark = '#000',
} = {}) {
const flagRadioDiv = document.createElement("div");
const flagRadio = document.createElement("input");
flagRadio.type = "radio";
flagRadio.id = `flag${flagNum}`;
flagRadio.name = `flag${flagNum}`;
flagRadio.value = flagNum;
const color = isNightMode ? color_light : color_dark;
const flagSymbol = flagNum > 0 ? 'â' : 'â';
const flagLabel = document.createElement("label");
flagLabel.htmlFor = `flag${flagNum}`;
flagLabel.innerHTML = `<span style="color: ${color};">${flagSymbol} ${label}</span>`;
flagRadio.addEventListener("change", (e) => {
const flag = e.target.value;
const isChecked = e.target.checked;
if (isChecked) {
// Set zero to null, null is used to indicate removal of a key from customData
globalThis.selectedFlag = flag === "0" ? null : { flag, color };
// Uncheck all other radios
document.querySelectorAll("input[type=radio]").forEach((radio) => {
if (radio !== e.target) {
radio.checked = false;
}
});
// Enable save button
document.getElementById("custom_flag_save_button").disabled = false;
}
});
flagRadioDiv.appendChild(flagRadio);
flagRadioDiv.appendChild(flagLabel);
return flagRadioDiv
};
/**
* Add custom flag setter to the container.
*
* @param {any} [flagContainer=flagSetter] Optional custom flag container to be provided.
* The container also have the id "custom_flag_setter" so that css can be applied to it.
*/
function addCustomFlagSetter(flagContainer = flagSetter) {
// Load More flags config json generated by Addon Config Sync
const configJsonPromise = fetch("./_146757953_meta.json").then(response => response.json());
// flags are an array in config.flags in the json, call makeFlagButton for each
configJsonPromise.then((module) => {
const flagsDef = module?.config?.flags;
if (!Array.isArray(flagsDef) || flagsDef.length === 0) {
return;
}
// Clear the flag container from any previous flags
flagContainer.innerHTML = "";
// Add a "No Flag" option first
flagsDef.unshift({ label: "No Flag", flagNum: 0 });
flagsDef.forEach((flagDef, i) => {
// Note, we turn flag 0 into null
const flagNum = i;
const { label, color_light, color_dark, flag } = flagDef;
const flagRadioDiv = makeFlagRadioButton({ flagNum, label, color_light, color_dark, flag });
flagContainer.appendChild(flagRadioDiv);
});
// Add save button, this will resolve the promise with the selected flag and can only be set
// once oer card front
const saveButtonRow = document.createElement("div");
const msgSpan = document.createElement("span");
msgSpan.id = "custom_flag_save_message";
msgSpan.innerText = "Click Save to finalize selection to customData.";
saveButtonRow.appendChild(msgSpan);
saveButtonRow.id = "custom_flag_save_button_row";
const saveButton = document.createElement("button");
// Should be disabled until a flag is selected
saveButton.disabled = true;
saveButton.id = "custom_flag_save_button";
saveButton.innerText = "Save";
saveButton.onclick = function () {
if (globalThis.selectedFlag) {
globalThis.Persistence.setItem("selectedFlag", globalThis.selectedFlag);
}
const { flag, color } = globalThis.selectedFlag || {};
globalThis.resolveGetCustomFlagsPromise({ "cf": flag });
// Disable all radios, since changing the value needs a reload
document.querySelectorAll("input[type=radio]").forEach((radio) => {
radio.disabled = true;
});
const trigger = document.getElementById("custom_flag_trigger");
trigger.style.color = color;
trigger.innerHTML = "<span style='color: green;'>â </span>";
// Add message on how to reset selection
msgSpan.innerText = "Selection saved, to reset you need to reload the card.";
};
saveButtonRow.appendChild(saveButton);
flagContainer.appendChild(saveButtonRow);
const UA = navigator.userAgent;
const isAndroidWebview = /wv/i.test(UA);
// Add a stickied button a the bottom of the screen that will also resolve the promise
// On AnkiDroid, the awaiting promise blocks advancing to the card back so this hack
// is needed to allow advancing without setting a flag
if (isAndroidWebview) {
// Change message if no selection made
if (!globalThis.selectedFlag) {
msgSpan.innerText = "Saving on card back not possible on AnkiDroid";
document.querySelectorAll("input[type=radio]").forEach((radio) => {
radio.disabled = true;
});
saveButton.disabled = true;
}
const fixedResolveButton = document.createElement("div");
fixedResolveButton.id = "android_fixed_resolve_button";
// Trigger on touchstart to make triggering with one touch on the edge of the
// Show answer button easier. It's still rather precise to try to trigger
// both with one touch
fixedResolveButton.ontouchstart = function () {
globalThis.resolveGetCustomFlagsPromise();
// Hide the button after resolving as it is not needed anymore
fixedResolveButton.style = "display: none;";
};
document.body.appendChild(fixedResolveButton);
document.body.style.height = "100vw";
}
});
}
addCustomFlagSetter();
In order to get your flag settings, Iâve taken advantage of my own Addon Config Sync which creates a copy of every addonâs config file in the collection.media folder. This enables importing that JSON file in Javascript and thus reading the flag settings youâve made on AnkiDroid!
If you donât want to use the addon youâll need to replace this part of the code with a manual copy of your flag settings. Which youâd need to also manually update whenever they change, so using the addon is very convenient.
// Load More flags config json generated by Addon Config Sync
const configJsonPromise = fetch("./_146757953_meta.json").then(response => response.json());
// flags are an array in config.flags in the json, call makeFlagButton for each
configJsonPromise.then((module) => {
const flagsDef = module?.config?.flags;
if (!Array.isArray(flagsDef) || flagsDef.length === 0) {
return;
}
#custom_flag_container {
position: absolute;
font-family: Arial, Helvetica, sans-serif;
top: 0;
right: 0;
font-size: 0.9em;
opacity: 0.7;
z-index: 1000;
pointer-events: all;
}
#custom_flag_trigger {
font-size: 0.9em;
padding: 0.2em 0.6em;
margin: 0;
border-radius: 0.3em;
border: 1px solid transparent;
cursor: pointer;
z-index: 1000;
float: right;
pointer-events: all;
}
#custom_flag_trigger:hover {
border-color: gray;
}
#custom_flag_setter {
position: relative;
top: 1.8em;
right: 0;
display: none;
padding: 0.5em;
text-align: left;
line-height: 1em;
border-radius: 0.3em;
border: 1px solid darkgray;
background-color: #fff;
}
.nightMode #custom_flag_setter {
background-color: #333;
}
#custom_flag_save_button_row {
width: 180px;
display: flex;
flex-flow: row;
align-items: flex-end;
}
#custom_flag_save_button {
margin: 0;
margin-top: 0.5em;
padding: 0.2em 0.6em;
border-radius: 0.3em;
border: 1px solid transparent;
}
#android_fixed_resolve_button {
position: fixed;
bottom: -10px;
margin: 0;
height: 70px;
width: 100%;
background-color: black;
opacity: 0.01;
z-index: 1001;
}
#custom_flag_save_message {
font-size: 0.8em;
display: inline;
word-break: keep-all;
}
#custom_flag_container.active #custom_flag_setter {
display: block;
}
#custom_flag_container.inactive #custom_flag_setter {
display: none;
}
#custom_flag_container.active #custom_flag_trigger:after {
content: "â â˛";
}
#custom_flag_container.inactive #custom_flag_trigger:after {
content: "â âź";
}
#custom_flag_container.active {
opacity: 1;
}
#custom_flag_container.inactive {
opacity: 0.7;
}
// Add card front identifier div
var isFront = document.querySelector("#is-front")
if (!isFront) {
isFront = document.createElement("div");
isFront.id = "is-front";
document.querySelector("#qa").appendChild(isFront);
}
/**
* Promise to set custom data when resolved. Can be resolved multiple times.
*/
globalThis.getCustomFlagsPromise = new Promise((resolve) => {
globalThis.resolveGetCustomFlagsPromise = resolve;
});
globalThis.getCustomFlagsPromise.then((newCustomData) => {
if (typeof newCustomData !== 'object' || newCustomData === null) {
return;
}
Object.entries(newCustomData).forEach(([key, value]) => {
['again', 'hard', 'good', 'easy'].forEach((type) => {
if (value === null) {
delete customData[type][key];
} else {
customData[type][key] = value;
}
});
});
});
// Assign to AnkiDroid user action button a function that will resolve the promise
// and show the answer as a slightly faster alternative to the hack of the fixed button
// on the bottom of the screen
globalThis.userJs1 = function () {
globalThis.resolveGetCustomFlagsPromise();
showAnswer();
};
// Await the promise to get the custom data, the scheduler hook will
// only write the custom data once when it is allowed to resolve
await globalThis.getCustomFlagsPromise;
console.log("customData", customData);
<script src="_customFlagSetter.js"></script>
. The JS can run as the very last thing.@import url("_customFlagSetter.css");
This creates a small button at the top right that you can click to open to select flags. The selected flag can be saved only once because once the scheduler hook has been allowed to finish, further edits to custom data will not get saved on answering the card. To undo selection you need to reload the card (exit and re-enter reviewer)
I made the UI use the popover style because I personally would need to flag things only occasionally.
Surprisingly, you can set the custom data on both the card front and back. Might be just an FSRS thing as, if I understand correctly, it ignores the interval values given by the custom scheduler? The extra convenience doesnât really matter much since you can use the addon on desktop to flag in the card browser.
The JS flagging might just a tiny bit faster? You still need a few clicks to tag. You could change the display to instead show flag buttons always inline thatâd require one click to flag (but still need a card reload to undo).
The main point, no addon on mobile, so the reviewer is the only way you can add custom data to a card through the custom scheduler. Thereâs more unfortunate drawbacks here:
userJs1
so you could bind some button on your phone to that to show the card back and resolve the promise in one action. Iâve bound mine to Volume Up.Hmm, does passing encoding="utf-8"
to your FileHandler help?
Oops, I missed that the reviewer does also get partially blocked on the card front on desktop too. I somehow did not look down when testing on desktop until today
This happens:
This is yet another fun way that the desktop reviewer JS handling differs from AnkiDroid.
When searching for cards which do not have cf=1 (i.e. a search like -prop:cdn:cf=1
) [any particula number, not necessarily 1], cards which do not have any flag are not shown. Is this intended behaviour?
When flagging cards, I am not able to Undo the flagging action with the shortcut Cmd + Z. Is it possible to add that?
Anki 24.11 (87ccd24e) (ao)
Python 3.9.18 Qt 6.6.2 PyQt 6.6.1
Platform: macOS-13.7.5-x86_64-i386-64bit
This is due to the way prop:cdn
is implemented in Anki. You can use the has-cd:cf
query to find cards w/o any custom flag.
It probably broke after some update. Will look into the issue as soon as possible.