From 13e8d41a80b47c065298064ee1e68b6446187305 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 1 Feb 2026 17:45:52 +0100 Subject: [PATCH] modified widgets --- glance-system/glance-helper.yaml | 487 ++++++++++++++++++++++++++++- glance-system/glance-kisfenyo.yaml | 13 +- 2 files changed, 497 insertions(+), 3 deletions(-) diff --git a/glance-system/glance-helper.yaml b/glance-system/glance-helper.yaml index 9b071d2..b716314 100644 --- a/glance-system/glance-helper.yaml +++ b/glance-system/glance-helper.yaml @@ -173,7 +173,7 @@ data: }}); if (response.ok) {{ lastSaved = content; - updateStatus('Saved ✓', 'saved'); + updateStatus('Saved ✓', 'saved'); setTimeout(() => updateStatus(''), 2000); }} else {{ updateStatus('Save failed', 'error'); @@ -209,6 +209,491 @@ data: return {"status": "ok", "user": safe_user} return Response(content="Failed to save", status_code=500) + # ================================ + # Unified User Data System (Notes, Todo, Motivation) + # ================================ + # File format: + # [Notes] + # Free-form notes text... + # + # [Todo] + # - [ ] Uncompleted task + # - [x] Completed task + # + # [Motivation] + # Quote 1 + # Quote 2 + + def get_userdata_file(user: str) -> str: + """Get user data file path, with validation.""" + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + data_dir = os.environ.get("DATA_DIR", "/data") + return os.path.join(data_dir, f"userdata_{safe_user}.txt") + + def load_userdata(user: str) -> dict: + """Load and parse user data file into sections.""" + filepath = get_userdata_file(user) + sections = {"Notes": "", "Todo": [], "Motivation": []} + try: + if os.path.exists(filepath): + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + current_section = None + section_lines = [] + for line in content.split('\n'): + stripped = line.strip() + if stripped.startswith('[') and stripped.endswith(']'): + # Save previous section + if current_section == "Notes": + sections["Notes"] = '\n'.join(section_lines).strip() + elif current_section == "Todo": + for l in section_lines: + l = l.strip() + if l.startswith('- [x] '): + sections["Todo"].append({"text": l[6:], "done": True}) + elif l.startswith('- [ ] '): + sections["Todo"].append({"text": l[6:], "done": False}) + elif current_section == "Motivation": + for l in section_lines: + l = l.strip() + if l: + sections["Motivation"].append(l) + # Start new section + current_section = stripped[1:-1] + section_lines = [] + else: + section_lines.append(line) + # Handle last section + if current_section == "Notes": + sections["Notes"] = '\n'.join(section_lines).strip() + elif current_section == "Todo": + for l in section_lines: + l = l.strip() + if l.startswith('- [x] '): + sections["Todo"].append({"text": l[6:], "done": True}) + elif l.startswith('- [ ] '): + sections["Todo"].append({"text": l[6:], "done": False}) + elif current_section == "Motivation": + for l in section_lines: + l = l.strip() + if l: + sections["Motivation"].append(l) + except Exception as e: + print(f"Error loading userdata for {user}: {e}") + return sections + + def save_userdata(user: str, sections: dict) -> bool: + """Save sections back to user data file.""" + filepath = get_userdata_file(user) + try: + lines = [] + lines.append("[Notes]") + lines.append(sections.get("Notes", "")) + lines.append("") + lines.append("[Todo]") + for item in sections.get("Todo", []): + mark = "x" if item.get("done") else " " + lines.append(f"- [{mark}] {item.get('text', '')}") + lines.append("") + lines.append("[Motivation]") + for quote in sections.get("Motivation", []): + lines.append(quote) + with open(filepath, "w", encoding="utf-8") as f: + f.write('\n'.join(lines)) + return True + except Exception as e: + print(f"Error saving userdata for {user}: {e}") + return False + + # -------------------------------- + # Todo Widget + # -------------------------------- + @APP.get("/userdata/todo") + def todo_widget(key: str = "", user: str = "default", accent: str = "4ade80", bgcolor: str = "0d1117"): + """Serve the Todo widget HTML page.""" + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + safe_accent = re.sub(r'[^a-fA-F0-9]', '', accent)[:6] or "4ade80" + safe_bgcolor = re.sub(r'[^a-fA-F0-9]', '', bgcolor)[:6] or "0d1117" + + sections = load_userdata(safe_user) + todos = sections.get("Todo", []) + + # Build todo items HTML + todo_html = "" + for i, item in enumerate(todos): + checked = "checked" if item.get("done") else "" + text_escaped = item.get("text", "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + done_class = "done" if item.get("done") else "" + todo_html += f'''
+ + {text_escaped} + +
''' + + html = f""" + + + + + + + +
+
+ + +
+
{todo_html if todo_html else '
No tasks yet
'}
+
+
+ + +""" + return Response(content=html, media_type="text/html") + + @APP.post("/userdata/todo/add") + async def todo_add(key: str = "", user: str = "default", content: dict = None): + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + if content and content.get("text"): + sections = load_userdata(safe_user) + sections["Todo"].append({"text": content["text"].strip(), "done": False}) + if save_userdata(safe_user, sections): + return {"status": "ok"} + return Response(content="Failed", status_code=500) + + @APP.post("/userdata/todo/toggle") + async def todo_toggle(key: str = "", user: str = "default", index: int = 0): + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + sections = load_userdata(safe_user) + if 0 <= index < len(sections["Todo"]): + sections["Todo"][index]["done"] = not sections["Todo"][index]["done"] + if save_userdata(safe_user, sections): + return {"status": "ok"} + return Response(content="Failed", status_code=500) + + @APP.post("/userdata/todo/delete") + async def todo_delete(key: str = "", user: str = "default", index: int = 0): + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + sections = load_userdata(safe_user) + if 0 <= index < len(sections["Todo"]): + sections["Todo"].pop(index) + if save_userdata(safe_user, sections): + return {"status": "ok"} + return Response(content="Failed", status_code=500) + + # -------------------------------- + # Motivation Widget + # -------------------------------- + import random as _random + + @APP.get("/userdata/motivation") + def motivation_widget(key: str = "", user: str = "default", accent: str = "4ade80", bgcolor: str = "0d1117"): + """Serve the Motivation widget HTML page with random quote and settings modal.""" + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + safe_accent = re.sub(r'[^a-fA-F0-9]', '', accent)[:6] or "4ade80" + safe_bgcolor = re.sub(r'[^a-fA-F0-9]', '', bgcolor)[:6] or "0d1117" + + sections = load_userdata(safe_user) + quotes = sections.get("Motivation", []) + + # Pick random quote + current_quote = _random.choice(quotes) if quotes else "Add your first motivational quote!" + current_quote_escaped = current_quote.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + + # Build quotes list for modal + quotes_html = "" + for i, q in enumerate(quotes): + q_escaped = q.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + quotes_html += f'
{q_escaped}
' + + html = f""" + + + + + + + +
+ +
+
"{current_quote_escaped}"
+
+
{len(quotes)} quote{'s' if len(quotes) != 1 else ''}
+
+ + + + + +""" + return Response(content=html, media_type="text/html") + + @APP.post("/userdata/motivation/add") + async def motivation_add(key: str = "", user: str = "default", content: dict = None): + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + if content and content.get("text"): + sections = load_userdata(safe_user) + sections["Motivation"].append(content["text"].strip()) + if save_userdata(safe_user, sections): + return {"status": "ok"} + return Response(content="Failed", status_code=500) + + @APP.post("/userdata/motivation/delete") + async def motivation_delete(key: str = "", user: str = "default", index: int = 0): + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + sections = load_userdata(safe_user) + if 0 <= index < len(sections["Motivation"]): + sections["Motivation"].pop(index) + if save_userdata(safe_user, sections): + return {"status": "ok"} + return Response(content="Failed", status_code=500) + + @APP.get("/userdata/motivation/random") + def motivation_random(key: str = "", user: str = "default"): + """Get a random motivation quote as JSON.""" + expected_key = os.environ.get("GLANCE_HELPER_KEY", "") + if key != expected_key: + return Response(content="Unauthorized", status_code=401) + safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default" + sections = load_userdata(safe_user) + quotes = sections.get("Motivation", []) + if quotes: + return {"quote": _random.choice(quotes), "total": len(quotes)} + return {"quote": "", "total": 0} + # ================================ # Időkép configuration # ================================ diff --git a/glance-system/glance-kisfenyo.yaml b/glance-system/glance-kisfenyo.yaml index 8103b47..b9175ed 100644 --- a/glance-system/glance-kisfenyo.yaml +++ b/glance-system/glance-kisfenyo.yaml @@ -763,9 +763,18 @@ data: # Calendar Widget - type: calendar first-day-of-week: monday - # To-Do List - - type: to-do + # Tasks (persistent, file-based) + - type: iframe + css-class: iframe-no-tint + source: https://glance-helper.dooplex.hu/userdata/todo?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=kisfenyo&accent=4ade80&bgcolor=0d1117 + height: 200 title: Tasks + # Motivation Quote + - type: iframe + css-class: iframe-no-tint + source: https://glance-helper.dooplex.hu/userdata/motivation?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=kisfenyo&accent=4ade80&bgcolor=0d1117 + height: 100 + title: Motivation # Outline Notes iframe - type: iframe source: https://outline.dooplex.hu/collection/dooplex-server-iTAZn04AaR/recent