Ban Yaro Blues — Hymne in der WELT-Welt
Eigener Song (KI-Demo via Suno) als Marken-Hymne. Dezente Player-Karte unter dem Tageszitat; preload=none → 6 MB MP3 lädt erst bei Play, der SW cacht sie danach für offline. Der Banner ist einmalige Einladung und verschwindet nach erstem Hören (durchgehört oder >30s + Pause); danach dezenter runder Play-Button unten links als Gegenspieler zum FAB, nur in WELT. Audio-Element zentral in index.html → übersteht Welt-Wechsel & Re-Renders. „Gehört" wird hybrid gemerkt: localStorage (sofort/offline) + DB-Flag anthem_heard am User (neue Spalte, über /auth/me, gesetzt via POST /api/profile/anthem-heard) — geräte- und deploy-übergreifend, damit der Banner nicht erneut nervt.
This commit is contained in:
parent
f7370028da
commit
d0a76e1b54
11 changed files with 149 additions and 18 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
1292
|
1295
|
||||||
|
|
@ -605,6 +605,7 @@ def _migrate(conn_factory):
|
||||||
("notes", "location_name", "TEXT"),
|
("notes", "location_name", "TEXT"),
|
||||||
("notes", "parent_label", "TEXT"),
|
("notes", "parent_label", "TEXT"),
|
||||||
("users", "notes_ki_enabled", "INTEGER NOT NULL DEFAULT 1"),
|
("users", "notes_ki_enabled", "INTEGER NOT NULL DEFAULT 1"),
|
||||||
|
("users", "anthem_heard", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
# Züchter-Rolle
|
# Züchter-Rolle
|
||||||
("users", "breeder_status", "TEXT"),
|
("users", "breeder_status", "TEXT"),
|
||||||
# Würfe: Verknüpfung mit Zuchtkartei-Hunden + Welfare
|
# Würfe: Verknüpfung mit Zuchtkartei-Hunden + Welfare
|
||||||
|
|
|
||||||
|
|
@ -398,7 +398,7 @@ async def me(user=Depends(get_current_user)):
|
||||||
bio, wohnort, erfahrung, social_link,
|
bio, wohnort, erfahrung, social_link,
|
||||||
profil_sichtbarkeit, avatar_url, created_at,
|
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,
|
notes_ki_enabled, gassi_stunde_push, anthem_heard,
|
||||||
preferred_theme, subscription_tier,
|
preferred_theme, subscription_tier,
|
||||||
subscription_expires_at, subscription_cancelled_at, needs_dog_selection,
|
subscription_expires_at, subscription_cancelled_at, needs_dog_selection,
|
||||||
billing_address, geburtstag
|
billing_address, geburtstag
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,16 @@ def _load_user(user_id: int) -> dict:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/anthem-heard", status_code=204)
|
||||||
|
async def mark_anthem_heard(user=Depends(get_current_user)):
|
||||||
|
"""Merkt server-seitig, dass die Hymne gehört wurde — geräteübergreifend und
|
||||||
|
übersteht Cache-Clear bzw. die iOS-Trennung von PWA- und Safari-localStorage,
|
||||||
|
damit der Banner nach einem Deploy nicht erneut auftaucht."""
|
||||||
|
with db() as conn:
|
||||||
|
conn.execute("UPDATE users SET anthem_heard=1 WHERE id=?", (user["id"],))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@router.patch("")
|
@router.patch("")
|
||||||
async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)):
|
async def update_profile(data: ProfileUpdate, user=Depends(get_current_user)):
|
||||||
fields = data.model_dump(exclude_none=True)
|
fields = data.model_dump(exclude_none=True)
|
||||||
|
|
|
||||||
|
|
@ -8267,6 +8267,29 @@ svg.empty-state-icon {
|
||||||
}
|
}
|
||||||
#worlds-fab:active { transform: scale(0.92); box-shadow: 0 2px 10px rgba(196,132,58,0.3); }
|
#worlds-fab:active { transform: scale(0.92); box-shadow: 0 2px 10px rgba(196,132,58,0.3); }
|
||||||
|
|
||||||
|
/* Hymne-Button — Gegenspieler zum FAB, unten links, nur WELT + nach erstem Hören */
|
||||||
|
#worlds-anthem {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(env(safe-area-inset-bottom, 16px) + 16px);
|
||||||
|
left: 20px;
|
||||||
|
width: 54px; height: 54px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(22,26,30,0.66);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid rgba(255,255,255,0.20);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 60;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 18px rgba(0,0,0,0.40);
|
||||||
|
transition: transform 0.12s, background 0.2s;
|
||||||
|
}
|
||||||
|
#worlds-anthem:active { transform: scale(0.92); }
|
||||||
|
#worlds-anthem.playing { background: var(--c-primary); border-color: transparent; }
|
||||||
|
|
||||||
/* Header + Bottom-Nav: vollständig entfernt — Welten übernehmen Navigation */
|
/* Header + Bottom-Nav: vollständig entfernt — Welten übernehmen Navigation */
|
||||||
#app-header { display: none !important; }
|
#app-header { display: none !important; }
|
||||||
#bottom-nav { display: none !important; }
|
#bottom-nav { display: none !important; }
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,14 @@
|
||||||
<title>Ban Yaro</title>
|
<title>Ban Yaro</title>
|
||||||
|
|
||||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||||
<script src="/js/boot-early.js?v=1292"></script>
|
<script src="/js/boot-early.js?v=1295"></script>
|
||||||
|
|
||||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||||
<link rel="stylesheet" href="/css/design-system.css?v=1292">
|
<link rel="stylesheet" href="/css/design-system.css?v=1295">
|
||||||
<link rel="stylesheet" href="/css/layout.css?v=1292">
|
<link rel="stylesheet" href="/css/layout.css?v=1295">
|
||||||
<link rel="stylesheet" href="/css/components.css?v=1292">
|
<link rel="stylesheet" href="/css/components.css?v=1295">
|
||||||
<link rel="stylesheet" href="/css/utilities.css?v=1292">
|
<link rel="stylesheet" href="/css/utilities.css?v=1295">
|
||||||
<link rel="stylesheet" href="/css/lists.css?v=1292">
|
<link rel="stylesheet" href="/css/lists.css?v=1295">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -596,6 +596,10 @@
|
||||||
<div class="world-panel" id="wp-hund"><div id="wh-content"></div></div>
|
<div class="world-panel" id="wp-hund"><div id="wh-content"></div></div>
|
||||||
<div class="world-panel" id="wp-welt"><div id="ww-content"></div></div>
|
<div class="world-panel" id="wp-welt"><div id="ww-content"></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="worlds-anthem" class="hidden" aria-label="Hymne abspielen" title="Ban Yaro Blues — unsere Hymne">
|
||||||
|
<svg class="ph-icon" aria-hidden="true" style="width:22px;height:22px"><use href="/icons/phosphor.svg#play"></use></svg>
|
||||||
|
</button>
|
||||||
|
<audio id="anthem-audio" src="/sounds/ban-yaro-blues.mp3" preload="none"></audio>
|
||||||
<button id="worlds-fab" aria-label="Hinzufügen">
|
<button id="worlds-fab" aria-label="Hinzufügen">
|
||||||
<svg class="offline-paw" viewBox="0 0 256 256" aria-hidden="true" style="width:24px;height:24px">
|
<svg class="offline-paw" viewBox="0 0 256 256" aria-hidden="true" style="width:24px;height:24px">
|
||||||
<!-- 5 Sub-Pfade einzeln einfärbbar via .paw-elem; Default: weiß auf orange -->
|
<!-- 5 Sub-Pfade einzeln einfärbbar via .paw-elem; Default: weiß auf orange -->
|
||||||
|
|
@ -620,12 +624,12 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=1292"></script>
|
<script src="/js/api.js?v=1295"></script>
|
||||||
<script src="/js/ui.js?v=1292"></script>
|
<script src="/js/ui.js?v=1295"></script>
|
||||||
<script src="/js/app.js?v=1292"></script>
|
<script src="/js/app.js?v=1295"></script>
|
||||||
<script src="/js/worlds.js?v=1292"></script>
|
<script src="/js/worlds.js?v=1295"></script>
|
||||||
<script src="/js/offline-indicator.js?v=1292"></script>
|
<script src="/js/offline-indicator.js?v=1295"></script>
|
||||||
<script src="/js/contact-form.js?v=1292"></script>
|
<script src="/js/contact-form.js?v=1295"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
@ -635,7 +639,7 @@
|
||||||
|
|
||||||
|
|
||||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||||
<script src="/js/boot.js?v=1292"></script>
|
<script src="/js/boot.js?v=1295"></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '1292'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '1295'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||||
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
||||||
window.APP_VERSION = APP_VERSION;
|
window.APP_VERSION = APP_VERSION;
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ window.Worlds = (() => {
|
||||||
track.style.transform = `translateX(${-_cur * (100 / 3)}%)`;
|
track.style.transform = `translateX(${-_cur * (100 / 3)}%)`;
|
||||||
_updateDots();
|
_updateDots();
|
||||||
_updateFab();
|
_updateFab();
|
||||||
|
_anthem.updateButton();
|
||||||
// Karte neu rendern nachdem Transition abgeschlossen
|
// Karte neu rendern nachdem Transition abgeschlossen
|
||||||
if (_cur === 2 && _map) {
|
if (_cur === 2 && _map) {
|
||||||
setTimeout(() => _map.invalidateSize(), animated ? 380 : 50);
|
setTimeout(() => _map.invalidateSize(), animated ? 380 : 50);
|
||||||
|
|
@ -1933,6 +1934,83 @@ window.Worlds = (() => {
|
||||||
{ t:"Ein Hund braucht keinen Kalender, er weiß genau, wann du nach Hause kommst.", a:"Unbekannt" },
|
{ t:"Ein Hund braucht keinen Kalender, er weiß genau, wann du nach Hause kommst.", a:"Unbekannt" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── HYMNE (Ban Yaro Blues) ───────────────────────────────────
|
||||||
|
// Banner in der WELT-Welt lädt zum ersten Hören ein und verschwindet danach.
|
||||||
|
// Ab dann: dezenter runder Play-Button unten links (Gegenspieler zum FAB).
|
||||||
|
// Audio-Element lebt zentral in index.html → übersteht Re-Renders & Welt-Wechsel.
|
||||||
|
const _anthem = (() => {
|
||||||
|
const KEY = 'by_anthem_heard';
|
||||||
|
let _bound = false;
|
||||||
|
const _audio = () => document.getElementById('anthem-audio');
|
||||||
|
// Gehört? Server-Flag (geräteübergreifend, deploy-fest) ODER lokal (sofort/offline).
|
||||||
|
const heard = () => {
|
||||||
|
if (_state?.user?.anthem_heard) return true;
|
||||||
|
try { return localStorage.getItem(KEY) === '1'; } catch (_) { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
function _markHeard() {
|
||||||
|
try { localStorage.setItem(KEY, '1'); } catch (_) {}
|
||||||
|
if (!_state?.user?.anthem_heard) { // server-seitig genau einmal merken
|
||||||
|
if (_state?.user) _state.user.anthem_heard = 1;
|
||||||
|
try { API.post('/profile/anthem-heard', {}).catch(() => {}); } catch (_) {}
|
||||||
|
}
|
||||||
|
document.getElementById('ww-anthem-card')?.classList.add('hidden'); // Banner live ausblenden
|
||||||
|
updateButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _sync() {
|
||||||
|
const a = _audio();
|
||||||
|
const playing = !!(a && !a.paused && !a.ended);
|
||||||
|
document.getElementById('ww-anthem-icon')?.querySelector('use')
|
||||||
|
?.setAttribute('href', `/icons/phosphor.svg#${playing ? 'pause' : 'play'}`);
|
||||||
|
const sub = document.getElementById('ww-anthem-sub');
|
||||||
|
if (sub) sub.textContent = playing ? 'läuft … (tippen zum Pausieren)' : 'unsere Hymne · anhören';
|
||||||
|
const btn = document.getElementById('worlds-anthem');
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle('playing', playing);
|
||||||
|
btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${playing ? 'pause' : 'play'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindAudio() {
|
||||||
|
if (_bound) return;
|
||||||
|
const a = _audio();
|
||||||
|
if (!a) return;
|
||||||
|
_bound = true;
|
||||||
|
a.addEventListener('play', _sync);
|
||||||
|
a.addEventListener('pause', () => { if (a.currentTime >= 30) _markHeard(); _sync(); });
|
||||||
|
a.addEventListener('ended', () => { _markHeard(); _sync(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
const a = _audio();
|
||||||
|
if (!a) return;
|
||||||
|
if (a.paused) a.play().catch(() => {});
|
||||||
|
else a.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButton() {
|
||||||
|
document.getElementById('worlds-anthem')?.classList.toggle('hidden', !(_cur === 2 && heard()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bei jedem WELT-Render aufrufen: Audio-Listener sichern, Klicks binden, Status setzen.
|
||||||
|
function initWelt() {
|
||||||
|
_bindAudio();
|
||||||
|
const card = document.getElementById('ww-anthem-card');
|
||||||
|
if (card && !card._anthemBound) {
|
||||||
|
card._anthemBound = true;
|
||||||
|
card.addEventListener('click', toggle);
|
||||||
|
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
|
||||||
|
}
|
||||||
|
const btn = document.getElementById('worlds-anthem');
|
||||||
|
if (btn && !btn._anthemBound) { btn._anthemBound = true; btn.addEventListener('click', toggle); }
|
||||||
|
_sync();
|
||||||
|
updateButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { heard, toggle, updateButton, initWelt };
|
||||||
|
})();
|
||||||
|
|
||||||
function _renderWelt() {
|
function _renderWelt() {
|
||||||
const el = document.getElementById('ww-content');
|
const el = document.getElementById('ww-content');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
@ -1959,6 +2037,19 @@ window.Worlds = (() => {
|
||||||
— ${_esc(quote.a)}
|
— ${_esc(quote.a)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${_anthem.heard() ? '' : `
|
||||||
|
<div class="world-info-card" id="ww-anthem-card" role="button" tabindex="0"
|
||||||
|
style="margin-top:10px;display:flex;align-items:center;gap:14px;cursor:pointer">
|
||||||
|
<div style="width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,0.12);
|
||||||
|
display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||||
|
<svg class="ph-icon" id="ww-anthem-icon" style="width:20px;height:20px;color:#fff" aria-hidden="true"><use href="/icons/phosphor.svg#play"></use></svg>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-size:var(--text-sm);font-weight:600;color:#fff">Ban Yaro Blues</div>
|
||||||
|
<div id="ww-anthem-sub" style="font-size:11px;color:rgba(255,255,255,0.45);margin-top:2px">unsere Hymne · anhören</div>
|
||||||
|
</div>
|
||||||
|
<svg class="ph-icon" style="width:15px;height:15px;color:rgba(255,255,255,0.3);flex-shrink:0" aria-hidden="true"><use href="/icons/phosphor.svg#music-notes"></use></svg>
|
||||||
|
</div>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="world-bottom">
|
<div class="world-bottom">
|
||||||
<div class="world-chips-grid">
|
<div class="world-chips-grid">
|
||||||
|
|
@ -1972,6 +2063,8 @@ window.Worlds = (() => {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav)));
|
el.querySelectorAll('[data-wnav]').forEach(e => e.addEventListener('click', () => navigateTo(e.dataset.wnav)));
|
||||||
|
|
||||||
|
_anthem.initWelt();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HELPERS ──────────────────────────────────────────────────
|
// ── HELPERS ──────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<script src="/js/landing-init.js?v=1292"></script>
|
<script src="/js/landing-init.js?v=1295"></script>
|
||||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, direkt im Browser oder als native iPhone-App (Ban Yaro Go).">
|
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, direkt im Browser oder als native iPhone-App (Ban Yaro Go).">
|
||||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||||
|
|
|
||||||
BIN
backend/static/sounds/ban-yaro-blues.mp3
Normal file
BIN
backend/static/sounds/ban-yaro-blues.mp3
Normal file
Binary file not shown.
|
|
@ -4,7 +4,7 @@
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||||
const VER = '1292';
|
const VER = '1295';
|
||||||
const CACHE_VERSION = `by-v${VER}`;
|
const CACHE_VERSION = `by-v${VER}`;
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue