added notes feature
This commit is contained in:
@@ -28,6 +28,173 @@ data:
|
|||||||
|
|
||||||
APP = FastAPI()
|
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
|
# Időkép configuration
|
||||||
# ================================
|
# ================================
|
||||||
|
|||||||
@@ -591,6 +591,10 @@ data:
|
|||||||
columns:
|
columns:
|
||||||
- size: small
|
- size: small
|
||||||
widgets:
|
widgets:
|
||||||
|
- type: iframe
|
||||||
|
source: https://glance-helper.dooplex.hu/notes?key=oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT&user=orsi
|
||||||
|
height: 250
|
||||||
|
title: Quick Notes
|
||||||
- type: bookmarks
|
- type: bookmarks
|
||||||
title: Links for Teaching
|
title: Links for Teaching
|
||||||
groups:
|
groups:
|
||||||
|
|||||||
Reference in New Issue
Block a user