From d25805130fc16a5c87c8a471c1e372efdcceeccf Mon Sep 17 00:00:00 2001 From: Capybla Date: Wed, 29 Apr 2026 16:05:17 +0200 Subject: [PATCH] Add phase-1 warm UI controls and in-app download panel --- frontend/src/App.css | 88 ++++++++++++++++++++++++++++++++ frontend/src/App.js | 118 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 182 insertions(+), 24 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index a47c26cd72..0e020e85ef 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1795,6 +1795,94 @@ body { font-weight: 500; } +.map-button-controls { + position: absolute; + z-index: 1200; + top: 10px; + right: 10px; + display: flex; + gap: 0.4rem; + flex-wrap: wrap; +} + +.map-button-controls button { + border: 1px solid #ef6c00; + background: rgba(255, 243, 224, 0.95); + color: #5d4037; + border-radius: 8px; + padding: 0.45rem 0.6rem; + font-weight: 700; + cursor: pointer; +} + +.map-button-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.downloads-panel { + background: #fff8e1; + border: 1px solid #ffcc80; + border-radius: 10px; + padding: 0.75rem; + margin-top: 0.75rem; +} + +.downloads-subtitle { + margin: 0.25rem 0 0.75rem 0; + color: #6d4c41; + font-size: 0.88rem; +} + +.download-item { + border: 1px solid #ffe0b2; + background: #fffdf8; + border-radius: 8px; + padding: 0.5rem; + margin-bottom: 0.5rem; +} + +.download-main { + display: flex; + justify-content: space-between; + margin-bottom: 0.35rem; +} + +.download-progress { + width: 100%; + height: 8px; + border-radius: 999px; + background: #ffe0b2; + overflow: hidden; +} + +.download-progress-fill { + height: 100%; + background: linear-gradient(90deg, #ff8a65, #ffb74d); +} + +.download-actions { + margin-top: 0.45rem; + display: flex; + align-items: center; + gap: 0.45rem; +} + +.download-actions button { + border: 1px solid #ffb74d; + background: #fff3e0; + border-radius: 6px; + padding: 0.3rem 0.5rem; + cursor: pointer; +} + +.download-status { + text-transform: uppercase; + font-size: 0.72rem; + letter-spacing: 0.02em; + color: #5d4037; +} + @media (max-width: 1024px) { .sidebar { width: 100%; diff --git a/frontend/src/App.js b/frontend/src/App.js index e0e77c376b..5793fe8e38 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -684,6 +684,9 @@ const FlightConditionsPanel = ({ conditions, sensorStatus, motionData }) => {

Recomendación: {recommendationLabel[conditions.recommendation] || conditions.recommendation}

Despegue óptimo: {Math.round(conditions.takeoff_heading_deg)}° (contra viento)

Aterrizaje óptimo: {Math.round(conditions.landing_heading_deg)}° (contra viento)

+ + ); +}; const NetworkStatusBanner = ({ isOnline, weatherFromCache, weatherUpdatedAt }) => { if (isOnline) return null; @@ -697,6 +700,71 @@ const NetworkStatusBanner = ({ isOnline, weatherFromCache, weatherUpdatedAt }) = ); }; +const MAP_DATASETS = [ + { id: 'terrain-iberia', type: 'terrain', name: 'Terreno Iberia (ES/PT)', sizeMb: 480 }, + { id: 'terrain-alps', type: 'terrain', name: 'Terreno Alpes (FR/IT/CH/AT)', sizeMb: 620 }, + { id: 'airspace-spain', type: 'airspace', name: 'Airspace España', sizeMb: 24 }, + { id: 'airspace-france', type: 'airspace', name: 'Airspace Francia', sizeMb: 28 } +]; + +const DownloadsPanel = ({ isOnline }) => { + const [downloads, setDownloads] = useState(() => MAP_DATASETS.map((item) => ({ ...item, status: 'idle', progress: 0 }))); + + useEffect(() => { + const timers = []; + downloads.forEach((item) => { + if (item.status !== 'downloading') return; + const timer = setInterval(() => { + setDownloads((prev) => prev.map((row) => { + if (row.id !== item.id || row.status !== 'downloading') return row; + const next = Math.min(100, row.progress + 10); + return { ...row, progress: next, status: next >= 100 ? 'done' : 'downloading' }; + })); + }, 600); + timers.push(timer); + }); + return () => timers.forEach(clearInterval); + }, [downloads]); + + const startDownload = (id) => { + if (!isOnline) return; + setDownloads((prev) => prev.map((row) => row.id === id ? { ...row, status: 'downloading', progress: row.progress || 1 } : row)); + }; + + const pauseDownload = (id) => { + setDownloads((prev) => prev.map((row) => row.id === id ? { ...row, status: 'paused' } : row)); + }; + + const resetDownload = (id) => { + setDownloads((prev) => prev.map((row) => row.id === id ? { ...row, status: 'idle', progress: 0 } : row)); + }; + + return ( +
+

📦 Descargas in-app

+

Mapas de terreno y airspace sin salir de la app.

+ {downloads.map((item) => ( +
+
+ {item.name} + {item.sizeMb} MB +
+
+
+
+
+ {item.status !== 'downloading' && item.status !== 'done' && } + {item.status === 'downloading' && } + {(item.status === 'paused' || item.status === 'done') && } + {item.status} +
+
+ ))} + {!isOnline &&

Sin conexión: vuelve online para iniciar descargas.

} +
+ ); +}; + const recommendationLabel = { recommended: '✅ Recomendado volar', caution: '⚠️ Volar con precaución', @@ -748,6 +816,15 @@ const DraggableWidget = ({ id, title, children, config, onUpdate }) => { ); }; +const MapButtonControls = ({ onCenterPosition, onToggleSidebar, onStartNavigation, onEndNavigation, canStartNavigation, navigationMode }) => ( +
+ + + {!navigationMode && } + {navigationMode && } +
+); + const WeatherWidget = ({ conditions, sensorStatus, motionData }) => { if (!conditions) { return

Cargando condiciones meteorológicas...

; @@ -1128,7 +1205,6 @@ const App = () => { const [sensorStatus, setSensorStatus] = useState({ gps: false, barometer: false, accelerometer: false, compass: false }); const [trackingEnabled, setTrackingEnabled] = useState(false); const [flightConditions, setFlightConditions] = useState(null); - const [motionData, setMotionData] = useState({ compassHeading: null, acceleration: null, speedMs: null }); const [motionData, setMotionData] = useState({ compassHeading: null, acceleration: null, speedMs: null, barometricAltitude: null, pressureHpa: null }); const [pages, setPages] = useState(UI_DEFAULT_PAGES); const [activePage, setActivePage] = useState(UI_DEFAULT_PAGES[0]); @@ -1271,6 +1347,12 @@ const App = () => { setTrackingEnabled(false); }; + const handleCenterPosition = () => { + if (currentPosition) { + setMapCenter([currentPosition.lat, currentPosition.lng]); + } + }; + const handlePositionUpdate = (position) => { setCurrentPosition(position); setMotionData(prev => ({ ...prev, speedMs: position.speed || prev.speedMs })); @@ -1284,8 +1366,6 @@ const App = () => { setSensorStatus(status); }; - const handleMotionUpdate = (data) => { - setMotionData(prev => ({ ...prev, ...data })); const motionUpdateRaf = useRef(null); const handleMotionUpdate = (data) => { @@ -1335,8 +1415,6 @@ const App = () => { params: { lat: currentPosition.lat, lng: currentPosition.lng } }); setFlightConditions(response.data); - } catch (error) { - console.error('Error loading flight conditions:', error); const minimizedData = { weather_description: response.data.weather_description, temperature_c: response.data.temperature_c, @@ -1360,8 +1438,7 @@ const App = () => { }; fetchConditions(); - }, [currentPosition]); - }, [currentPosition, isOnline]); + }, [currentPosition, isOnline, lastWeatherSnapshot]); useEffect(() => { @@ -1495,6 +1572,7 @@ const App = () => {
+ )} @@ -1503,6 +1581,14 @@ const App = () => { {visibleOnPage('map') && (
+ setShowSidebar(!showSidebar)} + onStartNavigation={handleStartNavigation} + onEndNavigation={handleEndNavigation} + canStartNavigation={Boolean(selectedRoute && sensorStatus.gps)} + navigationMode={navigationMode} + /> {navigationMode && selectedRoute && ( @@ -1559,22 +1645,6 @@ const App = () => { )} - - - - {/* GPS Tracker - always active */} - {visibleOnPage('weather') && ( @@ -1592,4 +1662,4 @@ const App = () => { ); }; -export default App; \ No newline at end of file +export default App;