Compare commits
2 commits
8e623d8909
...
e2c75f04bc
| Author | SHA1 | Date | |
|---|---|---|---|
| e2c75f04bc | |||
| c5bdad2d86 |
10 changed files with 130 additions and 48 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1222
|
||||
1223
|
||||
|
|
@ -7294,14 +7294,25 @@ svg.empty-state-icon {
|
|||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
background: var(--c-text-secondary, #6b7280);
|
||||
color: #fff;
|
||||
font-size: var(--text-sm);
|
||||
background: #1f2937;
|
||||
color: #f3f4f6;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 7px) 16px 7px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.3);
|
||||
pointer-events: none;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
/* Eingeklappt (5s nach Offline-Gang, boot.js): schmale Icon-Leiste statt 2-Zeilen-Banner —
|
||||
das volle Banner verdeckte die Karten-Steuerung oben (Gerätetest iOS 2026-06-06). */
|
||||
#offline-banner.collapsed {
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 2px) 16px 2px;
|
||||
}
|
||||
#offline-banner.collapsed #offline-banner-text { display: none; }
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
STREAK-WIDGET (Welcome-Seite)
|
||||
|
|
|
|||
|
|
@ -86,24 +86,19 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1222"></script>
|
||||
<script src="/js/boot-early.js?v=1223"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1222">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1222">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1222">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1222">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1222">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1223">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1223">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1223">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1223">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1223">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Offline-Banner -->
|
||||
<div id="offline-banner" aria-live="polite"
|
||||
style="display:none;position:fixed;top:0;left:0;right:0;z-index:9999;
|
||||
background:#1f2937;color:#f3f4f6;font-size:0.78rem;font-weight:500;
|
||||
padding:calc(env(safe-area-inset-top, 0px) + 7px) 16px 7px;
|
||||
align-items:center;justify-content:center;gap:8px;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,.3)">
|
||||
<!-- Offline-Banner (Styling in components.css — boot.js toggelt display + .collapsed) -->
|
||||
<div id="offline-banner" aria-live="polite">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256">
|
||||
<path d="M213.92,210.62l-160-176A8,8,0,1,0,42.08,45.38L81.06,88.86A152.34,152.34,0,0,0,26.49,130a8,8,0,0,0,11,11.61,136.36,136.36,0,0,1,52-37.29l19.2,21.12A96.09,96.09,0,0,0,67.6,160.59,8,8,0,1,0,79,172.2a80.12,80.12,0,0,1,33.5-23.89L128,165.37V224a8,8,0,0,0,16,0V183.94l69.92,76.92a8,8,0,1,0,11.84-10.76ZM128,141.46,108.42,120A80.38,80.38,0,0,1,128,116a79.91,79.91,0,0,1,19.59,2.43l-19.59,23Zm0-85.46a167.9,167.9,0,0,1,101.51,34.17,8,8,0,1,0,9.72-12.72A183.82,183.82,0,0,0,128,40a183.5,183.5,0,0,0-48.55,6.55L95,64.18A168.23,168.23,0,0,1,128,56Zm57.09,72.41a8,8,0,0,0,11.22-1.36,8,8,0,0,0-1.36-11.22,136.72,136.72,0,0,0-31.62-18.23L178,114.26A120.52,120.52,0,0,1,185.09,128.41Z"/>
|
||||
</svg>
|
||||
|
|
@ -617,11 +612,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1222"></script>
|
||||
<script src="/js/ui.js?v=1222"></script>
|
||||
<script src="/js/app.js?v=1222"></script>
|
||||
<script src="/js/worlds.js?v=1222"></script>
|
||||
<script src="/js/offline-indicator.js?v=1222"></script>
|
||||
<script src="/js/api.js?v=1223"></script>
|
||||
<script src="/js/ui.js?v=1223"></script>
|
||||
<script src="/js/app.js?v=1223"></script>
|
||||
<script src="/js/worlds.js?v=1223"></script>
|
||||
<script src="/js/offline-indicator.js?v=1223"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -631,7 +626,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1222"></script>
|
||||
<script src="/js/boot.js?v=1223"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1222'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1223'; // ← 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;
|
||||
|
|
|
|||
|
|
@ -35,10 +35,18 @@
|
|||
// Offline-Banner
|
||||
// ----------------------------------------------------------
|
||||
(function() {
|
||||
var _collapseTimer = null;
|
||||
function _updateBanner() {
|
||||
var banner = document.getElementById('offline-banner');
|
||||
if (!banner) return;
|
||||
clearTimeout(_collapseTimer);
|
||||
banner.classList.remove('collapsed');
|
||||
banner.style.display = navigator.onLine ? 'none' : 'flex';
|
||||
// Nach 5s auf schmale Icon-Leiste einklappen — das volle Banner verdeckt
|
||||
// sonst die Steuerung oben (z.B. Karten-Legende; Gerätetest iOS 2026-06-06).
|
||||
if (!navigator.onLine) {
|
||||
_collapseTimer = setTimeout(function() { banner.classList.add('collapsed'); }, 5000);
|
||||
}
|
||||
}
|
||||
window.addEventListener('offline', function() {
|
||||
_updateBanner();
|
||||
|
|
|
|||
|
|
@ -77,11 +77,14 @@ window.MapOffline = (function () {
|
|||
var r = lat * Math.PI / 180;
|
||||
return Math.floor((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2 * Math.pow(2, z));
|
||||
}
|
||||
function _tileList(lat, lon, radiusKm) {
|
||||
function _bboxAround(lat, lon, radiusKm) {
|
||||
var dLat = radiusKm / 111, dLon = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
|
||||
var w = lon - dLon, e = lon + dLon, s = lat - dLat, n = lat + dLat, list = [];
|
||||
return { south: lat - dLat, west: lon - dLon, north: lat + dLat, east: lon + dLon };
|
||||
}
|
||||
function _tileList(lat, lon, radiusKm) {
|
||||
var b = _bboxAround(lat, lon, radiusKm), list = [];
|
||||
for (var z = 0; z <= MAXZOOM; z++) {
|
||||
var x0 = _x(w, z), x1 = _x(e, z), y0 = _y(n, z), y1 = _y(s, z);
|
||||
var x0 = _x(b.west, z), x1 = _x(b.east, z), y0 = _y(b.north, z), y1 = _y(b.south, z);
|
||||
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) list.push([z, x, y]);
|
||||
}
|
||||
return list;
|
||||
|
|
@ -120,11 +123,54 @@ window.MapOffline = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
// POI-Marker der Region mitspeichern (Key-Präfix 'p/<type>' im Tiles-Store) — sonst steht die
|
||||
// Offline-Karte ohne Marker da (Gerätetest 2026-06-06). Quelle: /api/osm/pois (liest die lokale
|
||||
// osm_pois-DB, fast=true). Typen = Werte von OSM_LAYER_MAP in pages/map.js (synchron halten).
|
||||
var POI_TYPES = ['waste_basket', 'dog_park', 'drinking_water', 'tierarzt', 'hundesalon', 'shop',
|
||||
'restaurant', 'bank', 'giftkoeder', 'kotbeutel', 'gefahr', 'parkplatz',
|
||||
'treffpunkt', 'sonstiges', 'hotel'];
|
||||
function _cachePois(bbox) {
|
||||
var total = 0;
|
||||
var jobs = POI_TYPES.map(function (type) {
|
||||
var params = new URLSearchParams({ type: type, fast: 'true',
|
||||
south: bbox.south, west: bbox.west, north: bbox.north, east: bbox.east });
|
||||
return fetch('/api/osm/pois?' + params)
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (fresh) {
|
||||
if (!fresh || !fresh.length) return;
|
||||
total += fresh.length;
|
||||
// Mit Bestand mergen (per id) — eine zweite Region (Urlaubsort) darf die erste nicht löschen.
|
||||
return _get('p/' + type).then(function (old) {
|
||||
var merged = fresh;
|
||||
if (old && old.length) {
|
||||
var seen = {};
|
||||
fresh.forEach(function (p) { seen[p.id] = true; });
|
||||
merged = fresh.concat(old.filter(function (p) { return !seen[p.id]; }));
|
||||
}
|
||||
return _put('p/' + type, merged);
|
||||
});
|
||||
})
|
||||
.catch(function () {});
|
||||
});
|
||||
return Promise.all(jobs).then(function () { return total; });
|
||||
}
|
||||
|
||||
// Gespeicherte POIs eines Typs im Bbox-Ausschnitt — Offline-Fallback für die Karten-Marker.
|
||||
function pois(type, bbox) {
|
||||
return _get('p/' + type).then(function (list) {
|
||||
if (!list || !list.length) return [];
|
||||
return list.filter(function (p) {
|
||||
return p.lat >= bbox.south && p.lat <= bbox.north && p.lon >= bbox.west && p.lon <= bbox.east;
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Bereich um lat/lon (radiusKm, Default 5) herunterladen + in IndexedDB ablegen.
|
||||
// onProgress({done,total,bytes}). Liefert {tiles,bytes}.
|
||||
// onProgress({done,total,bytes}). Liefert {tiles,bytes,pois}.
|
||||
function downloadAround(lat, lon, radiusKm, onProgress) {
|
||||
radiusKm = radiusKm || 5;
|
||||
var list = _tileList(lat, lon, radiusKm), total = list.length, done = 0, bytes = 0, stored = 0, i = 0, CONC = 6;
|
||||
var poiCount = 0;
|
||||
function next() {
|
||||
if (i >= total) return Promise.resolve();
|
||||
var t = list[i++], key = t[0] + '/' + t[1] + '/' + t[2];
|
||||
|
|
@ -139,10 +185,11 @@ window.MapOffline = (function () {
|
|||
var w = []; for (var k = 0; k < CONC; k++) w.push(next());
|
||||
return Promise.all(w)
|
||||
.then(function () { return _cacheGlyphs(); }) // Glyphs mitcachen (sonst offline kein Render)
|
||||
.then(function (gb) { bytes += gb; return _req(META, 'readwrite', function (os) {
|
||||
os.put({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: stored, bytes: bytes, savedAt: Date.now() }, 'region');
|
||||
.then(function (gb) { bytes += gb; return _cachePois(_bboxAround(lat, lon, radiusKm)); })
|
||||
.then(function (pc) { poiCount = pc; return _req(META, 'readwrite', function (os) {
|
||||
os.put({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: stored, bytes: bytes, pois: poiCount, savedAt: Date.now() }, 'region');
|
||||
}); })
|
||||
.then(function () { return { tiles: stored, bytes: bytes }; });
|
||||
.then(function () { return { tiles: stored, bytes: bytes, pois: poiCount }; });
|
||||
}
|
||||
|
||||
function stats() {
|
||||
|
|
@ -159,6 +206,6 @@ window.MapOffline = (function () {
|
|||
|
||||
return {
|
||||
registerProtocol: registerProtocol, downloadAround: downloadAround, tile: tile, glyph: glyph,
|
||||
stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
|
||||
pois: pois, stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1477,7 +1477,15 @@ window.Page_map = (() => {
|
|||
const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json());
|
||||
_replaceOsmMarkers(layerKey, pois);
|
||||
return pois.length;
|
||||
} catch { return 0; }
|
||||
} catch {
|
||||
// Offline: gespeicherte Region-POIs aus IndexedDB (MapOffline.downloadAround
|
||||
// legt sie beim Region-Download mit ab) statt leerer Karte.
|
||||
try {
|
||||
const off = window.MapOffline ? await MapOffline.pois(osmType, bbox) : [];
|
||||
if (off.length) { _replaceOsmMarkers(layerKey, off); return off.length; }
|
||||
} catch (e) {}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
const fastCounts = await Promise.all(fastTasks);
|
||||
const fastTotal = fastCounts.reduce((a, b) => a + b, 0);
|
||||
|
|
@ -2152,7 +2160,7 @@ window.Page_map = (() => {
|
|||
_setOsmStatus(`Offline: ${Math.round(p.done / p.total * 100)} %…`);
|
||||
});
|
||||
_setOsmStatus('');
|
||||
UI.toast.success(`Gegend offline gespeichert — ${res.tiles} Kacheln, ${(res.bytes / 1048576).toFixed(1)} MB.`);
|
||||
UI.toast.success(`Gegend offline gespeichert — ${res.tiles} Kacheln, ${res.pois || 0} Marker, ${(res.bytes / 1048576).toFixed(1)} MB.`);
|
||||
window.OfflineIndicator?.refresh(); // Pfoten-Segment 5 sofort grün
|
||||
} catch (e) {
|
||||
_setOsmStatus('');
|
||||
|
|
|
|||
|
|
@ -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=1222"></script>
|
||||
<script src="/js/landing-init.js?v=1223"></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 = '1222';
|
||||
const VER = '1223';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
|
|
@ -1,28 +1,41 @@
|
|||
# Offline-Karten (GL/Vektor) — Feature-Plan
|
||||
|
||||
**Status:** KERN UMGESETZT + headless verifiziert (2026-06-05, v1213), **flag-gated `by_offline_tiles` (Default AUS)** bis Gerätetest.
|
||||
**Stand:** 2026-06-05. Autor: René + Claude (Design).
|
||||
**Status:** KERN + Follow-ups Runde 1 umgesetzt, **Staging-Default AN seit 2026-06-06 (v1222)** — Gerätetest ausstehend; Production AUS bis Freigabe.
|
||||
**Stand:** 2026-06-06. Autor: René + Claude (Design).
|
||||
|
||||
## Umsetzungsstand (2026-06-05)
|
||||
**✅ Fertig + headless bewiesen:**
|
||||
## Umsetzungsstand (2026-06-06, v1222 auf Staging)
|
||||
**✅ Fertig + headless bewiesen (2026-06-05, v1213):**
|
||||
- `map-offline.js` (`window.MapOffline`): Region-Download (`downloadAround(lat,lon,radiusKm)`) → Vektorkacheln
|
||||
z0–14 via `pmtiles.getZxy` (liefert bereits dekomprimierte MVT) + Glyphs in **IndexedDB** (`by-offline-tiles`).
|
||||
`byt://`-MapLibre-Protokoll (IndexedDB-first, remote-Fallback). ~15 MB / 5 km (dekomprimiert).
|
||||
- `map-gl-style.js` `build({offline})`: `byt`-Source statt `pmtiles://`. Flag `by_offline_tiles` (Default AUS).
|
||||
- `map-gl-style.js` `build({offline})`: `byt`-Source statt `pmtiles://`.
|
||||
- ui.js/map.js laden map-offline + registrieren `byt`. `UI.loadMapLibreUI` exportiert.
|
||||
- Welten-FAB Segment 5: prüft im GL-Modus gespeicherte Region (nicht mehr OSM-Raster); „Fehlende nachladen"
|
||||
stößt `MapOffline.downloadAround(GPS, 5km)` an.
|
||||
- **Beweis:** Download 97 Tiles (5 km München) → Netz AUS → **1903 Features gerendert**, nicht geladene
|
||||
Gegend (Paris) leer; Glyphs nötig (sonst lässt MapLibre offline die ganze Kachel fallen).
|
||||
|
||||
**🔲 Offen (Follow-ups):**
|
||||
- **Gerätetest (iOS-PWA offline/IndexedDB)** → dann Flag-Default auf Staging-AN (analog `by_map_gl`).
|
||||
- Download-Button auf der **Karte** (`map-offline-btn`) im GL-Modus auf `downloadAround(Karten-Center)` umbiegen
|
||||
(bisher OSM-Raster-Prefetch).
|
||||
**✅ Follow-ups Runde 1 (2026-06-06, v1222):**
|
||||
- **Flag-Default Staging-AN:** `by_offline_tiles` Default AN auf `staging.banyaro.app`, AUS sonst;
|
||||
localStorage `1`/`0` bzw. `?tilesoffline=1/0` (boot.js) übersteuert. Default-Logik 3× synchron:
|
||||
`map-gl-style.js _offlineEnabled()`, `offline-indicator.js _offlineTilesMode()`, `pages/map.js _offlineTilesEnabled()`.
|
||||
- **Karten-Download-Button:** Speed-Dial „Karte offline speichern" (`map-offline-btn`, war seit FAB-Redesign
|
||||
verwaist) — GL-Modus → `downloadAround(Kartenmitte, 5 km)` mit Fortschritt in der Statusbar (Kartenmitte
|
||||
statt GPS: Urlaubsort vorab speicherbar); Leaflet-Modus → alter Raster-Prefetch (`_cacheTiles`).
|
||||
Sichtbarkeit gated: GL ohne Offline-Flag (= Production) zeigt den Button nicht.
|
||||
- **Glyph-Persistenz:** Glyphs in IndexedDB (Key-Präfix `f/` im Tiles-Store, kein Schema-Bump) + Protokoll
|
||||
`byt://f/{fontstack}/{range}` (IndexedDB-first, remote-Fallback); Style nutzt offline die byt-Glyph-URL
|
||||
→ überlebt App-Updates (SW-Cache wird gepurged, IndexedDB nicht).
|
||||
- **Raster-Prefetch gegated:** `offline-indicator.js init()` überspringt `_prefetchTiles()` im
|
||||
Offline-Tiles-Modus (GL nutzt das OSM-Raster nicht).
|
||||
|
||||
**🔲 Offen (Follow-ups Runde 2):**
|
||||
- **Gerätetest (iOS-PWA offline/IndexedDB)** auf Staging — jetzt ohne Flag-Frickelei möglich (Default AN).
|
||||
Danach: Prod-Freigabe-Entscheidung (Default-Hostnames erweitern analog `by_map_gl`).
|
||||
- **Adaptives Lernen** (rollendes Vorausladen beim Aufzeichnen + Funkloch-Gedächtnis).
|
||||
- **Bereichsauswahl / Routen-Korridor** (inkl. „Route offline speichern" aus routes.js `_openDetail`).
|
||||
- **Glyph-Persistenz** über App-Updates (aktuell SW-Cache, wird bei Update gepurged) → in IndexedDB ablegen + via `byt://f/` servieren.
|
||||
- Alten OSM-Raster-Prefetch (`offline-indicator.js _prefetchTiles`) entfernen, wenn Flag dauerhaft AN.
|
||||
- Alten OSM-Raster-Prefetch (`offline-indicator.js _prefetchTiles` + `map.js _cacheTiles`) komplett
|
||||
entfernen, wenn Flag dauerhaft AN (auch Prod).
|
||||
|
||||
## Ziel
|
||||
GL-Vektorkarten offline-tauglich machen — Kernszenario **Gassi/Wandern im Funkloch**.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue