Navi-Sounds: 2x Wuff = links, 1x Wuff = rechts, Klaeffen = falscher Weg (Idee Rene)
- NavSound (routes.js): WebAudio-Wuff-Synthese (Saegezahn-Sweep + Tiefpass, kein Asset, laeuft offline); echte Aufnahmen unter /sounds/wuff.mp3 + klaeffen.mp3 (z.B. von Yaro) werden automatisch bevorzugt - Abbiege-Erkennung: Turns einmalig aus dem Track (Peilung ueber 15-m- Stuetzpunkte gegen GPS-Zickzack, >=40 Grad), Ansage bei <=45 m davor, einmal pro Abbiegepunkt - Falscher Weg (>50 m, bestehende Warnung): Klaeffen beim Abkommen + Erinnerung alle 30 s; zurueck auf Route reset - Toggle im Navi-Header (Lautsprecher-Icon, by_nav_sound, Default AN), iOS-Audio-Unlock beim Navi-Start + Toggle (Probe-Wuff als Bestaetigung) Bump v1241
This commit is contained in:
parent
a1b2644cce
commit
ca23b3ec46
6 changed files with 153 additions and 16 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1240
|
||||
1241
|
||||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1240"></script>
|
||||
<script src="/js/boot-early.js?v=1241"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1240">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1241">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1241">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1241">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1241">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1241">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -612,11 +612,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1240"></script>
|
||||
<script src="/js/ui.js?v=1240"></script>
|
||||
<script src="/js/app.js?v=1240"></script>
|
||||
<script src="/js/worlds.js?v=1240"></script>
|
||||
<script src="/js/offline-indicator.js?v=1240"></script>
|
||||
<script src="/js/api.js?v=1241"></script>
|
||||
<script src="/js/ui.js?v=1241"></script>
|
||||
<script src="/js/app.js?v=1241"></script>
|
||||
<script src="/js/worlds.js?v=1241"></script>
|
||||
<script src="/js/offline-indicator.js?v=1241"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -626,7 +626,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1240"></script>
|
||||
<script src="/js/boot.js?v=1241"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1240'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1241'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
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_VERSION = APP_VERSION;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,81 @@ window.Page_routes = (() => {
|
|||
let _navCompassHeading = null;
|
||||
let _navHeadingSmoothed = null;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NAVI-SOUNDS (Idee René 2026-06-06): links = 2× Wuff, rechts = 1× Wuff,
|
||||
// falscher Weg = Kläffen. WebAudio-Synthese (kein Asset, läuft offline) —
|
||||
// liegen echte Aufnahmen unter /sounds/wuff.mp3 + /sounds/klaeffen.mp3
|
||||
// (z.B. von Yaro 🐕), werden DIE bevorzugt. iOS: Audio braucht eine User-Geste
|
||||
// → unlock() beim Navi-Start/Toggle.
|
||||
// ----------------------------------------------------------
|
||||
const NavSound = (() => {
|
||||
let ctx = null;
|
||||
let files = null; // { wuff, klaeffen } HTMLAudio | leer = Synthese
|
||||
const enabled = () => { try { return localStorage.getItem('by_nav_sound') !== '0'; } catch (e) { return true; } };
|
||||
|
||||
function _ctx() {
|
||||
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (ctx.state === 'suspended') ctx.resume().catch(() => {});
|
||||
return ctx;
|
||||
}
|
||||
// Ein synthetischer „Wuff": Sägezahn-Sweep durch Tiefpass, kurzer Attack, schneller Decay.
|
||||
function _wuff(at, pitch = 1) {
|
||||
const c = _ctx(), t = c.currentTime + at;
|
||||
const o = c.createOscillator(), g = c.createGain(), f = c.createBiquadFilter();
|
||||
o.type = 'sawtooth';
|
||||
o.frequency.setValueAtTime(240 * pitch, t);
|
||||
o.frequency.exponentialRampToValueAtTime(75 * pitch, t + 0.16);
|
||||
f.type = 'lowpass'; f.frequency.value = 900 * pitch; f.Q.value = 3;
|
||||
g.gain.setValueAtTime(0.0001, t);
|
||||
g.gain.exponentialRampToValueAtTime(0.9, t + 0.02);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
|
||||
o.connect(f); f.connect(g); g.connect(c.destination);
|
||||
o.start(t); o.stop(t + 0.25);
|
||||
}
|
||||
function _barks(n, pitch, gap) {
|
||||
if (!enabled()) return;
|
||||
const sample = files && (pitch > 1.3 ? files.klaeffen : files.wuff);
|
||||
if (sample) { // echte Aufnahme: n-mal hintereinander
|
||||
let i = 0;
|
||||
const play = () => {
|
||||
if (i++ >= n) return;
|
||||
sample.currentTime = 0;
|
||||
sample.play().catch(() => {});
|
||||
setTimeout(play, gap * 1000 + 200);
|
||||
};
|
||||
play();
|
||||
return;
|
||||
}
|
||||
try { for (let i = 0; i < n; i++) _wuff(i * gap, pitch); } catch (e) {}
|
||||
}
|
||||
return {
|
||||
enabled,
|
||||
unlock() { // in User-Geste aufrufen (iOS-Autoplay-Policy)
|
||||
try {
|
||||
const c = _ctx();
|
||||
const b = c.createBuffer(1, 1, 22050), s = c.createBufferSource();
|
||||
s.buffer = b; s.connect(c.destination); s.start(0);
|
||||
} catch (e) {}
|
||||
// Echte Samples einmalig anfragen (404 → Synthese bleibt)
|
||||
if (files === null) {
|
||||
files = {};
|
||||
['wuff', 'klaeffen'].forEach(name => {
|
||||
const a = new Audio(`/sounds/${name}.mp3`);
|
||||
a.preload = 'auto';
|
||||
a.addEventListener('canplaythrough', () => { files[name] = a; }, { once: true });
|
||||
a.addEventListener('error', () => {}, { once: true });
|
||||
});
|
||||
}
|
||||
},
|
||||
links() { _barks(2, 1.0, 0.30); }, // 2× Wuff
|
||||
rechts() { _barks(1, 1.0, 0.30); }, // 1× Wuff
|
||||
klaeffen() { _barks(4, 1.7, 0.16); }, // schnelles, höheres Bellen
|
||||
};
|
||||
})();
|
||||
let _navSndAnnouncedIdx = -1; // bis zu welchem Track-Index Abbiegungen angesagt wurden
|
||||
let _navSndOffRoute = false; // Off-Route-Zustand (Kläffen beim Eintritt + alle 30 s)
|
||||
let _navSndLastKlaeff = 0;
|
||||
|
||||
// Recording-Overlay state
|
||||
let _recOvl = null, _recMap = null;
|
||||
let _recFollow = true; // Karte folgt dem Standort bei Aufzeichnung (Drag pausiert)
|
||||
|
|
@ -1670,6 +1745,12 @@ window.Page_routes = (() => {
|
|||
const track = route.gps_track || [];
|
||||
if (track.length < 2) return;
|
||||
|
||||
// Navi-Sounds: Audio in der User-Geste freischalten (iOS) + Ansage-Status zurücksetzen
|
||||
NavSound.unlock();
|
||||
_navSndAnnouncedIdx = -1;
|
||||
_navSndOffRoute = false;
|
||||
_navSndLastKlaeff = 0;
|
||||
|
||||
_navMaxIdx = 0;
|
||||
_navRecorded = false;
|
||||
_navLastBearing = null;
|
||||
|
|
@ -1689,6 +1770,7 @@ window.Page_routes = (() => {
|
|||
background:var(--c-surface);border-bottom:1px solid var(--c-border);flex-shrink:0">
|
||||
<button id="rk-nav-back" style="background:none;border:none;color:var(--c-primary);font-size:16px;cursor:pointer;padding:4px 0">← Zurück</button>
|
||||
<span style="font-weight:700;font-size:14px;flex:1;text-align:center;margin:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(route.name)}</span>
|
||||
<button id="rk-nav-sound-btn" style="background:none;border:none;color:${NavSound.enabled() ? 'var(--c-primary)' : 'var(--c-text-secondary)'};cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Navi-Sounds (2× Wuff = links, 1× = rechts, Kläffen = falscher Weg)">${UI.icon(NavSound.enabled() ? 'speaker-high' : 'speaker-none')}</button>
|
||||
<button id="rk-nav-center-btn" style="background:none;border:none;color:var(--c-primary);cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Auf Standort zentrieren">${UI.icon('crosshair')}</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -1894,6 +1976,27 @@ window.Page_routes = (() => {
|
|||
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
|
||||
};
|
||||
|
||||
// Abbiegepunkte EINMALIG aus dem Track ableiten: Peilung über ~15-m-Stützpunkte
|
||||
// (sonst macht GPS-Zickzack aus jeder Geraden eine Kurve), Richtungsänderung ≥ 40°
|
||||
// = Abbiegung, > 0 = rechts. Mindestabstand zwischen Ansagen ~25 m.
|
||||
const _navTurns = (() => {
|
||||
const out = [];
|
||||
const distM = (a, b) => _haversineKm(a.lat, a.lon, b.lat, b.lon) * 1000;
|
||||
let lastIdx = -1;
|
||||
for (let i = 1; i < track.length - 1; i++) {
|
||||
let p = i - 1, accP = distM(track[p], track[i]);
|
||||
while (p > 0 && accP < 15) { p--; accP += distM(track[p], track[p + 1]); }
|
||||
let n = i + 1, accN = distM(track[i], track[n]);
|
||||
while (n < track.length - 1 && accN < 15) { n++; accN += distM(track[n - 1], track[n]); }
|
||||
const d = ((_bearingTo(track[i], track[n]) - _bearingTo(track[p], track[i]) + 540) % 360) - 180;
|
||||
if (Math.abs(d) >= 40 && (lastIdx < 0 || distM(track[lastIdx], track[i]) > 25)) {
|
||||
out.push({ idx: i, right: d > 0 });
|
||||
lastIdx = i;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
|
||||
const _updateStats = (idx, distToRoute, userLat, userLon) => {
|
||||
if (idx > _navMaxIdx) {
|
||||
_navMaxIdx = idx;
|
||||
|
|
@ -1924,12 +2027,33 @@ window.Page_routes = (() => {
|
|||
_navLastBearing = _bearingTo({ lat: userLat, lon: userLon }, track[idx + 1]);
|
||||
_updateDimArrow();
|
||||
}
|
||||
// Abbiege-Ansage (René: 2× Wuff = links, 1× = rechts): nächster Turn vor uns,
|
||||
// angesagt sobald ≤ 45 m entfernt — einmal pro Abbiegepunkt.
|
||||
if (userLat != null && distToRoute < 0.1) {
|
||||
const next = _navTurns.find(t => t.idx > idx && t.idx > _navSndAnnouncedIdx);
|
||||
if (next) {
|
||||
const dM = _haversineKm(userLat, userLon, track[next.idx].lat, track[next.idx].lon) * 1000;
|
||||
if (dM <= 45) {
|
||||
_navSndAnnouncedIdx = next.idx;
|
||||
if (next.right) NavSound.rechts(); else NavSound.links();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const offWarn = document.getElementById('rk-nav-offwarn');
|
||||
if (distToRoute * 1000 > 50) {
|
||||
offWarn.style.display = '';
|
||||
if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
|
||||
// Falscher Weg = Kläffen (beim Abkommen + Erinnerung alle 30 s)
|
||||
const _now = Date.now();
|
||||
if (!_navSndOffRoute || _now - _navSndLastKlaeff > 30000) {
|
||||
_navSndOffRoute = true;
|
||||
_navSndLastKlaeff = _now;
|
||||
NavSound.klaeffen();
|
||||
}
|
||||
} else {
|
||||
offWarn.style.display = 'none';
|
||||
_navSndOffRoute = false;
|
||||
}
|
||||
// Polylines aktualisieren
|
||||
doneLine.setLatLngs(track.slice(0, idx + 1).map(p => [p.lat, p.lon]));
|
||||
|
|
@ -2057,6 +2181,19 @@ window.Page_routes = (() => {
|
|||
if (locMarker) _navMap.setView(locMarker.getLatLng(), 16);
|
||||
});
|
||||
|
||||
// Navi-Sounds an/aus (Klick = User-Geste → unlock + Probe-Wuff als Bestätigung)
|
||||
document.getElementById('rk-nav-sound-btn')?.addEventListener('click', e => {
|
||||
const on = !NavSound.enabled();
|
||||
try { localStorage.setItem('by_nav_sound', on ? '1' : '0'); } catch (err) {}
|
||||
const btn = e.currentTarget;
|
||||
btn.style.color = on ? 'var(--c-primary)' : 'var(--c-text-secondary)';
|
||||
btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${on ? 'speaker-high' : 'speaker-none'}`);
|
||||
if (on) { NavSound.unlock(); NavSound.rechts(); }
|
||||
UI.toast.info(on
|
||||
? 'Navi-Sounds an: 2× Wuff = links, 1× Wuff = rechts, Kläffen = falscher Weg 🐕'
|
||||
: 'Navi-Sounds aus.');
|
||||
});
|
||||
|
||||
document.getElementById('rk-nav-pois')?.addEventListener('click', () => {
|
||||
if (!_navPois.length) { UI.toast.info('Keine POIs entlang dieser Route.'); return; }
|
||||
const byType = {};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1240"></script>
|
||||
<script src="/js/landing-init.js?v=1241"></script>
|
||||
<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, ohne App Store.">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
============================================================ */
|
||||
|
||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||
const VER = '1240';
|
||||
const VER = '1241';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue