Sprint 10: OSM-POI-Cache, Karten-Clustering, Routen-Redesign
Karte (map.js):
- OSM Overpass API: Restaurants, Tierärzte, Parkplätze, Bänke, Wasserstellen
- Leaflet.markercluster für alle OSM-Layer
- Standort-Dot mit GPS-Genauigkeitskreis, Wake-Lock bei Aufzeichnung
- Community-Pins setzen/löschen, Meldungen, Crosshair-Placement
- Layer-Sichtbarkeit in localStorage (by_map_visible_v1)
Routen (routes.js + routen.py):
- Komoot-Stil: SVG-Track-Preview, Foto-Upload, Nearby-POIs im Detail-Modal
- Neue Felder: is_public, hunde_tauglichkeit, foto_urls
- Rate-Endpoint (POST /api/routes/{id}/rate)
- Foto-Upload (POST /api/routes/{id}/photo)
- Fix: json_extract $[-1] → $[#-1] (SQLite-kompatibler Pfad für letztes Element)
Backend (osm.py, database.py, scheduler.py):
- /api/osm/pois: OSM-Overpass-Cache mit Tile-Logik (14 Tage TTL)
- /api/osm/user-poi: Community-Marker CRUD
- /api/osm/report: Marker als ungültig melden
- Neue Tabellen: osm_pois, osm_tiles, user_map_pois, osm_reports
- Giftköder-Archiv-Job (täglich 03:00, soft-delete nach Ablauf)
- Giftköder-Archiv-Job als APScheduler-CronJob
UI: Orte-Menüpunkt entfernt (in Karte integriert), APP_VER auf 62
This commit is contained in:
parent
bf26e5faf4
commit
ebe4ce20cf
16 changed files with 3020 additions and 737 deletions
60
backend/static/css/MarkerCluster.Default.css
Normal file
60
backend/static/css/MarkerCluster.Default.css
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
}
|
||||
|
||||
/* IE 6-8 fallback colors */
|
||||
.leaflet-oldie .marker-cluster-small {
|
||||
background-color: rgb(181, 226, 140);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-small div {
|
||||
background-color: rgb(110, 204, 57);
|
||||
}
|
||||
|
||||
.leaflet-oldie .marker-cluster-medium {
|
||||
background-color: rgb(241, 211, 87);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-medium div {
|
||||
background-color: rgb(240, 194, 12);
|
||||
}
|
||||
|
||||
.leaflet-oldie .marker-cluster-large {
|
||||
background-color: rgb(253, 156, 115);
|
||||
}
|
||||
.leaflet-oldie .marker-cluster-large div {
|
||||
background-color: rgb(241, 128, 23);
|
||||
}
|
||||
|
||||
.marker-cluster {
|
||||
background-clip: padding-box;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.marker-cluster div {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.marker-cluster span {
|
||||
line-height: 30px;
|
||||
}
|
||||
14
backend/static/css/MarkerCluster.css
Normal file
14
backend/static/css/MarkerCluster.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
|
||||
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
|
||||
}
|
||||
|
||||
.leaflet-cluster-spider-leg {
|
||||
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
|
||||
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
|
||||
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
|
||||
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
|
||||
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
|
||||
}
|
||||
|
|
@ -1439,126 +1439,329 @@ textarea.form-control {
|
|||
}
|
||||
|
||||
/* ============================================================
|
||||
ROUTEN (routes.js)
|
||||
ROUTEN — Komoot-Stil (routes.js)
|
||||
============================================================ */
|
||||
.routes-layout {
|
||||
.rk-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--c-bg);
|
||||
}
|
||||
.routes-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid var(--c-border-light);
|
||||
flex-shrink: 0;
|
||||
background: var(--c-surface);
|
||||
}
|
||||
.routes-tab {
|
||||
flex: 1;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--c-text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.routes-tab.active {
|
||||
color: var(--c-primary);
|
||||
border-color: var(--c-primary);
|
||||
}
|
||||
.routes-tab-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.routes-map {
|
||||
height: 40%;
|
||||
min-height: 160px;
|
||||
.rk-header {
|
||||
background: var(--c-surface);
|
||||
border-bottom: 1px solid var(--c-border-light);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.routes-list {
|
||||
.rk-search-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
/* Import-Label als Button */
|
||||
.rk-imp-btn {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Import Modal */
|
||||
.rk-import-preview {
|
||||
height: 160px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
background: var(--c-surface-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.rk-import-stats {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--c-text-secondary);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.rk-search {
|
||||
flex: 1;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1.5px solid var(--c-border);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text);
|
||||
outline: none;
|
||||
}
|
||||
.rk-search:focus { border-color: var(--c-primary); }
|
||||
.rk-rec-btn {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rk-filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.rk-filter-group {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.rk-filter-group::-webkit-scrollbar { display: none; }
|
||||
.rk-chip {
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1.5px solid var(--c-border);
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.rk-chip.active {
|
||||
background: var(--c-primary);
|
||||
border-color: var(--c-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.rk-grid {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
gap: var(--space-3);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--c-primary) var(--c-surface);
|
||||
}
|
||||
.routes-card {
|
||||
.rk-loading, .rk-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-10) var(--space-4);
|
||||
color: var(--c-text-secondary);
|
||||
}
|
||||
.rk-empty-icon { font-size: 3rem; margin-bottom: var(--space-3); }
|
||||
.rk-empty--onboarding { padding: var(--space-6) var(--space-4); }
|
||||
.rk-empty-title { font-size: var(--text-xl); font-weight: 700; color: var(--c-text-primary); margin: 0 0 var(--space-2); }
|
||||
.rk-empty-text { color: var(--c-text-secondary); margin-bottom: var(--space-5); max-width: 320px; margin-left: auto; margin-right: auto; }
|
||||
.rk-empty-features {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2) var(--space-4);
|
||||
max-width: 320px; margin: 0 auto var(--space-6); text-align: left;
|
||||
}
|
||||
.rk-empty-feature { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--c-text-secondary); }
|
||||
.rk-empty-feature span:first-child { font-size: 1.1rem; flex-shrink: 0; }
|
||||
.rk-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
flex-direction: column;
|
||||
background: var(--c-surface);
|
||||
border: 1.5px solid var(--c-border-light);
|
||||
border-radius: var(--radius-lg);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.15s;
|
||||
transition: box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
.routes-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.routes-card-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-weight: var(--weight-bold);
|
||||
font-size: var(--text-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.rk-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.rk-card-preview {
|
||||
height: 140px;
|
||||
overflow: hidden;
|
||||
background: #e8f0e8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rk-preview-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
font-size: 2.5rem;
|
||||
color: var(--c-text-muted);
|
||||
opacity: 0.4;
|
||||
}
|
||||
.routes-card-body { flex: 1; min-width: 0; }
|
||||
.routes-card-name { font-weight: var(--weight-semibold); color: var(--c-text); }
|
||||
.routes-card-meta { font-size: var(--text-sm); color: var(--c-text-secondary); margin-top: 2px; }
|
||||
.routes-card-tags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }
|
||||
.routes-badge {
|
||||
.rk-card-body {
|
||||
padding: var(--space-3) var(--space-4) var(--space-4);
|
||||
}
|
||||
.rk-card-name {
|
||||
font-weight: var(--weight-semibold);
|
||||
font-size: var(--text-base);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
.rk-card-stats {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--c-text-secondary);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.rk-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.rk-badge {
|
||||
font-size: var(--text-xs);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--c-surface-2);
|
||||
color: var(--c-text-secondary);
|
||||
}
|
||||
.routes-badge--leicht { background: #dcfce7; color: #15803d; }
|
||||
.routes-badge--mittel { background: #fef9c3; color: #a16207; }
|
||||
.routes-badge--anspruchsvoll{ background: #fee2e2; color: #b91c1c; }
|
||||
.routes-badge--info { background: var(--c-primary-subtle); color: var(--c-primary-dark); }
|
||||
|
||||
/* Aufzeichnungs-Panel */
|
||||
.routes-rec-panel {
|
||||
padding: var(--space-5) var(--space-4);
|
||||
background: var(--c-surface);
|
||||
flex-shrink: 0;
|
||||
.rk-badge--leicht { background: #dcfce7; color: #15803d; }
|
||||
.rk-badge--mittel { background: #fef9c3; color: #a16207; }
|
||||
.rk-badge--anspruchsvoll{ background: #fee2e2; color: #b91c1c; }
|
||||
.rk-badge--info { background: var(--c-primary-subtle); color: var(--c-primary-dark); }
|
||||
.rk-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.routes-rec-stats {
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
justify-content: center;
|
||||
.rk-stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.routes-rec-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 70px;
|
||||
}
|
||||
.routes-rec-stat-val {
|
||||
font-size: 1.8rem;
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--c-primary);
|
||||
.rk-star {
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
color: var(--c-border);
|
||||
transition: color 0.1s, transform 0.1s;
|
||||
line-height: 1;
|
||||
}
|
||||
.routes-rec-stat-lbl {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--c-text-muted);
|
||||
margin-top: 2px;
|
||||
.rk-star.filled { color: #F59E0B; }
|
||||
.rk-star:hover { color: #F59E0B; transform: scale(1.2); }
|
||||
.rk-star-count { font-size: var(--text-xs); color: var(--c-text-muted); margin-left: 4px; }
|
||||
.rk-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.rk-card-author { font-size: var(--text-xs); color: var(--c-text-muted); }
|
||||
.rk-dl-btn {
|
||||
font-size: var(--text-xs);
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--c-border);
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text-secondary);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rk-dl-btn:hover { background: var(--c-surface-2); }
|
||||
|
||||
/* Hundetauglichkeit-Badge */
|
||||
.rk-badge--dog { background: #fef3c7; color: #92400e; font-size: 1rem; }
|
||||
.rk-badge--private { background: #f1f5f9; color: #64748b; }
|
||||
|
||||
/* Foto-Galerie in Route-Detail */
|
||||
.rk-photo-gallery {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
overflow-x: auto;
|
||||
margin: var(--space-2) 0;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.rk-photo-thumb {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid var(--c-border-light);
|
||||
}
|
||||
.rk-photo-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 2px dashed var(--c-border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
.rk-photo-add-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: 2px dashed var(--c-border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
color: var(--c-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
/* Nearby POIs */
|
||||
.rk-nearby-section { margin-top: var(--space-3); }
|
||||
.rk-nearby-title {
|
||||
font-weight: var(--weight-semibold);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--c-text);
|
||||
}
|
||||
.rk-nearby-group {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.rk-nearby-group-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--c-text-secondary);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
.rk-nearby-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
align-items: baseline;
|
||||
padding: var(--space-1) 0;
|
||||
border-bottom: 1px solid var(--c-border-light);
|
||||
}
|
||||
.rk-nearby-item:last-child { border-bottom: none; }
|
||||
.rk-nearby-name { font-size: var(--text-sm); color: var(--c-text); }
|
||||
.rk-nearby-detail { font-size: var(--text-xs); color: var(--c-text-muted); }
|
||||
.rk-nearby-phone { color: var(--c-primary); text-decoration: none; }
|
||||
|
||||
/* Hundetauglichkeit-Auswahl im Formular */
|
||||
.rk-paw-select {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
.rk-paw-btn {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1.5px solid var(--c-border);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--c-bg);
|
||||
color: var(--c-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rk-paw-btn.selected {
|
||||
border-color: var(--c-primary);
|
||||
background: var(--c-primary-subtle);
|
||||
color: var(--c-primary-dark);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
|
||||
/* Aufzeichnungs-FAB Zustand */
|
||||
.map-fab--rec.recording {
|
||||
background: #EF4444;
|
||||
border-color: #EF4444;
|
||||
color: #fff;
|
||||
animation: rec-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes rec-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(239,68,68,0); }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
|
@ -1574,50 +1777,80 @@ textarea.form-control {
|
|||
z-index: 1;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.map-full-layout {
|
||||
top: 0;
|
||||
left: var(--nav-sidebar-width);
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
.map-full {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.map-full-layout { top: 0; left: var(--nav-sidebar-width); bottom: 0; }
|
||||
}
|
||||
.map-full { width: 100%; height: 100%; }
|
||||
|
||||
/* Legende: horizontaler Scroll-Strip oben */
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: var(--space-2);
|
||||
left: 42px; /* Zoom-Control (+/-) freilassen */
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--space-1);
|
||||
max-width: calc(100vw - var(--space-6));
|
||||
justify-content: center;
|
||||
padding: 0 var(--space-3) 0 var(--space-1);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.map-legend::-webkit-scrollbar { display: none; }
|
||||
|
||||
.map-legend-btn {
|
||||
padding: var(--space-1) var(--space-2);
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(255,255,255,0.92);
|
||||
background: rgba(255,255,255,0.93);
|
||||
border: 1.5px solid var(--layer-color, var(--c-border));
|
||||
color: var(--c-text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-size: 11px;
|
||||
font-weight: var(--weight-semibold);
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(6px);
|
||||
transition: all 0.15s;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.map-legend-btn.active {
|
||||
background: var(--layer-color, var(--c-primary));
|
||||
color: #fff;
|
||||
background: var(--layer-color, var(--c-primary));
|
||||
color: #fff;
|
||||
border-color: var(--layer-color, var(--c-primary));
|
||||
}
|
||||
.map-locate-btn {
|
||||
position: absolute;
|
||||
bottom: var(--space-6);
|
||||
right: var(--space-4);
|
||||
z-index: 1000;
|
||||
.map-legend-label { font-size: 10px; }
|
||||
.map-legend-all {
|
||||
font-size: 1rem;
|
||||
min-width: 32px;
|
||||
padding: 0 var(--space-2);
|
||||
background: var(--c-surface-2);
|
||||
border-color: var(--c-border);
|
||||
color: var(--c-text-secondary);
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
.map-legend-all.all-off {
|
||||
background: #1e293b;
|
||||
border-color: #1e293b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* FAB-Gruppe rechts unten */
|
||||
.map-fabs {
|
||||
position: absolute;
|
||||
bottom: var(--space-4);
|
||||
right: var(--space-3);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
.map-fab {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
|
|
@ -1625,11 +1858,247 @@ textarea.form-control {
|
|||
border: 1.5px solid var(--c-border);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
font-size: 1.3rem;
|
||||
font-size: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.map-fab:hover { background: var(--c-surface-2); }
|
||||
.map-fab--pin.active {
|
||||
background: var(--c-danger);
|
||||
border-color: var(--c-danger);
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.map-fab:disabled { opacity: 0.5; cursor: default; }
|
||||
.map-fab--offline.loading { animation: fab-spin 1.2s linear infinite; pointer-events: none; }
|
||||
@keyframes fab-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Aufzeichnungs-Panel — schiebt sich von unten rein */
|
||||
.map-rec-panel {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
color: #fff;
|
||||
padding: var(--space-4) var(--space-5) calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
|
||||
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
z-index: 420;
|
||||
pointer-events: none;
|
||||
}
|
||||
.map-rec-panel.active {
|
||||
transform: translateY(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
.map-rec-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.map-rec-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.map-rec-stat--main .map-rec-val { font-size: 2.4rem; }
|
||||
.map-rec-val {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.map-rec-lbl {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(255,255,255,0.55);
|
||||
}
|
||||
.map-rec-actions {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.map-rec-action-btn { flex: 1; }
|
||||
.map-rec-hint {
|
||||
text-align: center;
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255,255,255,0.45);
|
||||
}
|
||||
.map-rec-panel.paused .map-rec-val { color: #F59E0B; }
|
||||
.map-rec-panel.paused .map-rec-hint::before { content: '⏸ Pausiert — '; }
|
||||
|
||||
/* Fadenkreuz-Overlay */
|
||||
.map-crosshair {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 410;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.map-crosshair.active { display: flex; }
|
||||
.map-crosshair-pin {
|
||||
font-size: 2.4rem;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 3px 6px rgba(0,0,0,0.45));
|
||||
transition: transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
|
||||
}
|
||||
.map-crosshair.dragging .map-crosshair-pin {
|
||||
transform: translateY(-10px) scale(1.15);
|
||||
}
|
||||
.map-crosshair-shadow {
|
||||
width: 10px;
|
||||
height: 4px;
|
||||
background: rgba(0,0,0,0.25);
|
||||
border-radius: 50%;
|
||||
margin-top: 1px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.map-crosshair.dragging .map-crosshair-shadow {
|
||||
width: 6px; opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Bestätigen-Leiste unten */
|
||||
.map-place-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--c-surface);
|
||||
border-top: 1px solid var(--c-border-light);
|
||||
padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
z-index: 410;
|
||||
box-shadow: 0 -4px 20px rgba(0,0,0,0.08);
|
||||
}
|
||||
.map-place-bar.active { display: flex; }
|
||||
.map-place-hint {
|
||||
text-align: center;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--c-text-secondary);
|
||||
}
|
||||
.map-place-btns {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.map-place-btns .btn { flex: 1; }
|
||||
|
||||
/* Statusleiste: nur Info, unten links */
|
||||
.map-statusbar {
|
||||
position: absolute;
|
||||
bottom: var(--space-3);
|
||||
left: var(--space-3);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background: rgba(255,255,255,0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid var(--c-border-light);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 4px 12px;
|
||||
font-size: 11px;
|
||||
color: var(--c-text-secondary);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||
max-width: calc(100% - 80px); /* Platz für FABs */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Giftköder-Marker — pulsierend, rot, sofort erkennbar */
|
||||
.poison-marker {
|
||||
position: relative;
|
||||
width: 48px; height: 48px;
|
||||
}
|
||||
.poison-dot {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 44px; height: 44px;
|
||||
background: #DC2626;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px;
|
||||
box-shadow: 0 2px 10px rgba(220,38,38,0.7);
|
||||
z-index: 2;
|
||||
}
|
||||
.poison-ring {
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 50%;
|
||||
background: rgba(220,38,38,0.35);
|
||||
animation: poison-pulse 1.8s ease-out infinite;
|
||||
z-index: 1;
|
||||
}
|
||||
.poison-ring:nth-child(2) { animation-delay: 0.6s; }
|
||||
@keyframes poison-pulse {
|
||||
0% { transform: translate(-50%,-50%) scale(1); opacity: 0.8; }
|
||||
100% { transform: translate(-50%,-50%) scale(2.8); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Pulsierender Standort-Marker */
|
||||
.loc-icon { position: relative; }
|
||||
.loc-dot {
|
||||
width: 16px; height: 16px;
|
||||
background: #3B82F6;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
|
||||
position: absolute;
|
||||
top: 4px; left: 4px;
|
||||
}
|
||||
.loc-ring {
|
||||
width: 24px; height: 24px;
|
||||
background: rgba(59,130,246,0.3);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
animation: loc-pulse 2s ease-out infinite;
|
||||
}
|
||||
@keyframes loc-pulse {
|
||||
0% { transform: scale(0.8); opacity: 1; }
|
||||
100% { transform: scale(2.2); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Pin-Typ-Auswahl im Modal */
|
||||
.poi-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.poi-type-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 4px;
|
||||
border: 2px solid var(--c-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--c-surface);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.poi-type-btn.selected {
|
||||
border-color: var(--pt-color, var(--c-primary));
|
||||
background: color-mix(in srgb, var(--pt-color, var(--c-primary)) 12%, transparent);
|
||||
}
|
||||
.poi-type-icon { font-size: 22px; line-height: 1; }
|
||||
.poi-type-label { font-size: 10px; color: var(--c-text-secondary); text-align: center; line-height: 1.2; }
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
GASSI-TREFFEN (walks.js)
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=32">
|
||||
<link rel="stylesheet" href="/css/components.css?v=32">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=62">
|
||||
<link rel="stylesheet" href="/css/components.css?v=62">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -55,10 +55,7 @@
|
|||
<div class="sidebar-item" data-page="routes">
|
||||
<span class="sidebar-item-icon">🥾</span> Routen
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="places">
|
||||
<span class="sidebar-item-icon">📍</span> Orte
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="events">
|
||||
<div class="sidebar-item" data-page="events">
|
||||
<span class="sidebar-item-icon">🎯</span> Events
|
||||
</div>
|
||||
|
||||
|
|
@ -136,11 +133,7 @@
|
|||
<div class="page-body page-container"></div>
|
||||
</section>
|
||||
|
||||
<section class="page" id="page-places">
|
||||
<div class="page-body page-container"></div>
|
||||
</section>
|
||||
|
||||
<section class="page" id="page-events">
|
||||
<section class="page" id="page-events">
|
||||
<div class="page-body page-container"></div>
|
||||
</section>
|
||||
|
||||
|
|
@ -214,9 +207,9 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=32"></script>
|
||||
<script src="/js/ui.js?v=32"></script>
|
||||
<script src="/js/app.js?v=32"></script>
|
||||
<script src="/js/api.js?v=62"></script>
|
||||
<script src="/js/ui.js?v=62"></script>
|
||||
<script src="/js/app.js?v=62"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
|
|||
|
|
@ -188,6 +188,14 @@ const API = (() => {
|
|||
create(data) { return post('/routes', data); },
|
||||
update(id, data) { return patch(`/routes/${id}`, data); },
|
||||
delete(id) { return del(`/routes/${id}`); },
|
||||
rate(id, wertung) { return post(`/routes/${id}/rate`, { wertung }); },
|
||||
addPhoto(id, file) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return fetch(`/api/routes/${id}/photo`, {
|
||||
method: 'POST', credentials: 'include', body: fd,
|
||||
}).then(r => r.ok ? r.json() : Promise.reject(new Error('Foto-Upload fehlgeschlagen')));
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '62'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -26,8 +28,7 @@ const App = (() => {
|
|||
'dog-profile': { title: 'Mein Hund', module: null },
|
||||
map: { title: 'Karte', module: null },
|
||||
routes: { title: 'Routen', module: null },
|
||||
places: { title: 'Orte', module: null },
|
||||
events: { title: 'Events', module: null },
|
||||
events: { title: 'Events', module: null },
|
||||
poison: { title: 'Giftköder-Alarm', module: null },
|
||||
walks: { title: 'Gassi-Treffen', module: null },
|
||||
sitting: { title: 'Sitting', module: null },
|
||||
|
|
@ -119,10 +120,12 @@ const App = (() => {
|
|||
}
|
||||
|
||||
function _loadScript(src) {
|
||||
// Versionierter URL damit SW/Browser-Cache bei jedem Deploy invalidiert wird
|
||||
const versioned = `${src}?v=${APP_VER}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
|
||||
if (document.querySelector(`script[src="${versioned}"]`)) { resolve(); return; }
|
||||
const s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.src = versioned;
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
|
|
@ -193,11 +196,7 @@ const App = (() => {
|
|||
}
|
||||
|
||||
function _openSidebar() {
|
||||
const s = document.getElementById('sidebar');
|
||||
if (!s) { UI.toast.error('sidebar: NULL'); return; }
|
||||
s.classList.add('open');
|
||||
const cs = getComputedStyle(s);
|
||||
UI.toast.info(`display:${cs.display} | transform:${cs.transform} | z:${cs.zIndex}`);
|
||||
document.getElementById('sidebar')?.classList.add('open');
|
||||
document.getElementById('sidebar-backdrop')?.classList.add('visible');
|
||||
}
|
||||
|
||||
|
|
@ -223,10 +222,7 @@ const App = (() => {
|
|||
<button class="btn btn-danger w-full" data-quick="poison">
|
||||
⚠️ Giftköder melden
|
||||
</button>
|
||||
<button class="btn btn-secondary w-full" data-quick="place">
|
||||
📍 Ort hinzufügen
|
||||
</button>
|
||||
<button class="btn btn-nature w-full" data-quick="walk">
|
||||
<button class="btn btn-nature w-full" data-quick="walk">
|
||||
🦮 Gassi-Treffen erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -246,8 +242,7 @@ const App = (() => {
|
|||
if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); }
|
||||
if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); }
|
||||
if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
|
||||
if (action === 'place') { navigate('places'); pages['places'].module?.openNew?.(); }
|
||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
|
||||
}, 350);
|
||||
}, { once: true });
|
||||
}
|
||||
|
|
|
|||
2
backend/static/js/leaflet.markercluster.js
Normal file
2
backend/static/js/leaflet.markercluster.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,11 @@
|
|||
/* ============================================================
|
||||
BAN YARO — Service Worker
|
||||
Offline-Cache + Push Notifications
|
||||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v33';
|
||||
const CACHE_VERSION = 'by-v62';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
||||
// index.html wird NICHT pre-gecacht (immer Network-First)
|
||||
const STATIC_ASSETS = [
|
||||
|
|
@ -14,6 +15,9 @@ const STATIC_ASSETS = [
|
|||
'/js/api.js',
|
||||
'/js/ui.js',
|
||||
'/js/app.js',
|
||||
'/js/leaflet.markercluster.js',
|
||||
'/css/MarkerCluster.css',
|
||||
'/css/MarkerCluster.Default.css',
|
||||
'/manifest.json',
|
||||
'/icons/icon-192.png',
|
||||
];
|
||||
|
|
@ -30,13 +34,15 @@ self.addEventListener('install', event => {
|
|||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ACTIVATE — alte Caches aufräumen
|
||||
// ACTIVATE — alte Caches aufräumen (CACHE_TILES behalten)
|
||||
// ----------------------------------------------------------
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.filter(k => k !== CACHE_STATIC).map(k => caches.delete(k))
|
||||
keys
|
||||
.filter(k => k !== CACHE_STATIC && k !== CACHE_TILES)
|
||||
.map(k => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
|
|
@ -59,6 +65,38 @@ self.addEventListener('fetch', event => {
|
|||
return;
|
||||
}
|
||||
|
||||
// OSM-Kartenkacheln: eigener persistenter Cache
|
||||
if (url.hostname.endsWith('tile.openstreetmap.org')) {
|
||||
event.respondWith(
|
||||
caches.open(CACHE_TILES).then(cache =>
|
||||
cache.match(event.request).then(cached => {
|
||||
if (cached) return cached;
|
||||
return fetch(event.request).then(response => {
|
||||
if (response.ok) cache.put(event.request, response.clone());
|
||||
return response;
|
||||
});
|
||||
})
|
||||
).catch(() => new Response('', { status: 503 }))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Seiten-Module (/js/pages/…): immer Network-First (versioniert über ?v=, kein alter Cache-Treffer)
|
||||
if (url.pathname.startsWith('/js/pages/')) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => caches.match(event.request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation (index.html): immer Network-First
|
||||
if (event.request.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
|
|
@ -93,6 +131,48 @@ self.addEventListener('fetch', event => {
|
|||
);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// MESSAGE — Tile-Vorausladung (Offline-Speicherung)
|
||||
// ----------------------------------------------------------
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data?.type !== 'CACHE_TILES') return;
|
||||
|
||||
const urls = event.data.urls || [];
|
||||
const source = event.source;
|
||||
let done = 0;
|
||||
const total = urls.length;
|
||||
|
||||
if (total === 0) {
|
||||
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: 0, total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
caches.open(CACHE_TILES).then(cache => {
|
||||
const queue = [...urls];
|
||||
|
||||
function fetchBatch() {
|
||||
if (queue.length === 0) {
|
||||
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: total, total });
|
||||
return;
|
||||
}
|
||||
const batch = queue.splice(0, 8);
|
||||
Promise.all(batch.map(url =>
|
||||
cache.match(url).then(cached => {
|
||||
if (cached) { done++; return; }
|
||||
return fetch(url, { mode: 'cors' })
|
||||
.then(r => { if (r.ok) cache.put(url, r); done++; })
|
||||
.catch(() => { done++; });
|
||||
})
|
||||
)).then(() => {
|
||||
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done, total });
|
||||
setTimeout(fetchBatch, 30);
|
||||
});
|
||||
}
|
||||
|
||||
fetchBatch();
|
||||
});
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUSH NOTIFICATIONS
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue