aboutsummaryrefslogtreecommitdiff
path: root/components/persistence.py
blob: 552347da54360c2deb9b6390ef3c70d1929239aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import json
import streamlit as st
import streamlit.components.v1 as components
from utils.security import escape_html


def render_persistence_bridge() -> None:
    """Sync watchlist + ticker to/from localStorage."""
    restored_wl = st.session_state.get("_persist_wl", "")
    restored_tk = st.session_state.get("_persist_tk", "")
    if restored_wl and not st.session_state.get("_persist_loaded"):
        try:
            st.session_state["watchlist"] = json.loads(restored_wl)
        except Exception:
            pass
        if restored_tk:
            st.session_state["ticker"] = restored_tk
        st.session_state["_persist_loaded"] = True
        # No st.rerun() — the bridge runs at the top of the render cycle so the
        # restored values propagate naturally to components below. Calling
        # st.rerun() here would abort the render before click-receiver inputs
        # (qt_click_receiver, wl_click_receiver) are registered, causing
        # Streamlit to clear their pending values and swallow the click.

    st.text_input("persist_wl", key="_persist_wl", label_visibility="collapsed")
    st.text_input("persist_tk", key="_persist_tk", label_visibility="collapsed")

    loaded = "1" if st.session_state.get("_persist_loaded") else "0"
    wl_json = escape_html(json.dumps(st.session_state.get("watchlist", [])))
    tk_val  = escape_html(st.session_state.get("ticker") or "")

    html = (
        "<div id='d' data-loaded='" + loaded + "' data-wl='" + wl_json + "' data-tk='" + tk_val + "'></div>"
        "<script>"
        "(function() {"
        "  var d = document.getElementById('d');"
        "  function setInput(label, val) {"
        "    var inputs = window.parent.document.querySelectorAll('input[type=text]');"
        "    for (var i = 0; i < inputs.length; i++) {"
        "      if (inputs[i].getAttribute('aria-label') === label) {"
        "        var setter = Object.getOwnPropertyDescriptor(window.parent.HTMLInputElement.prototype, 'value').set;"
        "        setter.call(inputs[i], val);"
        "        inputs[i].dispatchEvent(new window.parent.Event('input', { bubbles: true }));"
        "        return;"
        "      }"
        "    }"
        "  }"
        "  if (d.dataset.loaded === '1') {"
        "    localStorage.setItem('prism_watchlist', d.dataset.wl);"
        "    if (d.dataset.tk) localStorage.setItem('prism_ticker', d.dataset.tk);"
        "    else localStorage.removeItem('prism_ticker');"
        "  } else {"
        "    var stored_wl = localStorage.getItem('prism_watchlist');"
        "    var stored_tk = localStorage.getItem('prism_ticker') || '';"
        "    if (!stored_wl) return;"
        "    setInput('persist_wl', stored_wl);"
        "    if (stored_tk) setInput('persist_tk', stored_tk);"
        "  }"
        "})();"
        "</script>"
    )
    components.html(html, height=0, scrolling=False)