From 01b2b94716c5976330f399333aaed154fe9775fc Mon Sep 17 00:00:00 2001 From: Stream Date: Tue, 5 May 2026 19:30:01 +0300 Subject: [PATCH] auto-sync: 2026-05-05 19:30:01 --- tasks/enduro-trails/prototype/static/app.css | 35 +++++----- tasks/enduro-trails/prototype/static/app.js | 72 ++++++++++++++++---- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/tasks/enduro-trails/prototype/static/app.css b/tasks/enduro-trails/prototype/static/app.css index 54ed9b7..3738913 100644 --- a/tasks/enduro-trails/prototype/static/app.css +++ b/tasks/enduro-trails/prototype/static/app.css @@ -284,25 +284,26 @@ body.has-map-mode #sheet-backdrop.visible { pointer-events: none; } .wl-remove:active { background: var(--red-bg); color: var(--red); } .wl-remove svg { width: 14px; height: 14px; } -/* Route actions */ -.route-actions { - display: flex; gap: 8px; margin: 8px 0; -} -.btn-action { - height: 36px; padding: 0 14px; - display: flex; align-items: center; justify-content: center; gap: 6px; - background: var(--surface2); border: 1px solid var(--border); - border-radius: 12px; color: var(--text2); - font-size: 13px; font-weight: 600; cursor: pointer; +/* Sheet icon buttons (header) */ +.sheet-icon-btn { + width: 32px; height: 32px; + display: flex; align-items: center; justify-content: center; + background: none; border: none; color: var(--text3); + border-radius: 8px; cursor: pointer; padding: 0; + flex-shrink: 0; + transition: background 0.15s, color 0.15s; -webkit-tap-highlight-color: transparent; - transition: background 0.15s, transform 0.1s, color 0.15s; } -.btn-action svg { width: 16px; height: 16px; flex-shrink: 0; transition: transform 0.1s; } -.btn-action:active { transform: scale(0.94); background: var(--surface3, var(--border)); } -.btn-action.primary { border-color: var(--accent); color: var(--accent); } -.btn-action.primary:active { background: var(--accent); color: #fff; border-color: var(--accent); } -.btn-action.danger { color: var(--red); } -.btn-action.danger:active { background: var(--red); color: #fff; border-color: var(--red); } +.sheet-icon-btn svg { width: 18px; height: 18px; } +.sheet-icon-btn:active { background: var(--surface2); } +.sheet-icon-btn.danger { color: var(--red); } +.sheet-icon-btn.danger:active { background: var(--red); color: #fff; } + +/* Add waypoint row */ +.wl-add { cursor: pointer; } +.wl-add:active { background: var(--surface); } +.wl-add .wl-pin svg path { fill: var(--text3) !important; } +.wl-add .wl-label { color: var(--text3); } /* ── Route Status ─────────────────────────────── */ #route-status { font-size: 13px; color: var(--text2); padding: 8px 0; display: flex; align-items: center; gap: 6px; } diff --git a/tasks/enduro-trails/prototype/static/app.js b/tasks/enduro-trails/prototype/static/app.js index 625fd66..26bcfca 100644 --- a/tasks/enduro-trails/prototype/static/app.js +++ b/tasks/enduro-trails/prototype/static/app.js @@ -443,7 +443,6 @@ function clearRoute() { } } document.getElementById('route-status').textContent = 'Тапни точку старта на карте'; - document.getElementById('route-actions').style.display = 'none'; document.getElementById('route-cards').innerHTML = ''; document.getElementById('waypoints-list').innerHTML = ''; if (routeMode && map) map.getCanvas().style.cursor = 'crosshair'; @@ -533,7 +532,7 @@ async function renderWaypointsList() { const list = document.getElementById('waypoints-list'); if (!routeWaypoints.length) { list.innerHTML = ''; return; } - list.innerHTML = routeWaypoints.map((wp, i) => { + let html = routeWaypoints.map((wp, i) => { const isStart = i === 0; const isEnd = i === routeWaypoints.length - 1; const label = isStart ? 'S' : isEnd ? 'F' : String(i); @@ -548,6 +547,16 @@ async function renderWaypointsList() { `; }).join(''); + // Кнопка «Добавить точку» в стиле wl-item + if (routeWaypoints.length < 10) { + html += `
+
${waypointPinSvg('+', 'var(--text3)')}
+ Добавить точку +
`; + } + + list.innerHTML = html; + // Async geocode routeWaypoints.forEach(async (wp, i) => { const name = await reverseGeocode(wp.lat, wp.lon); @@ -571,8 +580,6 @@ function removeWaypoint(idx) { } routeResults = []; document.getElementById('route-cards').innerHTML = ''; - document.getElementById('route-actions').style.display = - routeWaypoints.length >= 2 ? 'block' : 'none'; document.getElementById('route-status').textContent = routeWaypoints.length === 0 ? 'Тапни точку старта на карте' : routeWaypoints.length === 1 ? 'Тапни точку финиша' : ''; @@ -607,7 +614,6 @@ async function buildRoute() { drawRouteResults(routeResults, 0); document.getElementById('route-status').textContent = `${routeResults.length} маршрут(ов)`; - document.getElementById('route-actions').style.display = 'block'; } catch(e) { document.getElementById('route-status').textContent = '❌ ' + e.message; document.getElementById('route-cards').innerHTML = ''; @@ -737,16 +743,14 @@ function escapeXml(str) { return (str || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } -function downloadGPX() { +function generateGPX() { const route = routeResults[activeRouteIdx]; - if (!route) return; + if (!route) return ''; const now = new Date(); const dateStr = now.toISOString().slice(0, 10); - const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15); - const filename = `enduro-${timeStr}.gpx`; const distKm = (route.distance_m / 1000).toFixed(1); const dirtPct = route.stats ? route.stats.dirt_total_pct : '?'; - + const wpts = routeWaypoints.map((wp, i) => { const name = i === 0 ? 'Старт' : i === routeWaypoints.length - 1 ? 'Финиш' : `Точка ${i}`; return ` ${escapeXml(name)}`; @@ -755,12 +759,12 @@ function downloadGPX() { markers.forEach(m => { wpts.push(` ${escapeXml(m.name)}${escapeXml(m.icon)}`); }); - + const trkpts = route.geometry.coordinates.map(([lon, lat]) => ` ` ).join('\n'); - - const gpx = ` + + return ` Enduro route ${dateStr} @@ -775,7 +779,14 @@ ${trkpts} `; - +} + +function downloadGPX() { + const gpx = generateGPX(); + if (!gpx) return; + const now = new Date(); + const timeStr = now.toISOString().replace(/[-:]/g, '').slice(0, 15); + const filename = `enduro-${timeStr}.gpx`; const blob = new Blob([gpx], { type: 'application/gpx+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -785,6 +796,38 @@ ${trkpts} URL.revokeObjectURL(url); } +async function shareRoute() { + const route = routeResults[activeRouteIdx]; + if (!route) { alert('Сначала построй маршрут'); return; } + + const distKm = (route.distance_m / 1000).toFixed(0); + const dirtPct = route.stats ? route.stats.dirt_total_pct : 0; + const shareText = `Маршрут: ${distKm} км, ${dirtPct}% грунт`; + const gpx = generateGPX(); + const blob = new Blob([gpx], { type: 'application/gpx+xml' }); + const file = new File([blob], 'enduro-route.gpx', { type: 'application/gpx+xml' }); + + // 1. File share (HTTPS only) + if (navigator.share && location.protocol === 'https:') { + try { + const canShare = navigator.canShare && navigator.canShare({ files: [file] }); + if (canShare) { + await navigator.share({ title: 'Enduro Route', text: shareText, files: [file] }); + return; + } + // canShare rejected files → share text only + await navigator.share({ title: 'Enduro Route', text: shareText }); + return; + } catch(e) { + if (e.name === 'AbortError') return; + console.warn('Share failed, falling back to download:', e); + } + } + + // 2. HTTPS unavailable → download GPX directly + downloadGPX(); +} + // ─── Флажки / именованные метки ──────────────────────────────────── const MARKER_ICONS = ['🚩', '⛺', '🔧', '⛽', '💧', '📍']; const MARKERS_KEY = 'enduro_markers'; @@ -1089,7 +1132,6 @@ function initRouteClicks(map) { } else if (routeWaypoints.length === 1) { routeWaypoints.push({ lon: lng, lat: lat }); rebuildWaypointMarkers(); renderWaypointsList(); - document.getElementById('route-actions').style.display = 'block'; buildRoute(); } });