Routen: Detail/Vorschlag-Zoom robust (ResizeObserver) + Navi-Sperrbildschirm nur per Fingerabdruck

Punkt 3 (Zoom auf die Route): feste Timeouts (0/200/500ms) griffen auf iOS oft
zu früh — der Modal-Container war noch nicht final vermessen, die Karte blieb
beim Start-Zoom (zoom 14, center=Start) hängen statt auf die ganze Route zu
zoomen. Jetzt _fitRouteMap mit ResizeObserver: fittet erneut, SOBALD der
Container seine endgültige Größe hat (Detail + Vorschläge). Facade-fitBounds
prüft jetzt auch clientHeight>0 (0-Höhe ergab schlechten Fit).

Punkt 5 (Navigations-Sperrbildschirm): der 2-Sek-Halten-Handler hing am ganzen
Dim-Overlay → Halten IRGENDWO entsperrte. Jetzt ein eigener Fingerabdruck-Knopf
(rk-nav-unlock-btn) wie beim Aufzeichnen-Dim; nur dort entsperrt es, mit
setPointerCapture. Tippen daneben tut bewusst nichts.

Verifiziert (headless): Detail fittet die ganze Route (v1204, 0 Fehler);
Dim-Hintergrund 2,2s halten → bleibt gesperrt, Knopf 2,2s halten → entsperrt.
This commit is contained in:
rene 2026-06-05 14:47:15 +02:00
parent 285928f6f7
commit d203ab17a8
7 changed files with 69 additions and 36 deletions

View file

@ -1 +1 @@
1203
1204

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1203"></script>
<script src="/js/boot-early.js?v=1204"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1203">
<link rel="stylesheet" href="/css/layout.css?v=1203">
<link rel="stylesheet" href="/css/components.css?v=1203">
<link rel="stylesheet" href="/css/utilities.css?v=1203">
<link rel="stylesheet" href="/css/lists.css?v=1203">
<link rel="stylesheet" href="/css/design-system.css?v=1204">
<link rel="stylesheet" href="/css/layout.css?v=1204">
<link rel="stylesheet" href="/css/components.css?v=1204">
<link rel="stylesheet" href="/css/utilities.css?v=1204">
<link rel="stylesheet" href="/css/lists.css?v=1204">
</head>
<body>
@ -617,11 +617,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1203"></script>
<script src="/js/ui.js?v=1203"></script>
<script src="/js/app.js?v=1203"></script>
<script src="/js/worlds.js?v=1203"></script>
<script src="/js/offline-indicator.js?v=1203"></script>
<script src="/js/api.js?v=1204"></script>
<script src="/js/ui.js?v=1204"></script>
<script src="/js/app.js?v=1204"></script>
<script src="/js/worlds.js?v=1204"></script>
<script src="/js/offline-indicator.js?v=1204"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1203"></script>
<script src="/js/boot.js?v=1204"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1203'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1204'; // ← 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;

View file

