Compare commits
3 Commits
V1.0.0
...
40bab48806
| Author | SHA1 | Date | |
|---|---|---|---|
| 40bab48806 | |||
| ed14dd4aef | |||
| 35e6a7b56e |
@@ -4,8 +4,8 @@
|
|||||||
"author": "Florian Heinz",
|
"author": "Florian Heinz",
|
||||||
"description": "Private Flask app for meals, shopping and gentle food planning",
|
"description": "Private Flask app for meals, shopping and gentle food planning",
|
||||||
"tagline": "einfach essen planen",
|
"tagline": "einfach essen planen",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"upstreamVersion": "1.0.0",
|
"upstreamVersion": "1.0.1",
|
||||||
"healthCheckPath": "/",
|
"healthCheckPath": "/",
|
||||||
"httpPort": 8000,
|
"httpPort": 8000,
|
||||||
"manifestVersion": 2,
|
"manifestVersion": 2,
|
||||||
|
|||||||
+32
-1
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
@@ -34,6 +35,7 @@ from .main import main_bp
|
|||||||
|
|
||||||
WEEKDAY_NAMES = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
|
WEEKDAY_NAMES = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
|
||||||
WEEKDAY_SHORT_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
WEEKDAY_SHORT_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
||||||
|
DEFAULT_RELEASE_URL = "https://git.hnz.io/hnzio/nouri-App/releases"
|
||||||
|
|
||||||
|
|
||||||
def load_secret_key(data_dir: Path) -> str:
|
def load_secret_key(data_dir: Path) -> str:
|
||||||
@@ -54,11 +56,38 @@ def load_secret_key(data_dir: Path) -> str:
|
|||||||
return secret_value
|
return secret_value
|
||||||
|
|
||||||
|
|
||||||
|
def load_app_version(root_dir: Path) -> str:
|
||||||
|
env_version = os.environ.get("NOURI_APP_VERSION", "").strip()
|
||||||
|
if env_version:
|
||||||
|
return env_version
|
||||||
|
|
||||||
|
manifest_path = root_dir / "CloudronManifest.json"
|
||||||
|
if manifest_path.exists():
|
||||||
|
try:
|
||||||
|
manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
manifest_data = {}
|
||||||
|
manifest_version = str(
|
||||||
|
manifest_data.get("upstreamVersion")
|
||||||
|
or manifest_data.get("version")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
if manifest_version:
|
||||||
|
return manifest_version
|
||||||
|
return "1.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def load_release_url() -> str:
|
||||||
|
return os.environ.get("NOURI_RELEASE_URL", DEFAULT_RELEASE_URL).strip() or DEFAULT_RELEASE_URL
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> Flask:
|
def create_app() -> Flask:
|
||||||
root_dir = Path(__file__).resolve().parent.parent
|
root_dir = Path(__file__).resolve().parent.parent
|
||||||
data_dir = Path(os.environ.get("NOURI_DATA_DIR", root_dir / "data")).resolve()
|
data_dir = Path(os.environ.get("NOURI_DATA_DIR", root_dir / "data")).resolve()
|
||||||
upload_dir = data_dir / "uploads"
|
upload_dir = data_dir / "uploads"
|
||||||
db_path = data_dir / "nouri.sqlite3"
|
db_path = data_dir / "nouri.sqlite3"
|
||||||
|
app_version = load_app_version(root_dir)
|
||||||
|
release_url = load_release_url()
|
||||||
|
|
||||||
data_dir.mkdir(parents=True, exist_ok=True)
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
ensure_upload_structure(upload_dir)
|
ensure_upload_structure(upload_dir)
|
||||||
@@ -74,7 +103,8 @@ def create_app() -> Flask:
|
|||||||
SESSION_COOKIE_HTTPONLY=True,
|
SESSION_COOKIE_HTTPONLY=True,
|
||||||
SESSION_COOKIE_SAMESITE="Lax",
|
SESSION_COOKIE_SAMESITE="Lax",
|
||||||
SESSION_COOKIE_SECURE=os.environ.get("NOURI_SECURE_COOKIES", "0") == "1",
|
SESSION_COOKIE_SECURE=os.environ.get("NOURI_SECURE_COOKIES", "0") == "1",
|
||||||
APP_VERSION="1.0.0",
|
APP_VERSION=app_version,
|
||||||
|
RELEASE_URL=release_url,
|
||||||
TIMEZONE=os.environ.get("NOURI_TIMEZONE", "Europe/Berlin"),
|
TIMEZONE=os.environ.get("NOURI_TIMEZONE", "Europe/Berlin"),
|
||||||
VAPID_PUBLIC_KEY=os.environ.get("NOURI_VAPID_PUBLIC_KEY", ""),
|
VAPID_PUBLIC_KEY=os.environ.get("NOURI_VAPID_PUBLIC_KEY", ""),
|
||||||
VAPID_PRIVATE_KEY=os.environ.get("NOURI_VAPID_PRIVATE_KEY", ""),
|
VAPID_PRIVATE_KEY=os.environ.get("NOURI_VAPID_PRIVATE_KEY", ""),
|
||||||
@@ -114,6 +144,7 @@ def create_app() -> Flask:
|
|||||||
"notification_channel_options": NOTIFICATION_CHANNEL_OPTIONS,
|
"notification_channel_options": NOTIFICATION_CHANNEL_OPTIONS,
|
||||||
"today": date.today(),
|
"today": date.today(),
|
||||||
"app_version": app.config["APP_VERSION"],
|
"app_version": app.config["APP_VERSION"],
|
||||||
|
"app_release_url": app.config["RELEASE_URL"],
|
||||||
"push_public_key": app.config["VAPID_PUBLIC_KEY"],
|
"push_public_key": app.config["VAPID_PUBLIC_KEY"],
|
||||||
"push_available": bool(app.config["VAPID_PUBLIC_KEY"] and app.config["VAPID_PRIVATE_KEY"]),
|
"push_available": bool(app.config["VAPID_PUBLIC_KEY"] and app.config["VAPID_PRIVATE_KEY"]),
|
||||||
"weekday_name": lambda value: WEEKDAY_NAMES[value.weekday()],
|
"weekday_name": lambda value: WEEKDAY_NAMES[value.weekday()],
|
||||||
|
|||||||
@@ -289,3 +289,31 @@ def category_update(category_id: int):
|
|||||||
get_db().commit()
|
get_db().commit()
|
||||||
flash("Die Zuordnung wurde aktualisiert.", "success")
|
flash("Die Zuordnung wurde aktualisiert.", "success")
|
||||||
return redirect(url_for("admin.category_settings"))
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.post("/categories/<int:category_id>/delete")
|
||||||
|
@admin_required
|
||||||
|
def category_delete(category_id: int):
|
||||||
|
category = get_db().execute(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM household_categories
|
||||||
|
WHERE id = ? AND household_id = ?
|
||||||
|
""",
|
||||||
|
(category_id, g.user["household_id"]),
|
||||||
|
).fetchone()
|
||||||
|
if category is None:
|
||||||
|
flash("Die Kategorie wurde nicht gefunden.", "error")
|
||||||
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|
||||||
|
if category["name"] in DEFAULT_CATEGORIES:
|
||||||
|
flash("Standardkategorien bleiben erhalten. Du kannst sie bei Bedarf pausieren.", "info")
|
||||||
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|
||||||
|
get_db().execute(
|
||||||
|
"DELETE FROM household_categories WHERE id = ? AND household_id = ?",
|
||||||
|
(category_id, g.user["household_id"]),
|
||||||
|
)
|
||||||
|
get_db().commit()
|
||||||
|
flash("Die Kategorie wurde entfernt.", "success")
|
||||||
|
return redirect(url_for("admin.category_settings"))
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ def get_db() -> sqlite3.Connection:
|
|||||||
g.db = sqlite3.connect(
|
g.db = sqlite3.connect(
|
||||||
current_app.config["DATABASE_PATH"],
|
current_app.config["DATABASE_PATH"],
|
||||||
detect_types=sqlite3.PARSE_DECLTYPES,
|
detect_types=sqlite3.PARSE_DECLTYPES,
|
||||||
|
timeout=30,
|
||||||
)
|
)
|
||||||
g.db.row_factory = sqlite3.Row
|
g.db.row_factory = sqlite3.Row
|
||||||
g.db.execute("PRAGMA foreign_keys = ON")
|
g.db.execute("PRAGMA foreign_keys = ON")
|
||||||
|
g.db.execute("PRAGMA busy_timeout = 30000")
|
||||||
return g.db
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+94
-35
@@ -4,6 +4,7 @@ from collections import defaultdict
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from itertools import product
|
from itertools import product
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
@@ -79,8 +80,10 @@ def refresh_due_context():
|
|||||||
if getattr(g, "user", None) is None:
|
if getattr(g, "user", None) is None:
|
||||||
return None
|
return None
|
||||||
if request.method == "GET" and endpoint.startswith("main."):
|
if request.method == "GET" and endpoint.startswith("main."):
|
||||||
ensure_user_settings_row()
|
try:
|
||||||
activate_due_shopping_needs()
|
activate_due_shopping_needs()
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
current_app.logger.warning("Due shopping needs could not be activated during this request.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -172,19 +175,54 @@ def get_household_settings() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def ensure_user_settings_row() -> None:
|
def default_user_settings() -> dict:
|
||||||
|
suggestion_style = "balanced"
|
||||||
|
return {
|
||||||
|
"user_id": int(g.user["id"]),
|
||||||
|
"reminders_enabled": True,
|
||||||
|
"push_enabled": False,
|
||||||
|
"notification_channel": "in_app",
|
||||||
|
"suggestion_style": suggestion_style,
|
||||||
|
"energy_preference": suggestion_style_energy_preference(suggestion_style),
|
||||||
|
"remind_before_shopping": True,
|
||||||
|
"remind_on_shopping_day": True,
|
||||||
|
"show_missing_for_upcoming_week": True,
|
||||||
|
"show_planned_not_shopped": True,
|
||||||
|
"remind_tomorrow_if_sparse": True,
|
||||||
|
"remind_week_if_sparse": True,
|
||||||
|
"push_missing_breakfast": False,
|
||||||
|
"push_missing_lunch": False,
|
||||||
|
"push_missing_dinner": False,
|
||||||
|
"suggest_home_for_today": True,
|
||||||
|
"remind_small_snack": False,
|
||||||
|
"remind_nuts": False,
|
||||||
|
"show_meal_balancing": True,
|
||||||
|
"suggest_templates": True,
|
||||||
|
"suggest_patterns": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_user_settings_row(*, commit: bool = False) -> None:
|
||||||
|
existing = get_db().execute(
|
||||||
|
"SELECT 1 FROM user_settings WHERE user_id = ? LIMIT 1",
|
||||||
|
(g.user["id"],),
|
||||||
|
).fetchone()
|
||||||
|
if existing is not None:
|
||||||
|
return
|
||||||
get_db().execute(
|
get_db().execute(
|
||||||
"INSERT OR IGNORE INTO user_settings (user_id) VALUES (?)",
|
"INSERT INTO user_settings (user_id) VALUES (?)",
|
||||||
(g.user["id"],),
|
(g.user["id"],),
|
||||||
)
|
)
|
||||||
|
if commit:
|
||||||
|
get_db().commit()
|
||||||
|
|
||||||
|
|
||||||
def get_user_settings() -> dict:
|
def get_user_settings() -> dict:
|
||||||
ensure_user_settings_row()
|
settings = default_user_settings()
|
||||||
row = get_db().execute("SELECT * FROM user_settings WHERE user_id = ?", (g.user["id"],)).fetchone()
|
row = get_db().execute("SELECT * FROM user_settings WHERE user_id = ?", (g.user["id"],)).fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
return {}
|
return settings
|
||||||
settings = dict(row)
|
settings.update(dict(row))
|
||||||
boolean_fields = {
|
boolean_fields = {
|
||||||
"reminders_enabled",
|
"reminders_enabled",
|
||||||
"push_enabled",
|
"push_enabled",
|
||||||
@@ -1239,57 +1277,76 @@ def build_generated_meal_name(combo: list[dict], daypart_slug: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def build_dynamic_meal_suggestions(home_foods: list[dict], daypart_slug: str, limit: int) -> list[dict]:
|
def build_dynamic_meal_suggestions(home_foods: list[dict], daypart_slug: str, limit: int) -> list[dict]:
|
||||||
builder_groups: dict[str, list[dict]] = defaultdict(list)
|
|
||||||
for food in home_foods:
|
|
||||||
for builder_key in food.get("builder_keys", ["neutral"]):
|
|
||||||
builder_groups[builder_key].append(food)
|
|
||||||
|
|
||||||
if daypart_slug in {"breakfast", "morning-snack", "afternoon-snack", "late-snack"}:
|
if daypart_slug in {"breakfast", "morning-snack", "afternoon-snack", "late-snack"}:
|
||||||
target_patterns = [
|
target_patterns = [
|
||||||
("carb", "dairy", "fruit"),
|
{
|
||||||
("carb", "dairy", "nuts"),
|
"slots": ({"carb"}, {"dairy", "protein"}, {"fruit", "nuts", "seeds"}),
|
||||||
("carb", "dairy", "seeds"),
|
"reason": "Passt gut zu Frühstück oder Snack",
|
||||||
("carb", "fruit", "dairy"),
|
},
|
||||||
|
{
|
||||||
|
"slots": ({"carb"}, {"dairy", "protein"}),
|
||||||
|
"reason": "Zuhause schnell kombinierbar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slots": ({"dairy", "protein"}, {"fruit", "nuts", "seeds"}),
|
||||||
|
"reason": "Lässt sich gut als kleiner Snack vormerken",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
reasons = {
|
|
||||||
("carb", "dairy", "fruit"): "Passt gut zu Frühstück oder Snack",
|
|
||||||
("carb", "dairy", "nuts"): "Lässt sich gut für einen Snack vormerken",
|
|
||||||
("carb", "dairy", "seeds"): "Lässt sich gut für einen Snack vormerken",
|
|
||||||
("carb", "fruit", "dairy"): "Zuhause gut kombinierbar",
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
target_patterns = [
|
target_patterns = [
|
||||||
("protein", "carb", "veg"),
|
{
|
||||||
("protein", "carb"),
|
"slots": ({"protein"}, {"carb"}, {"veg"}),
|
||||||
|
"reason": "Zuhause als vollständige Mahlzeit möglich",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slots": ({"protein"}, {"carb"}),
|
||||||
|
"reason": "Lässt sich leicht ergänzen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slots": ({"protein"}, {"veg"}),
|
||||||
|
"reason": "Zuhause schon gut kombinierbar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slots": ({"carb"}, {"veg"}),
|
||||||
|
"reason": "Daraus kann schnell etwas Einfaches werden",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
reasons = {
|
|
||||||
("protein", "carb", "veg"): "Zuhause als vollständige Mahlzeit möglich",
|
|
||||||
("protein", "carb"): "Lässt sich leicht ergänzen",
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestions: list[dict] = []
|
suggestions: list[dict] = []
|
||||||
seen_signatures: set[tuple[int, ...]] = set()
|
seen_signatures: set[tuple[int, ...]] = set()
|
||||||
|
|
||||||
|
def slot_matches(food: dict, slot_keys: set[str]) -> bool:
|
||||||
|
return bool(slot_keys & set(food.get("builder_keys", ["neutral"])))
|
||||||
|
|
||||||
for pattern in target_patterns:
|
for pattern in target_patterns:
|
||||||
groups = [builder_groups.get(builder_key, []) for builder_key in pattern]
|
slot_candidates = []
|
||||||
if any(not group for group in groups):
|
for slot_keys in pattern["slots"]:
|
||||||
|
matches = [food for food in home_foods if slot_matches(food, slot_keys)]
|
||||||
|
if not matches:
|
||||||
|
slot_candidates = []
|
||||||
|
break
|
||||||
|
slot_candidates.append(matches)
|
||||||
|
if not slot_candidates:
|
||||||
continue
|
continue
|
||||||
for combo in product(*groups):
|
|
||||||
|
for combo in product(*slot_candidates):
|
||||||
signature = normalized_component_signature([item["id"] for item in combo])
|
signature = normalized_component_signature([item["id"] for item in combo])
|
||||||
if len(signature) != len(pattern) or signature in seen_signatures:
|
if len(signature) != len(combo) or signature in seen_signatures:
|
||||||
continue
|
continue
|
||||||
seen_signatures.add(signature)
|
seen_signatures.add(signature)
|
||||||
combo_items = list(combo)
|
combo_items = list(combo)
|
||||||
suggestions.append(
|
suggestions.append(
|
||||||
{
|
{
|
||||||
"title": build_generated_meal_name(combo_items, daypart_slug),
|
"title": build_generated_meal_name(combo_items, daypart_slug),
|
||||||
"reason": reasons.get(pattern, "Zuhause gut kombinierbar"),
|
"reason": pattern["reason"],
|
||||||
"component_ids": [item["id"] for item in combo_items],
|
"component_ids": [item["id"] for item in combo_items],
|
||||||
"existing_item_id": None,
|
"existing_item_id": None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if len(suggestions) >= limit:
|
if len(suggestions) >= limit * 3:
|
||||||
return suggestions
|
break
|
||||||
|
if len(suggestions) >= limit * 3:
|
||||||
|
break
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
|
|
||||||
@@ -1719,6 +1776,7 @@ def build_day_planner_sections(
|
|||||||
+ [item for item in candidates if item["kind"] == "food"],
|
+ [item for item in candidates if item["kind"] == "food"],
|
||||||
limit=20,
|
limit=20,
|
||||||
)
|
)
|
||||||
|
search_candidates = dedupe_items(meal_candidates + food_candidates, limit=24)
|
||||||
entry_item_ids = [int(entry["item_id"]) for entry in entries]
|
entry_item_ids = [int(entry["item_id"]) for entry in entries]
|
||||||
sections.append(
|
sections.append(
|
||||||
{
|
{
|
||||||
@@ -1727,6 +1785,7 @@ def build_day_planner_sections(
|
|||||||
"candidates": candidates,
|
"candidates": candidates,
|
||||||
"meal_candidates": meal_candidates,
|
"meal_candidates": meal_candidates,
|
||||||
"food_candidates": food_candidates,
|
"food_candidates": food_candidates,
|
||||||
|
"search_candidates": search_candidates,
|
||||||
"recipe_suggestions": build_home_recipe_suggestions(int(daypart["id"]), limit=3),
|
"recipe_suggestions": build_home_recipe_suggestions(int(daypart["id"]), limit=3),
|
||||||
"suggestions": build_daypart_suggestions(daypart["id"]),
|
"suggestions": build_daypart_suggestions(daypart["id"]),
|
||||||
"balance_suggestion": build_balance_suggestion(int(daypart["id"]), entry_item_ids),
|
"balance_suggestion": build_balance_suggestion(int(daypart["id"]), entry_item_ids),
|
||||||
|
|||||||
+47
-2
@@ -78,8 +78,8 @@
|
|||||||
const applyFilter = () => {
|
const applyFilter = () => {
|
||||||
const term = input.value.trim().toLowerCase();
|
const term = input.value.trim().toLowerCase();
|
||||||
if (!term) {
|
if (!term) {
|
||||||
items.forEach((item) => {
|
items.forEach((item, index) => {
|
||||||
item.hidden = false;
|
item.hidden = hasLimit ? index >= resultLimit : false;
|
||||||
});
|
});
|
||||||
syncGroups();
|
syncGroups();
|
||||||
return;
|
return;
|
||||||
@@ -109,8 +109,53 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initIosPullToRefresh = () => {
|
||||||
|
const isAppleTouchDevice = /iP(ad|hone|od)/.test(navigator.userAgent)
|
||||||
|
|| (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||||
|
if (!isAppleTouchDevice) return;
|
||||||
|
|
||||||
|
let startY = 0;
|
||||||
|
let maxPull = 0;
|
||||||
|
let tracking = false;
|
||||||
|
|
||||||
|
window.addEventListener("touchstart", (event) => {
|
||||||
|
if (window.scrollY > 0) {
|
||||||
|
tracking = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startY = event.touches[0].clientY;
|
||||||
|
maxPull = 0;
|
||||||
|
tracking = true;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
window.addEventListener("touchmove", (event) => {
|
||||||
|
if (!tracking) return;
|
||||||
|
const currentY = event.touches[0].clientY;
|
||||||
|
maxPull = Math.max(maxPull, currentY - startY);
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
window.addEventListener("touchend", () => {
|
||||||
|
if (tracking && maxPull > 96 && window.scrollY <= 2) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
tracking = false;
|
||||||
|
maxPull = 0;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener("gesturestart", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("touchmove", (event) => {
|
||||||
|
if (event.touches.length > 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
initMobileSheet();
|
initMobileSheet();
|
||||||
initFilterInputs();
|
initFilterInputs();
|
||||||
|
initIosPullToRefresh();
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -66,6 +66,12 @@
|
|||||||
{% if category.is_active %}Pausieren{% else %}Wieder aktivieren{% endif %}
|
{% if category.is_active %}Pausieren{% else %}Wieder aktivieren{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% if category.name not in default_categories %}
|
||||||
|
<form method="post" action="{{ url_for('admin.category_delete', category_id=category.id) }}">
|
||||||
|
{{ csrf_input() }}
|
||||||
|
<button class="ghost-button" type="submit">Löschen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="de" data-theme="auto">
|
<html lang="de" data-theme="auto">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
||||||
<title>{% block title %}Nouri{% endblock %}</title>
|
<title>{% block title %}Nouri{% endblock %}</title>
|
||||||
<meta name="theme-color" content="#de9862">
|
<meta name="theme-color" content="#de9862">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<div class="footer-copy">
|
<div class="footer-copy">
|
||||||
<span>Version {{ app_version }}</span>
|
<a href="{{ app_release_url }}" target="_blank" rel="noreferrer">Version {{ app_version }}</a>
|
||||||
<span>Made with <span class="ui-icon icon-heart"></span> in Göttingen</span>
|
<span>Made with <span class="ui-icon icon-heart"></span> in Göttingen</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-copy">
|
<div class="footer-copy">
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
<input type="text" placeholder="Lebensmittel oder Mahlzeiten suchen" data-filter-input data-filter-target="#planner-list-{{ section.daypart.id }}" data-filter-limit="3">
|
<input type="text" placeholder="Lebensmittel oder Mahlzeiten suchen" data-filter-input data-filter-target="#planner-list-{{ section.daypart.id }}" data-filter-limit="3">
|
||||||
</label>
|
</label>
|
||||||
<div class="compact-picker-list" id="planner-list-{{ section.daypart.id }}">
|
<div class="compact-picker-list" id="planner-list-{{ section.daypart.id }}">
|
||||||
{% for item in section.food_candidates %}
|
{% for item in section.search_candidates %}
|
||||||
<form method="post" action="{{ url_for('main.planner_day', date=selected_date.isoformat()) }}" data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }}">
|
<form method="post" action="{{ url_for('main.planner_day', date=selected_date.isoformat()) }}" data-filter-label="{{ item.name|lower }} {{ item.category|default('', true)|lower }}">
|
||||||
{{ csrf_input() }}
|
{{ csrf_input() }}
|
||||||
<input type="hidden" name="plan_date" value="{{ selected_date.isoformat() }}">
|
<input type="hidden" name="plan_date" value="{{ selected_date.isoformat() }}">
|
||||||
@@ -184,7 +184,10 @@
|
|||||||
<input type="hidden" name="visibility" value="{{ item.visibility }}">
|
<input type="hidden" name="visibility" value="{{ item.visibility }}">
|
||||||
<button class="picker-row" type="submit">
|
<button class="picker-row" type="submit">
|
||||||
<span>{{ item.name }}</span>
|
<span>{{ item.name }}</span>
|
||||||
{% if item.availability_state == 'home' %}<small>zuhause</small>{% endif %}
|
<small>
|
||||||
|
{{ item_kind_labels[item.kind]|lower }}
|
||||||
|
{% if item.availability_state == 'home' %} · zuhause{% endif %}
|
||||||
|
</small>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user