I’ve been banging my head for a while at implementing some complex js for a card template and really needed the ability to debug why things worked on desktop but on AnkiDroid.
So, I implemented a custom consoleLog
function just for doing that. : I hope other people will find this helpful. I’ve tested it on AnkiDroid but not AnkiMobile.
Custom consoleLog function
Add this toin your card template to debug things in AnkiDroid.
<script>
// consoleLogMessages is an array of html strings which are the individual logs the custom console will show
// To prevent lag, the number of stored messages is limited to 100
globalThis.consoleLogMessages = globalThis.consoleLogMessages || [];
// consoleLogMessageCount is only used for a pretty counter of the log
// It's necessary due to the limiting of consoleLogMessages, otherwise
// the index arg in .map() could be used.
globalThis.consoleLogMessageCount = globalThis.consoleLogMessageCount || 0;
// Update console content with current consoleLogMessages
// This only needs to be done when showing the console
function renderLogMessages() {
const consoleElement = document.getElementById('custom-console');
const startingMsgCount = Math.max(globalThis.consoleLogMessageCount - globalThis.consoleLogMessages.length, 0);
// Limit stored messages to the last 100 to limit lag
globalThis.consoleLogMessages = (globalThis.consoleLogMessages || []).slice(-100);
consoleElement.innerHTML = ''
+ globalThis.consoleLogMessages.map((msg, i) => {
const msgNUm = startingMsgCount + i + 1;
const digitCount = parseInt(Math.log10(msgNUm));
let zeroes = "";
for (let j = 0; j < 4 - digitCount; j++) {
zeroes += "0";
}
return `<div style="margin-bottom: 2px; border-bottom: 1px solid black;"><span style="float: right; font-size: 0.7em; color: #858585;">${zeroes}${msgNUm}</span>${msg}</div>`
})
.join('');
// Auto-scroll to bottom
consoleElement.scrollTop = consoleElement.scrollHeight;
return consoleElement;
}
// Init function for inserting the two elements needed for the console into the card
// the console itself and a toggle for showing/hiding the console
function insertCustomLogElements() {
// Create custom console element
let customConsole = document.getElementById('custom-console');
if (!customConsole) {
customConsole = document.createElement('div');
customConsole.id = 'custom-console';
customConsole.style.cssText = 'position: absolute; top: 25px; right: 0; width:80%; max-height: 500px; overflow-y: auto; background-color: #333; color: #fff; font-size: 0.5em; font-weight: normal; border: 1px solid #ccc; padding: 10px; z-index: 1000; display: none; text-align: left;';
customConsole.innerHTML = (globalThis.consoleLogMessages || []).join('');
document.body.insertBefore(customConsole, document.body.firstChild);
}
// Create toggle button for console
let toggleConsoleBtn = document.getElementById('toggle-console-btn');
if (!toggleConsoleBtn) {
toggleConsoleBtn = document.createElement('button');
toggleConsoleBtn.id = 'toggle-console-btn';
toggleConsoleBtn.style.cssText = 'position: absolute; top: 0; right: 0; background-color: #007bff; color: #fff; border: none; cursor: pointer; z-index: 1001;margin: 0;';
if (globalThis.ankiPlatform === "desktop") {
toggleConsoleBtn.style.cssText += " width: 15px; height: 15px; padding: 0px 0px 2px 0px;";
} else {
toggleConsoleBtn.style.cssText += " width: 25px; height: 25px; padding: 4px 4px 6px 4px;";
}
toggleConsoleBtn.innerHTML = '▼'; // Downward arrow by default
// Toggle visibility of the custom console
toggleConsoleBtn.addEventListener('click', function toggleConsoleVisiblity() {
const customConsoleCur = document.getElementById('custom-console');
customConsoleCur.innerHTML = (globalThis.consoleLogMessages || []).join(''); // Update console with all logged messages
customConsoleCur.scrollTop = customConsoleCur.scrollHeight; // Auto-scroll to bottom
const toggleBtn = document.getElementById('toggle-console-btn');
if (customConsoleCur.style.display === 'none') {
customConsoleCur.style.display = 'block';
toggleBtn.innerHTML = '▲'; // Upward arrow when expanded
// Console will be shown, so render the messages
renderLogMessages();
} else {
customConsoleCur.style.display = 'none';
toggleBtn.innerHTML = '▼'; // Downward arrow when collapsed
}
})
document.body.insertBefore(toggleConsoleBtn, document.body.firstChild);
}
}
// Function to log messages to the custom console and store them in consoleLogMessages
function consoleLog(...args) {
// Also log to normal console, when testing changes to the console code on desktop
// console.log(...args);
globalThis.consoleLogMessageCount = globalThis.consoleLogMessageCount || 0;
globalThis.consoleLogMessageCount++;
var logHtml = '';
args.forEach((arg, i) => {
// Wrap first arg in span so that it's on the same line as the msg counter
const wrapper = i === 0 ? 'span' : 'div';
if (arg instanceof Error) {
// Render stack trace for Error objects in red
logHtml += `<${wrapper} style="color: #FF6B6B;">${arg.toString()}</${wrapper}>`;
if (arg.stack) {
logHtml += `<${wrapper} style="color: #FF6B6B;">${arg.stack.replace(/\n/g, '<br>')}</${wrapper}>`;
}
} else if ((typeof arg === 'object' && arg !== null) || typeof arg === 'function') {
// Render objects and functions in light blue
logHtml += `<${wrapper} style="color: #7FDBFF;">${stringifyLimited(arg)}</${wrapper}>`;
} else if (typeof arg === 'string') {
// Render other types of data as plain text in white
logHtml += `<${wrapper} style="color: #FFFFFF;">${arg}</${wrapper}>`;
} else {
// null, undefined, numbers in gray
logHtml += `<${wrapper} style="color: #AAAAAA;">${arg}</${wrapper}>`;
}
});
// Using straight-up JSON.stringify on objects and arrays created logs that were
// way too long. The normal browser limits rendering objects and arrays to just
// the first level so this does the same. You can't expand the stuff like in a normal
// console though
function stringifyLimited(obj, depth = 1) {
let res = '<span>'
if (depth < 2) {
if (Array.isArray(obj)) {
res += '['
obj.forEach((el, i) => {
res += `<div>${[i]}: ${stringifyLimited(el, depth + 1)}</div>`
})
res += ']';
} else if (typeof obj === 'object' && obj !== null) {
res += '{'
Object.entries(obj).forEach(([key, val]) => {
res += `<div>${[key]}: ${stringifyLimited(val, depth + 1)}</div>`
})
res += '}';
} else if (typeof obj === 'function') {
res += `ƒ <i>${obj.name}()<i>`;
} else {
res += JSON.stringify(obj);
}
} else {
if (Array.isArray(obj)) {
res += `Array[${obj.length}]`;
} else if (typeof obj === 'object' && obj !== null) {
res += `Object[${Object.keys(obj).length}]`;
} else if (typeof obj === 'function') {
res += `ƒ <i>${obj.name}()</i>`;
} else {
res += JSON.stringify(obj);
}
}
res += '</span>';
return res;
}
// Just in case, ensure consoleLogMessages is initialized here too
globalThis.consoleLogMessages = globalThis.consoleLogMessages || [];
globalThis.consoleLogMessages.push(logHtml); // Store the log message as HTML string
// If the console is open while a log is added, we need to render it
const customConsoleCur = document.getElementById('custom-console');
if (customConsoleCur.style.display === 'block') {
renderLogMessages();
}
};
</script>
<script>
if (globalThis.ankiPlatform === "desktop") {
// Replace the custom consoleLog with normal console.log on desktop
// Regardless of whether we're on desktop or mobile, we will call consoleLog and not console.log
consoleLog = console.log;
} else {
// If not on desktop, insert the custom console log elements so we can use it
insertCustomLogElements();
}
consoleLog("ankiPlatform", globalThis.ankiPlatform);
consoleLog("AnkiDroidJS", globalThis.AnkiDroidJS);
var UA = navigator.userAgent;
var isMobile = /Android/i.test(UA);
var isAndroidWebview = /wv/i.test(UA);
consoleLog("UA", UA)
consoleLog("isMobile", isMobile)
consoleLog("isAndroidWebview", isAndroidWebview);
</script>
Cross-platform script loader with error logging
Here’s how I’m using the consoleLog
function to debug imports on AnkiDroid using my version of the cross-platform custom script loader by kleinerpirat:
<script>
// Wrap the whole thing in a try-catch block to debug issues with the importer itself
try {
function getAnkiPrefix() {
return globalThis.ankiPlatform === "desktop"
? ""
: globalThis.AnkiDroidJS
? "https://appassets.androidplatform.net"
: ".";
}
var cardScripts = [
{
name: "Card info",
url: "_cardInfo.js",
init: (moduleObj) => {
globalThis.addCardInfo = moduleObj.addCardInfo;
globalThis.addCardInfo();
}
},
];
// init first time ExternalScriptsLoaded
if (!globalThis.ExternalScriptsLoaded) {
// We'll keep track of the load status of each individual script so that
// upon any script finishing loading, we can check if everything's done and
// run the functions that should wait until everything's loaded
globalThis.ExternalScriptsLoaded = cardScripts.reduce((dict, curScript) => {
dict[curScript.name] = false;
return dict;
}, {})
}
// Functions to run after all scripts are loaded
var onScriptsLoadedFuncs = [
() => {
// The cardInfo adding is done in the script's init function
// on desktop the script is only loaded once, so we need to also have this
// so that the cardInfo is loaded for new reviews as well
if (globalThis.addCardInfo) globalThis.addCardInfo();
},
];
function onScriptsLoaded() {
for (func of onScriptsLoadedFuncs) {
if (typeof func === 'function') func();
}
}
var allScriptsLoaded = () => Object.values(globalThis.ExternalScriptsLoaded).every(isLoaded => isLoaded);
// guard to prevent further imports on Desktop & AnkiMobile
if (!allScriptsLoaded()) {
for (script of cardScripts) {
loadScript(script);
}
} else {
onScriptsLoaded();
}
async function loadScript(script) {
const scriptPromise = import(`${getAnkiPrefix()}/${script.url}`);
consoleLog(`Loading ${script.name}.`);
scriptPromise
.then(
(moduleObj) => {
consoleLog(`Loaded ${script.name}.`);
// If the script exports a myCustomExport object, it'll be available in moduleObject.myCustomExport,
// which the init script can then set to globalThis to store it persistently between reviews
// This only works in desktop though, AnkiDroid resets the globalThis object
// between front and back and reviews
if (script.init) script.init(moduleObj);
// Set this script as loaded
globalThis.ExternalScriptsLoaded[script.name] = true;
// If all scripts have finished loading, run final setup
// Since loading is async, we don't know when we'll get to this,
// thus it's checked on every script load
if (allScriptsLoaded()) {
onScriptsLoaded();
}
},
(error) =>
consoleLog(`An error occured while loading ${script.name}:`, error)
)
.catch((error) =>
consoleLog(`An error occured while executing ${script.name}:`, error)
);
if (globalThis.onUpdateHook) {
onUpdateHook.push(() => scriptPromise);
}
}
} catch (e) {
// An error in the importer code itself happened
consoleLog(e);
}
</script>
Card Info file
And here is the _cardInfo.js
imported above. I use this in multiple templates so it’s useful to have as a separate file like this.
export function addCardInfo() {
const UA = navigator.userAgent;
const isMobile = /Android/i.test(UA);
const isAndroidWebview = /wv/i.test(UA);
console.log(UA, isMobile, isAndroidWebview);
// Create a div.card-info element that will contain the card info
let cardInfo = document.getElementById("card-info");
if (!cardInfo) {
cardInfo = document.createElement("div");
cardInfo.id = "card-info";
cardInfo.style.fontFamily = "arial, serif";
cardInfo.style.fontWeight = "normal";
cardInfo.style.fontSize = "1rem";
cardInfo.style.color = "gray";
// Append as the first child of the body
document.body.insertBefore(cardInfo, document.body.firstChild);
}
const setInfoInSpan = (id, text) => {
// Append span with id="id" into the card-info div
// Does it already exist?
let span = document.getElementById(id);
if (!span) {
span = document.createElement("span");
span.id = id;
// Add margin
span.style.marginRight = "1.5rem";
cardInfo.appendChild(span);
}
span.innerHTML = text;
}
const setReps = (repsNum = 0) => {
setInfoInSpan("reps", "reps: " + repsNum);
};
const setFactor = (fctNum = 0) => {
setInfoInSpan("ease", "ease: " + fctNum / 10 + "%");
};
const setIvl = (ivlNum = 0) => {
setInfoInSpan("ivl", "ivl: " + ivlNum + "d");
};
if (globalThis.AnkiDroidJS) {
const jsApi = { version: "0.0.1", developer: "https://github.com/<your_github_username_here>" };
const apiStatus = AnkiDroidJS.init(JSON.stringify(jsApi));
setReps(AnkiDroidJS.ankiGetCardReps());
setFactor(AnkiDroidJS.ankiGetCardFactor());
setIvl(AnkiDroidJS.ankiGetCardInterval());
} else if (globalThis.ankiPlatform === 'desktop') {
// On desktop, we'll use AnkiDroid API equivalent functions added by this addon:
// https://ankiweb.net/shared/info/1490471827
pycmd("AnkiJS.ankiGetCardReps()", setReps);
pycmd("AnkiJS.ankiGetCardFactor()", setFactor);
pycmd("AnkiJS.ankiGetCardInterval()", setIvl);
}
}