@ -28,7 +28,8 @@
// Nur fitten wenn Bounds gültig UND der Container eine Größe hat (im Modal
// ist er beim Erstellen 0×0 → fitBounds würde NaN werfen; der Re-Fit nach
// Modal-Animation greift dann).
if (bb && !isNaN(bb.getWest()) && map.getContainer().clientWidth > 0) {
var _c = map.getContainer();
if (bb && !isNaN(bb.getWest()) && _c.clientWidth > 0 && _c.clientHeight > 0) {
var pad = 30;
if (opts && opts.padding) pad = Array.isArray(opts.padding) ? opts.padding[0] : opts.padding;
try { map.fitBounds(bb, { padding: pad, maxZoom: opts && opts.maxZoom, duration: 0 }); } catch (e) {}

View file

@ -617,8 +617,7 @@ window.Page_routes = (() => {
UI.map.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1, weight:2 }).addTo(_suggestMap);
UI.map.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1, weight:2 }).addTo(_suggestMap);
_addRouteArrows(_suggestMap, track, '#3b82f6');
const _sfit = () => { _suggestMap?.invalidateSize(); _suggestMap?.fitBounds(poly.getBounds(), { padding: [16, 16] }); };
_sfit(); setTimeout(_sfit, 200); setTimeout(_sfit, 500);
_fitRouteMap(_suggestMap, mapEl, () => poly.getBounds());
};
_initMap();
@ -1713,14 +1712,25 @@ window.Page_routes = (() => {
stroke-linejoin="round"
stroke-linecap="round"/>
</svg>
<!-- 2-Sek-Halten-Ring -->
<svg width="56" height="56" viewBox="0 0 56 56">
<circle cx="28" cy="28" r="24" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="2.5"/>
<circle id="rk-nav-dim-prog" cx="28" cy="28" r="24" fill="none" stroke="rgba(255,255,255,.7)" stroke-width="2.5"
stroke-dasharray="150.8" stroke-dashoffset="150.8" stroke-linecap="round"
transform="rotate(-90 28 28)" style="transition:none"/>
</svg>
<div style="font-size:11px;opacity:.3;margin-top:8px">2 Sek. halten</div>
<!-- 2-Sek-Halten-Knopf: NUR hier entsperrt sich der Bildschirm (Fingerabdruck).
Tippen irgendwo sonst auf dem Dim-Overlay tut bewusst nichts. -->
<button id="rk-nav-unlock-btn"
style="background:none;border:none;cursor:pointer;outline:none;
display:flex;flex-direction:column;align-items:center;gap:0;
padding:0 16px 16px;-webkit-tap-highlight-color:transparent;
touch-action:none;user-select:none">
<svg width="56" height="56" viewBox="0 0 56 56">
<circle cx="28" cy="28" r="24" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="2.5"/>
<circle id="rk-nav-dim-prog" cx="28" cy="28" r="24" fill="none" stroke="rgba(255,255,255,.7)" stroke-width="2.5"
stroke-dasharray="150.8" stroke-dashoffset="150.8" stroke-linecap="round"
transform="rotate(-90 28 28)" style="transition:none"/>
</svg>
<!-- Fingerabdruck, inline path (kein <use> wegen iOS-Bug) -->
<svg viewBox="0 0 256 256" width="28" height="28" fill="white" style="margin-top:12px;opacity:0.5">
<path d="M126.42,24C70.73,24.85,25.21,70.09,24,125.81a103.53,103.53,0,0,0,13.52,53.54,4,4,0,0,0,7.1-.3,119.35,119.35,0,0,0,11.37-51A71.77,71.77,0,0,1,83,71.83a8,8,0,1,1,9.86,12.61A55.82,55.82,0,0,0,72,128.07a135.28,135.28,0,0,1-18.45,68.35,4,4,0,0,0,.61,4.85c2,2,4.09,4,6.25,5.82a4,4,0,0,0,6-1A151.18,151.18,0,0,0,85,158.49a8,8,0,1,1,15.68,3.19,167.33,167.33,0,0,1-21.07,53.64,4,4,0,0,0,1.6,5.63c2.47,1.25,5,2.41,7.57,3.47a4,4,0,0,0,5-1.61A183,183,0,0,0,120,128.28a8.16,8.16,0,0,1,7.44-8.21,8,8,0,0,1,8.56,8,198.94,198.94,0,0,1-25.21,97.16,4,4,0,0,0,2.95,5.92q4.55.63,9.21.86a4,4,0,0,0,3.67-2.1A214.88,214.88,0,0,0,152,128.8c.05-13.25-10.3-24.49-23.54-24.74A24,24,0,0,0,104,128a8.1,8.1,0,0,1-7.29,8,8,8,0,0,1-8.71-8,40,40,0,0,1,40.42-40c22,.23,39.68,19.17,39.57,41.16a231.37,231.37,0,0,1-20.52,94.57,4,4,0,0,0,4.62,5.51,103.49,103.49,0,0,0,10.26-3,4,4,0,0,0,2.35-2.22,243.76,243.76,0,0,0,11.48-34,8,8,0,1,1,15.5,4q-1.12,4.37-2.4,8.7a4,4,0,0,0,6.46,4.17A104,104,0,0,0,126.42,24ZM198,161.08a8,8,0,0,1-7.92,7,8.39,8.39,0,0,1-1-.06,8,8,0,0,1-6.95-8.93,252.57,252.57,0,0,0,1.92-31,56.08,56.08,0,0,0-56-56,56.78,56.78,0,0,0-7,.43,8,8,0,0,1-2-15.89,72.1,72.1,0,0,1,81,71.49A266.93,266.93,0,0,1,198,161.08Z"/>
</svg>
<div style="font-size:11px;opacity:.3;margin-top:6px">2 Sek. halten</div>
</button>
</div>
`;
document.body.appendChild(ovl);
@ -1936,23 +1946,32 @@ window.Page_routes = (() => {
_navResetInactTimer();
const dim = document.getElementById('rk-nav-dim');
// Entsperren reagiert NUR auf den Fingerabdruck-Knopf (2 Sek. halten) — nicht mehr
// auf das ganze Dim-Overlay. Tippen daneben lässt den Bildschirm bewusst gedimmt.
const navUnlock = document.getElementById('rk-nav-unlock-btn');
let _lpTimer = null;
const cancelLp = () => {
clearTimeout(_lpTimer);
const prog = document.getElementById('rk-nav-dim-prog');
if (prog) { prog.style.transition = 'none'; prog.style.strokeDashoffset = '150.8'; }
};
dim.addEventListener('pointerdown', e => {
navUnlock.addEventListener('pointerdown', e => {
e.stopPropagation();
try { navUnlock.setPointerCapture(e.pointerId); } catch (err) {}
const prog = document.getElementById('rk-nav-dim-prog');
if (prog) { prog.style.transition = 'stroke-dashoffset 2s linear'; prog.style.strokeDashoffset = '0'; }
_lpTimer = setTimeout(() => {
dim.style.display = 'none'; _navDimmed = false; _navResetInactTimer();
}, 2000);
});
dim.addEventListener('pointerup', cancelLp);
dim.addEventListener('pointercancel', cancelLp);
dim.addEventListener('pointerleave', cancelLp);
navUnlock.addEventListener('pointerup', cancelLp);
navUnlock.addEventListener('pointercancel', cancelLp);
// Verlässt der Finger den Knopf während des Haltens → abbrechen (sonst entsperrt
// ein wegrutschender Finger weiter). pointerleave reicht dank setPointerCapture.
navUnlock.addEventListener('pointerleave', cancelLp);
// Sicherheitsnetz: ein Tipp aufs Dim-Overlay (nicht auf den Knopf) tut nichts,
// aber wir schlucken ihn, damit darunterliegende Buttons nicht reagieren.
dim.addEventListener('pointerdown', e => { if (e.target === dim) e.stopPropagation(); });
// Aktions-Buttons
document.getElementById('rk-nav-back').addEventListener('click', _closeNav);
@ -2655,6 +2674,23 @@ window.Page_routes = (() => {
}
}
// Karte robust auf die ganze Route fitten — auch wenn der Container beim Erstellen
// noch 0×0 ist (Modal-Animation / spätes Layout auf iOS). Feste Timeouts greifen dort
// oft zu früh; der ResizeObserver fittet erneut, SOBALD der Container seine endgültige
// Größe hat. Das war der Grund, warum die Detail-/Vorschlag-Karte auf dem Gerät beim
// Start-Zoom (zoom 14, center=Start) hängen blieb statt auf die Route zu zoomen.
function _fitRouteMap(m, el, getBounds, opts) {
opts = opts || { padding: [16, 16] };
const fit = () => { try { m.invalidateSize(); m.fitBounds(getBounds(), opts); } catch (e) {} };
fit();
setTimeout(fit, 150); setTimeout(fit, 400);
if (window.ResizeObserver && el) {
const ro = new ResizeObserver(() => { if (el.clientWidth > 0 && el.clientHeight > 0) fit(); });
ro.observe(el);
setTimeout(() => { try { ro.disconnect(); } catch (e) {} }, 4000);
}
}
async function _buildDetailMap(el, track) {
const lls = track.map(p => [p.lat, p.lon]);
const m = await UI.map.create(el, {
@ -2665,11 +2701,7 @@ window.Page_routes = (() => {
_addRouteArrows(m, track, '#3b82f6');
UI.map.circleMarker(lls[0], { radius:7, color:'#22C55E', fillColor:'#22C55E', fillOpacity:1 }).addTo(m);
UI.map.circleMarker(lls.at(-1), { radius:7, color:'#EF4444', fillColor:'#EF4444', fillOpacity:1 }).addTo(m);
// Mehrfach fitten: beim Erstellen ist der Modal-Container evtl. noch 0×0 → nach
// der Animation (resize) erneut auf die ganze Route zoomen.
const _fit = () => { m.invalidateSize(); m.fitBounds(poly.getBounds(), { padding:[16,16] }); };
_fit();
setTimeout(_fit, 200); setTimeout(_fit, 500);
_fitRouteMap(m, el, () => poly.getBounds());
return m;
}

View file

@ -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=1203"></script>
<script src="/js/landing-init.js?v=1204"></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">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1203';
const VER = '1204';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten