Retrieving localstorage values in addon

I’m trying to create a slideshow-style image carousel that remembers the last slide the user navigated to.

I’ve been able to implement this really well within a continuous anki instance on desktop using localstorage paired with noteids. Unfortunately, with the dynamic port assignment on anki startup, localstorage does not persist after anki desktop restarts. I know of no way to persist data after restart without an add-on.

I’m developing an addon with the goal of saving specific localstorage key/value pairs as a static array in a .js file in the media folder of an anki instance. The benefit of this is that it will sync across devices, with ankihub, and the .js file can be called in the note template so that ankidroid/mobile can at least read the relevant key/value pairs into their localstorage. This should create a read-only way to specify the starting slide of the slideshow, with write capacity in desktops with the addon installed.

Unfortunately, I’ve run into a problem with my code in retrieving the localstorage values from python.

I started out with mw.web.eval(“Object.entries(localStorage)”) which returned no values in the debugger or the addon.

I then verified that the key/value pairs exist with this javascript added to my notetypes:

var text = '';
                for (var i = 0; i < localStorage.length; i++) {
                    var key = localStorage.key(i);
                var value = localStorage.getItem(key);
                text += key + ' : ' + value + '<br>';
                }

After confirming they exist, I attempted to just call this javascript through python:

mw.web.evalWithCallback("""
    var text = '';
    for (var i = 0; i < localStorage.length; i++) {
        var key = localStorage.key(i);
        var value = localStorage.getItem(key);
        text += key + ' : ' + value + '<br>';
    }
    text;
""", save_key_value_pairs)

I ran into some problems here, and the localstorage string was showing up as null again, so I ran the following in the debug console:

js = """
    var text = '';
    for (var i = 0; i < localStorage.length; i++) {
        var key = localStorage.key(i);
        var value = localStorage.getItem(key);
        text += key + ' : ' + value + '<br>';
    }
    text;
"""
resultvar = mw.web.eval(js)

print(resultvar)

Sure enough, still returning as “None”. I went back into the cards and the javascript is still there correctly returning the storage. Is this something to do with the webview I am loading from? I’m using these hooks:

addHook("profileLoaded", load_key_value_pairs)
addHook("unloadProfile", save_key_value_pairs)

What am I doing wrong/not understanding? I verified syntax with other similar addons, but still can’t figure it out.

Just to clarify:

mw.web.eval(js)

This will always evaluate to None as eval() is asynchronous and you need to use evalWithCallback() coupled with a callback as you did in the previous example to read the JS return back.

Your code works fine for me when used with evalWithCallback() in the console, e.g.:

from aqt import mw

def read_local_storage(_):
   from aqt import mw

   mw.web.evalWithCallback("""
      var text = '';
      for (var i = 0; i < localStorage.length; i++) {
          var key = localStorage.key(i);
          var value = localStorage.getItem(key);
          text += key + ' : ' + value + '<br>';
      }
      text;""", lambda js_return: print(js_return)
    )

# Execute read_local_storage as a callback to make sure it runs after local storage has been modified: 

mw.web.evalWithCallback("""localStorage.setItem("foo", "bar");""", read_local_storage)
1 Like

Thank you for your response! I’m a fan of your work.

Am I doing something wrong with the debug console? I’m still getting

I put a little more work into the overall code, which also does not work still, due to the empty js_return output. Am I doing something that is just obviously wrong?

import os
import json
from aqt import mw
from anki.hooks import addHook
from aqt.utils import showInfo

# List of strings to filter the keys
KEY_FILTER_LIST = {
    'group0': ['bnb', 'filter2']
}


def retrieve_local_storage():
    try:
        mw.AnkiWebView.evalWithCallback("""
            var text = 'var key_value_pairs = [\n';
            for (var i = 0; i < localStorage.length; i++) {
                var key = localStorage.key(i);
                var value = localStorage.getItem(key);
                text += '    ["' + key + '", "' + value + '"],\n';
            }
            text = text.slice(0, -2);
            text += '\n];';
        """, save_key_value_pairs)

    except Exception as e:
        showInfo("Error occurred during loading: " + str(e))

def save_key_value_pairs(js_return):
    try:
        key_value_pairs = js_return
        showInfo("key value pairs list: " + key_value_pairs)

        if not key_value_pairs:
            showInfo("localStorage is empty.")
            return

        # Save the array string to a JavaScript file in the media folder
        media_folder = mw.col.media.dir()
        file_path = os.path.join(media_folder, "__persistentlocalstorage.js")
        file_path_str = str(file_path)
        showInfo("File path: " + file_path_str)
        with open(file_path, "w", encoding="utf-8") as file:
            file.write(key_value_pairs)

    except Exception as e:
        showInfo("Error occurred during saving: " + str(e))

The callback in read_local_storage prints to stdout, so you should see the output in your terminal if you launch Anki from it.

Try replacing

mw.AnkiWebView.evalWithCallback

with

mw.web.evalWithCallback

Also: In your JS code, you’ll need to return text, either by moving your code into a function and using an explicit return or just writing text as a statement as you did in the example before.

I’ve simplified the code down and tried several things to get it to return a value with no success.

import os
import json
import traceback
from aqt import mw, gui_hooks
from anki.hooks import addHook
from aqt.utils import showInfo, showWarning

def retrieve_local_storage():
    try:
        inclusion_list_str = json.dumps(inclusion_list)
        showInfo("got to point A")

        js_code = """
            var text = '';
            for (var i = 0; i < localStorage.length; i++) {
                var key = localStorage.key(i);
                var value = localStorage.getItem(key);
                text += key ;
            }
            text = unescape(text);
            return text;
        """

        mw.web.evalWithCallback(js_code, save_key_value_pairs)

    except Exception as e:
        showInfo("Error occurred during loading: " + str(e) + "\n" + traceback.format_exc())

def save_key_value_pairs(js_return: str):
    try:

        showInfo("got to point B")
        if not js_return:
            showInfo("js_return is empty.")
            return

        if not key_value_pairs:
            showInfo("localStorage is empty.")
            return

        # Save the array string to a JavaScript file in the media folder
        media_folder = mw.col.media.dir()
        file_path = os.path.join(media_folder, "__persistentlocalstorage.js")
        file_path_str = str(file_path)
        showInfo("File path: " + file_path_str)
        with open(file_path, "w", encoding="utf-8") as file:
            file.write(key_value_pairs)

    except Exception as e:
        # showinfo("Error occurred during saving: " + str(e))
        showinfo("Error occurred during saving: " + str(e) + "\n" + traceback.format_exc())

So far I’ve tried adding unescape (because I saw it in another addon), simplifying, etc, but all debug information is telling me that js_return is empty. I also tried removing my test code from the cards back template and updating my hooks to:

gui_hooks.profile_did_open.append(load_key_value_pairs)
gui_hooks.reviewer_will_end.append(retrieve_local_storage)

This is really tough. Hard to find documentation on this kind of thing. I’ve even resorted to trying to search all github code for relevant working examples.

You will have to wrap your JS code in a function or IIFE to return from it, otherwise you’ll get an Illegal return statement error and, due to that, a null return on the Python side.

(to see these errors, I would highly recommend either executing Anki from a terminal and looking at its output, or – for JS errors – debugging via AnkiWebView Inspector or Chrome remote debugging)

Thank you for your help! Very simple addon made my studying a whole lot easier!

https://ankiweb.net/shared/info/92595818

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.