From 2ff6d4dfe4018dd98f7ede3bc91b345e3570fef0 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 8 May 2026 18:58:31 +0200 Subject: [PATCH 1/2] Feature: Hintergrundbild Portrait=Panorama (track/300vw) vs Landscape=Vollbild (overlay/100vw) mit Orientation-Listener (SW by-v784) --- backend/main.py | 2 +- backend/static/index.html | 8 +++---- backend/static/js/app.js | 2 +- backend/static/js/worlds.js | 43 ++++++++++++++++++++++++++++++------- backend/static/sw.js | 2 +- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/backend/main.py b/backend/main.py index dc31b11..9218a81 100644 --- a/backend/main.py +++ b/backend/main.py @@ -327,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "783" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "784" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index dd4b114..7213d86 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -575,10 +575,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 8d6c130..163f8de 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '783'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '784'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index ef13b02..33fb509 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -917,23 +917,51 @@ window.Worlds = (() => { } catch { return dog.foto_url || null; } } + let _bgUrl = null; // aktuell gesetztes Hintergrundbild + + function _applyBgOrientation() { + const ov = document.getElementById('worlds-overlay'); + const track = document.getElementById('worlds-track'); + if (!ov || !track || !_bgUrl) return; + const portrait = window.matchMedia('(orientation: portrait)').matches; + if (portrait) { + // Panorama: Bild über alle drei Welten, scrollt mit dem Swipe + ov.style.backgroundImage = ''; + track.style.backgroundImage = `url('${_bgUrl}')`; + track.style.backgroundSize = 'cover'; + track.style.backgroundPosition = 'center 40%'; + track.style.backgroundRepeat = 'no-repeat'; + } else { + // Vollbild pro Welt (Landscape / Desktop) + track.style.backgroundImage = ''; + ov.style.backgroundImage = `url('${_bgUrl}')`; + ov.style.backgroundSize = 'cover'; + ov.style.backgroundPosition = 'center 40%'; + ov.style.backgroundRepeat = 'no-repeat'; + } + } + + // Orientierungswechsel → Bild neu setzen + window.matchMedia('(orientation: portrait)').addEventListener('change', _applyBgOrientation); + function _applyBgImage(url) { - const ov = document.getElementById('worlds-overlay'); - if (!ov) return; + const ov = document.getElementById('worlds-overlay'); + const track = document.getElementById('worlds-track'); + if (!ov || !track) return; if (url) { const toLoad = new Image(); toLoad.onload = () => { _hasBgPhoto = true; - ov.style.backgroundImage = `url('${url}')`; - ov.style.backgroundSize = 'cover'; - ov.style.backgroundPosition = 'center 40%'; - ov.style.backgroundRepeat = 'no-repeat'; + _bgUrl = url; + _applyBgOrientation(); document.getElementById('wh-photo-hint')?.remove(); }; toLoad.onerror = () => _applyBgImage(null); toLoad.src = url; } else { _hasBgPhoto = false; + _bgUrl = null; + track.style.backgroundImage = ''; ov.style.backgroundImage = 'linear-gradient(160deg,#1a1f35 0%,#16213e 33%,#1a2535 67%,#0f1921 100%)'; ov.style.backgroundSize = '100% 100%'; } @@ -979,8 +1007,7 @@ window.Worlds = (() => { const staleMin = Math.max(weatherObj.ageMin || 0, dogsObj.ageMin || 0); // Panorama-Bild setzen (nur wenn noch kein Bild vorhanden) - const ov = document.getElementById('worlds-overlay'); - if (dog && !ov?.style.backgroundImage?.startsWith('url')) { + if (dog && !_bgUrl) { _loadDailyImage(dog).then(_applyBgImage); } else if (!dog) { _applyBgImage(null); } diff --git a/backend/static/sw.js b/backend/static/sw.js index f38d799..fbdb8fe 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v783'; +const CACHE_VERSION = 'by-v784'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache From e8cf742911b5370c6433de23aa55602cb550f8f4 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 8 May 2026 19:06:29 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Feature:=20App-Einstellungen=20in=20DB=20(p?= =?UTF-8?q?referred=5Ftheme=20neu,=20notes=5Fki+gassi=5Fstunde=20schon=20d?= =?UTF-8?q?rin)=20=E2=80=94=20ger=C3=A4te=C3=BCbergreifend=20sync=20(SW=20?= =?UTF-8?q?by-v785)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database.py | 4 ++++ backend/main.py | 2 +- backend/routes/auth.py | 4 +++- backend/routes/profile.py | 3 +++ backend/static/index.html | 8 ++++---- backend/static/js/app.js | 14 +++++++++++++- backend/static/js/pages/settings.js | 24 ++++++++++++------------ backend/static/sw.js | 2 +- 8 files changed, 41 insertions(+), 20 deletions(-) diff --git a/backend/database.py b/backend/database.py index cff0f89..89c141e 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2075,6 +2075,10 @@ def _migrate(conn_factory): _seed_help_articles(conn) logger.info("Migration: Hilfe/FAQ-Tabelle bereit.") + if 'preferred_theme' not in [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()]: + conn.execute("ALTER TABLE users ADD COLUMN preferred_theme TEXT DEFAULT 'system'") + logger.info("Migration: preferred_theme Spalte zu users hinzugefügt.") + conn.executescript(""" CREATE TABLE IF NOT EXISTS bday_ki_cache ( dog_id INTEGER NOT NULL, diff --git a/backend/main.py b/backend/main.py index 9218a81..4a9ee01 100644 --- a/backend/main.py +++ b/backend/main.py @@ -327,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "784" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "785" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 4772ae6..a0174e6 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -238,7 +238,9 @@ async def me(user=Depends(get_current_user)): """SELECT id, name, real_name, email, rolle, is_premium, email_verified, bio, wohnort, erfahrung, social_link, profil_sichtbarkeit, avatar_url, created_at, - is_founder, is_partner, founder_number, is_founder_pending + is_founder, is_partner, founder_number, is_founder_pending, + notes_ki_enabled, gassi_stunde_push, + preferred_theme FROM users WHERE id=?""", (user["id"],) ).fetchone() diff --git a/backend/routes/profile.py b/backend/routes/profile.py index 9762f95..9cb0667 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -27,6 +27,7 @@ class ProfileUpdate(BaseModel): profil_sichtbarkeit: Optional[str] = None notes_ki_enabled: Optional[int] = None gassi_stunde_push: Optional[int] = None + preferred_theme: Optional[str] = None def _load_user(user_id: int) -> dict: @@ -54,6 +55,8 @@ async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)): raise HTTPException(400, f"erfahrung muss eines von {sorted(VALID_ERFAHRUNG)} sein.") if "profil_sichtbarkeit" in fields and fields["profil_sichtbarkeit"] not in VALID_SICHTBARKEIT: raise HTTPException(400, f"profil_sichtbarkeit muss eines von {sorted(VALID_SICHTBARKEIT)} sein.") + if "preferred_theme" in fields and fields["preferred_theme"] not in ("system", "light", "dark"): + raise HTTPException(400, "preferred_theme muss 'system', 'light' oder 'dark' sein.") if "bio" in fields and len(fields["bio"]) > 300: raise HTTPException(400, "bio darf maximal 300 Zeichen lang sein.") if "wohnort" in fields and len(fields["wohnort"]) > 60: diff --git a/backend/static/index.html b/backend/static/index.html index 7213d86..8b0f233 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -575,10 +575,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 163f8de..d7377de 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '784'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '785'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen @@ -529,6 +529,9 @@ const App = (() => { navigate('onboarding'); } + // Theme aus DB-Profil übernehmen (überschreibt localStorage-Wert) + _applyUserTheme(state.user); + // Drei Welten nach Login starten (falls noch nicht initialisiert) if (window.Worlds) window.Worlds.init(state); @@ -607,6 +610,15 @@ const App = (() => { navigate('welcome', false); } + function _applyUserTheme(user) { + const theme = user?.preferred_theme; + if (!theme || theme === 'system') return; // System-Einstellung: nichts tun + localStorage.setItem('by_theme', theme); + const html = document.documentElement; + if (theme === 'dark') html.setAttribute('data-theme', 'dark'); + else if (theme === 'light') html.setAttribute('data-theme', 'light'); + } + function _showVerifyBanner() { const banner = document.getElementById('verify-banner'); if (!banner) return; diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 07cfab3..b772d0f 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -315,9 +315,9 @@ window.Page_settings = (() => { font-size:var(--text-sm); font-family:inherit; cursor:pointer"> - - - + + + @@ -849,22 +849,22 @@ window.Page_settings = (() => { } }); - document.getElementById('select-theme')?.addEventListener('change', e => { + document.getElementById('select-theme')?.addEventListener('change', async e => { const val = e.target.value; - localStorage.setItem('by_theme', val); + localStorage.setItem('by_theme', val); // lokaler Cache für schnellen Start const html = document.documentElement; - if (val === 'dark') { - html.setAttribute('data-theme', 'dark'); - } else if (val === 'light') { - html.setAttribute('data-theme', 'light'); - } else { - html.removeAttribute('data-theme'); - } + if (val === 'dark') html.setAttribute('data-theme', 'dark'); + else if (val === 'light') html.setAttribute('data-theme', 'light'); + else html.removeAttribute('data-theme'); UI.toast.info( val === 'dark' ? 'Dark Mode aktiviert.' : val === 'light' ? 'Hell-Modus aktiviert.' : 'Theme folgt der Systemeinstellung.' ); + try { + await API.patch('/profile', { preferred_theme: val }); + if (_appState?.user) _appState.user.preferred_theme = val; + } catch { /* ignorieren — localStorage-Fallback greift */ } }); document.getElementById('toggle-pocket-mode')?.addEventListener('change', e => { diff --git a/backend/static/sw.js b/backend/static/sw.js index fbdb8fe..0149aa7 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v784'; +const CACHE_VERSION = 'by-v785'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache