diff options
| author | Tyler <tyler@tylerhoang.xyz> | 2026-05-17 00:48:26 -0700 |
|---|---|---|
| committer | Tyler <tyler@tylerhoang.xyz> | 2026-05-17 00:48:26 -0700 |
| commit | e885be599095037f93209d42ba55c77c2fb6b6ee (patch) | |
| tree | 5773174f4b4e11ad3cc03ec6fda4cac47638ffeb /components/persistence.py | |
| parent | 2ea1abbe88b2b4acb1c58a74022fcec42d02a4c2 (diff) | |
Add session persistence and watchlist exit button
Persists watchlist and active ticker across browser refreshes via a
localStorage bridge (components/persistence.py), and adds a sidebar
"← Watchlist" button to clear the active ticker and return to the
QuoteTable landing page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'components/persistence.py')
| -rw-r--r-- | components/persistence.py | 54 |
1 files changed, 54 insertions, 0 deletions
diff --git a/components/persistence.py b/components/persistence.py new file mode 100644 index 0000000..1dafdbe --- /dev/null +++ b/components/persistence.py @@ -0,0 +1,54 @@ +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 + st.rerun() + + st.text_input("persist_wl", key="_persist_wl", label_visibility="collapsed") + st.text_input("persist_tk", key="_persist_tk", label_visibility="collapsed") + + 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-wl='" + wl_json + "' data-tk='" + tk_val + "'></div>" + "<script>" + "(function() {" + " var d = document.getElementById('d');" + " localStorage.setItem('prism_watchlist', d.dataset.wl);" + " if (d.dataset.tk) localStorage.setItem('prism_ticker', d.dataset.tk);" + " else localStorage.removeItem('prism_ticker');" + " var stored_wl = localStorage.getItem('prism_watchlist');" + " var stored_tk = localStorage.getItem('prism_ticker') || '';" + " if (!stored_wl) return;" + " 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;" + " }" + " }" + " }" + " setInput('persist_wl', stored_wl);" + " if (stored_tk) setInput('persist_tk', stored_tk);" + "})();" + "</script>" + ) + components.html(html, height=0, scrolling=False) |
