feat(web-app): Implement standard item list and improved item adding logic (see diff for details)
This commit is contained in:
92
web-app/main.py
Normal file
92
web-app/main.py
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
import sqlite3
|
||||
import uvicorn
|
||||
import requests
|
||||
import os
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# --- Gotify Konfiguration ---
|
||||
# Die Gotify URL wird aus der Umgebungsvariable GOTIFY_URL gelesen.
|
||||
# Beispiel: https://gotify.example.com/message?token=MY_TOKEN
|
||||
GOTIFY_URL = os.getenv("GOTIFY_URL")
|
||||
|
||||
def send_gotify_notification(title: str, message: str):
|
||||
if not GOTIFY_URL:
|
||||
print("GOTIFY_URL ist nicht gesetzt. Überspringe Benachrichtigung.")
|
||||
return
|
||||
try:
|
||||
payload = {"title": title, "message": message}
|
||||
requests.post(GOTIFY_URL, json=payload)
|
||||
print("Gotify-Benachrichtigung gesendet.")
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Senden der Gotify-Benachrichtigung: {e}")
|
||||
|
||||
from translations import get_standard_items
|
||||
|
||||
# --- Datenbank-Setup ---
|
||||
def init_db():
|
||||
conn = sqlite3.connect("shoppinglist.db")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Datenbank initialisiert.")
|
||||
|
||||
@app.get("/api/standard-items/{lang}")
|
||||
async def get_standard_items_route(lang: str):
|
||||
return get_standard_items(lang)
|
||||
|
||||
# --- Datenmodelle ---
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
|
||||
# --- API Endpunkte ---
|
||||
@app.get("/api/items")
|
||||
async def get_items():
|
||||
conn = sqlite3.connect("shoppinglist.db")
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM items ORDER BY name")
|
||||
items = cursor.fetchall()
|
||||
conn.close()
|
||||
return {"items": [dict(row) for row in items]}
|
||||
|
||||
@app.post("/api/items")
|
||||
async def add_item(item: Item):
|
||||
conn = sqlite3.connect("shoppinglist.db")
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("INSERT INTO items (name) VALUES (?)", (item.name,))
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
conn.close()
|
||||
raise HTTPException(status_code=400, detail="Item already exists")
|
||||
finally:
|
||||
conn.close()
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.delete("/api/items/{item_id}")
|
||||
async def delete_item(item_id: int):
|
||||
conn = sqlite3.connect("shoppinglist.db")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM items WHERE id = ?", (item_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.post("/api/notify")
|
||||
async def trigger_notification():
|
||||
send_gotify_notification("Einkaufsliste aktualisiert", "Die Liste wurde von einem Benutzer geändert.")
|
||||
return {"status": "notification sent"}
|
||||
|
||||
# --- Statische Dateien (Frontend) ---
|
||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
||||
139
web-app/static/index.html
Normal file
139
web-app/static/index.html
Normal file
@@ -0,0 +1,139 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Geteilte Einkaufsliste</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; max-width: 600px; margin: auto; padding: 20px; }
|
||||
h1 { text-align: center; }
|
||||
ul { list-style-type: none; padding: 0; }
|
||||
li { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #ddd; }
|
||||
.delete-btn { cursor: pointer; color: red; }
|
||||
form { display: flex; margin-top: 20px; }
|
||||
input[type="text"] { flex-grow: 1; padding: 10px; }
|
||||
button { padding: 10px; }
|
||||
#notify-btn { width: 100%; margin-top: 15px; background-color: #28a745; color: white; border: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Geteilte Einkaufsliste</h1>
|
||||
|
||||
<ul id="item-list"></ul>
|
||||
|
||||
<form id="add-item-form">
|
||||
<input type="text" id="item-name" placeholder="Neuer Artikel..." list="standard-items" required>
|
||||
<datalist id="standard-items"></datalist>
|
||||
<button type="submit">Hinzufügen</button>
|
||||
<button type="button" id="add-combined-btn">+1</button>
|
||||
</form>
|
||||
|
||||
<button id="notify-btn">Änderungen abschließen & Benachrichtigen</button>
|
||||
|
||||
<script>
|
||||
const itemList = document.getElementById('item-list');
|
||||
const addItemForm = document.getElementById('add-item-form');
|
||||
const itemNameInput = document.getElementById('item-name');
|
||||
const notifyBtn = document.getElementById('notify-btn');
|
||||
const standardItemsDatalist = document.getElementById('standard-items');
|
||||
const addCombinedBtn = document.getElementById('add-combined-btn');
|
||||
|
||||
let standardItems = [];
|
||||
|
||||
// --- Funktionen ---
|
||||
|
||||
async function fetchStandardItems() {
|
||||
const lang = navigator.language.startsWith('de') ? 'de' : 'en';
|
||||
const response = await fetch(`/api/standard-items/${lang}`);
|
||||
standardItems = await response.json();
|
||||
standardItemsDatalist.innerHTML = standardItems.map(item => `<option value="${item}">`).join('');
|
||||
}
|
||||
|
||||
async function fetchItems() {
|
||||
const response = await fetch('/api/items');
|
||||
const data = await response.json();
|
||||
itemList.innerHTML = ''; // Liste leeren
|
||||
data.items.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = item.name;
|
||||
|
||||
const deleteBtn = document.createElement('span');
|
||||
deleteBtn.textContent = 'Löschen';
|
||||
deleteBtn.className = 'delete-btn';
|
||||
deleteBtn.onclick = () => deleteItem(item.id);
|
||||
|
||||
li.appendChild(deleteBtn);
|
||||
itemList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function addItem(name) {
|
||||
if (!name) return;
|
||||
|
||||
await fetch('/api/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name })
|
||||
});
|
||||
|
||||
itemNameInput.value = '';
|
||||
fetchItems();
|
||||
}
|
||||
|
||||
async function addItemsFromInput(inputString, splitByComma) {
|
||||
if (!inputString.trim()) return;
|
||||
|
||||
let itemsToAdd = [];
|
||||
if (splitByComma && inputString.includes(',')) {
|
||||
itemsToAdd = inputString.split(',').map(item => item.trim()).filter(item => item);
|
||||
} else {
|
||||
itemsToAdd.push(inputString.trim());
|
||||
}
|
||||
|
||||
for (const item of itemsToAdd) {
|
||||
await addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteItem(id) {
|
||||
await fetch(`/api/items/${id}`, { method: 'DELETE' });
|
||||
fetchItems();
|
||||
}
|
||||
|
||||
async function sendNotification() {
|
||||
notifyBtn.textContent = 'Sende...';
|
||||
await fetch('/api/notify', { method: 'POST' });
|
||||
notifyBtn.textContent = 'Benachrichtigung gesendet!';
|
||||
setTimeout(() => {
|
||||
notifyBtn.textContent = 'Änderungen abschließen & Benachrichtigen';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// --- Event Listeners ---
|
||||
addItemForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
await addItemsFromInput(itemNameInput.value, true);
|
||||
});
|
||||
|
||||
itemNameInput.addEventListener('keydown', async (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
await addItemsFromInput(itemNameInput.value, true);
|
||||
}
|
||||
});
|
||||
|
||||
addCombinedBtn.addEventListener('click', async () => {
|
||||
await addItemsFromInput(itemNameInput.value, false);
|
||||
});
|
||||
|
||||
notifyBtn.addEventListener('click', sendNotification);
|
||||
|
||||
// --- Initialisierung ---
|
||||
fetchStandardItems();
|
||||
fetchItems();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
23
web-app/translations.py
Normal file
23
web-app/translations.py
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List
|
||||
|
||||
def get_standard_items(lang: str) -> List[str]:
|
||||
"""
|
||||
Parses the strings.xml file for the given language and returns the standard shopping list items.
|
||||
"""
|
||||
if lang == "de":
|
||||
filepath = "../app/src/main/res/values-de/strings.xml"
|
||||
else:
|
||||
filepath = "../app/src/main/res/values/strings.xml"
|
||||
|
||||
try:
|
||||
tree = ET.parse(filepath)
|
||||
root = tree.getroot()
|
||||
for string_array in root.findall('string-array'):
|
||||
if string_array.get('name') == 'standard_list_items':
|
||||
return [item.text for item in string_array.findall('item')]
|
||||
except (ET.ParseError, FileNotFoundError):
|
||||
return []
|
||||
|
||||
return []
|
||||
Reference in New Issue
Block a user