added notes feature
This commit is contained in:
@@ -28,6 +28,173 @@ data:
|
||||
|
||||
APP = FastAPI()
|
||||
|
||||
# ================================
|
||||
# Simple Notes Widget - Multi-user
|
||||
# ================================
|
||||
|
||||
def get_notes_file(user: str) -> str:
|
||||
"""Get notes file path for a user, with validation."""
|
||||
# Sanitize username: only allow alphanumeric, dash, underscore
|
||||
safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user)
|
||||
if not safe_user:
|
||||
safe_user = "default"
|
||||
data_dir = os.environ.get("DATA_DIR", "/data")
|
||||
return os.path.join(data_dir, f"notes_{safe_user}.txt")
|
||||
|
||||
def load_notes(user: str) -> str:
|
||||
"""Load notes from file for a specific user."""
|
||||
notes_file = get_notes_file(user)
|
||||
try:
|
||||
if os.path.exists(notes_file):
|
||||
with open(notes_file, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"Error loading notes for {user}: {e}")
|
||||
return ""
|
||||
|
||||
def save_notes(user: str, content: str) -> bool:
|
||||
"""Save notes to file for a specific user."""
|
||||
notes_file = get_notes_file(user)
|
||||
try:
|
||||
with open(notes_file, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving notes for {user}: {e}")
|
||||
return False
|
||||
|
||||
@APP.get("/notes")
|
||||
def notes_widget(key: str = "", user: str = "default"):
|
||||
"""Serve the notes widget HTML page for a specific user."""
|
||||
expected_key = os.environ.get("GLANCE_HELPER_KEY", "")
|
||||
if key != expected_key:
|
||||
return Response(content="Unauthorized", status_code=401)
|
||||
|
||||
# Sanitize user for display
|
||||
safe_user = re.sub(r'[^a-zA-Z0-9_-]', '', user) or "default"
|
||||
current_notes = load_notes(safe_user)
|
||||
# Escape for safe HTML embedding
|
||||
escaped_notes = current_notes.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
background: transparent;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}}
|
||||
.container {{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
}}
|
||||
.status {{
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
padding: 4px 0;
|
||||
text-align: right;
|
||||
min-height: 20px;
|
||||
}}
|
||||
.status.saving {{ color: rgba(255, 200, 100, 0.8); }}
|
||||
.status.saved {{ color: rgba(100, 255, 150, 0.8); }}
|
||||
.status.error {{ color: rgba(255, 100, 100, 0.8); }}
|
||||
textarea {{
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 12px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}}
|
||||
textarea:focus {{
|
||||
border-color: rgba(180, 130, 220, 0.5);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}}
|
||||
textarea::placeholder {{
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<textarea id="notes" placeholder="Write your notes here...">{escaped_notes}</textarea>
|
||||
<div class="status" id="status"></div>
|
||||
</div>
|
||||
<script>
|
||||
const textarea = document.getElementById('notes');
|
||||
const status = document.getElementById('status');
|
||||
const apiKey = '{expected_key}';
|
||||
const user = '{safe_user}';
|
||||
let saveTimeout = null;
|
||||
let lastSaved = textarea.value;
|
||||
|
||||
function updateStatus(text, className) {{
|
||||
status.textContent = text;
|
||||
status.className = 'status ' + (className || '');
|
||||
}}
|
||||
|
||||
async function saveNotes() {{
|
||||
const content = textarea.value;
|
||||
if (content === lastSaved) return;
|
||||
|
||||
updateStatus('Saving...', 'saving');
|
||||
try {{
|
||||
const response = await fetch('/notes/save?key=' + apiKey + '&user=' + user, {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{ content: content }})
|
||||
}});
|
||||
if (response.ok) {{
|
||||
lastSaved = content;
|
||||
updateStatus('Saved', 'saved');
|
||||
setTimeout(() => updateStatus(''), 2000);
|
||||
}} else {{
|
||||
updateStatus('Save failed', 'error');
|
||||
}}
|
||||
}} catch (e) {{
|
||||
updateStatus('Save failed', 'error');
|
||||
}}
|
||||
}}
|
||||
|
||||
textarea.addEventListener('input', () => {{
|
||||
if (saveTimeout) clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(saveNotes, 1000);
|
||||
}});
|
||||
|
||||
textarea.addEventListener('blur', saveNotes);
|
||||
window.addEventListener('beforeunload', saveNotes);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return Response(content=html, media_type="text/html")
|
||||
|
||||
@APP.post("/notes/save")
|
||||
async def save_notes_api(key: str = "", user: str = "default", content: dict = None):
|
||||
"""API endpoint to save notes for a specific user."""
|
||||
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" in content:
|
||||
if save_notes(safe_user, content["content"]):
|
||||
return {"status": "ok", "user": safe_user}
|
||||
return Response(content="Failed to save", status_code=500)
|
||||
|
||||
# ================================
|
||||
# Időkép configuration
|
||||
# ================================
|
||||
|
||||
@@ -591,6 +591,10 @@ data:
|
||||
columns:
|
||||
- size: small
|
||||
widgets:
|
||||
- type: iframe
|
||||
source: https://glance-helper.dooplex.hu/notes?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=orsi
|
||||
height: 250
|
||||
title: Quick Notes
|
||||
- type: bookmarks
|
||||
title: Links for Teaching
|
||||
groups:
|
||||
|
||||
Reference in New Issue
Block a user