diff --git a/android/app/build.gradle b/android/app/build.gradle index a957c39..b87e49c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -20,7 +20,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "1.0.0" + versionName "deriva_sonora_v1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/build-apk.sh b/build-apk.sh index edccd3b..ec18077 100755 --- a/build-apk.sh +++ b/build-apk.sh @@ -56,7 +56,7 @@ timestamp=$(date +"%Y%m%d-%H%M%S") # Development feature keyword system - Recording Error Fixes # You can modify this keyword based on the current development focus -DEV_KEYWORD="topbar-rebalance" +DEV_KEYWORD="map-zoom-derive-colors" # Alternative keywords for different features: # DEV_KEYWORD="spatial-audio-nearby" # For proximity-based spatial audio diff --git a/src/components/AliasPrompt.jsx b/src/components/AliasPrompt.jsx index 3e610d2..6783d84 100644 --- a/src/components/AliasPrompt.jsx +++ b/src/components/AliasPrompt.jsx @@ -17,14 +17,14 @@ const AliasPrompt = ({ onSubmit, onCancel }) => { left: 0, right: 0, bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.7)', + backgroundColor: 'rgb(20 50 20 / 65%)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 }}>
{ margin: '0 0 8px 0', fontSize: '18px', fontWeight: '600', - color: '#111827' + color: '#000000c9' }}> Tu nombre de caminante @@ -57,7 +57,7 @@ const AliasPrompt = ({ onSubmit, onCancel }) => { style={{ width: '100%', padding: '10px 12px', - border: '1px solid #D1D5DB', + border: '1px solid rgba(78,78,134,0.22)', borderRadius: '8px', fontSize: '16px', outline: 'none', @@ -72,7 +72,7 @@ const AliasPrompt = ({ onSubmit, onCancel }) => { style={{ flex: 1, padding: '10px', - backgroundColor: alias.trim() ? '#10B981' : '#9CA3AF', + backgroundColor: alias.trim() ? '#9dc04cd4' : '#9CA3AF', color: 'white', border: 'none', borderRadius: '8px', @@ -88,9 +88,9 @@ const AliasPrompt = ({ onSubmit, onCancel }) => { onClick={onCancel} style={{ padding: '10px 16px', - backgroundColor: 'white', - color: '#374151', - border: '1px solid #D1D5DB', + backgroundColor: '#f0f1ec', + color: 'rgb(1 9 2 / 84%)', + border: '1px solid rgba(78,78,134,0.22)', borderRadius: '8px', fontSize: '14px', cursor: 'pointer' diff --git a/src/components/BreadcrumbVisualization.jsx b/src/components/BreadcrumbVisualization.jsx index d01aee2..a13a42d 100644 --- a/src/components/BreadcrumbVisualization.jsx +++ b/src/components/BreadcrumbVisualization.jsx @@ -5,14 +5,14 @@ import L from 'leaflet'; // Create custom icons for breadcrumb markers const createBreadcrumbIcon = (isMoving, audioLevel, size = 8) => { // Color based on movement and audio level - let color = '#3B82F6'; // blue (default) + let color = '#4e4e86'; // blue (default) if (isMoving) { - if (audioLevel > 0.7) color = '#EF4444'; // red (high audio, moving) + if (audioLevel > 0.7) color = '#c24a6e'; // red (high audio, moving) else if (audioLevel > 0.4) color = '#F59E0B'; // amber (medium audio, moving) - else color = '#10B981'; // green (low audio, moving) + else color = '#9dc04cd4'; // green (low audio, moving) } else { - if (audioLevel > 0.7) color = '#8B5CF6'; // purple (high audio, stationary) + if (audioLevel > 0.7) color = '#6a6aad'; // purple (high audio, stationary) else if (audioLevel > 0.4) color = '#EC4899'; // pink (medium audio, stationary) else color = '#6B7280'; // gray (low audio, stationary) } @@ -40,7 +40,7 @@ const createDirectionIcon = (direction, size = 16) => { html: `
+
this.props.getPreviousRecording(this.props.point)} className="fas fa-chevron-left text-4xl text-white hover:text-black cursor-pointer m-2 md:m-12">
diff --git a/src/components/LandingPage.jsx b/src/components/LandingPage.jsx index d58ea35..a45ed48 100644 --- a/src/components/LandingPage.jsx +++ b/src/components/LandingPage.jsx @@ -51,7 +51,7 @@ const LandingPage = ({ onModeSelect, hasRequestedPermission, setHasRequestedPerm boxSizing: 'border-box' }}>
e.currentTarget.style.backgroundColor = 'rgba(16, 185, 129, 1)'} - onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(16, 185, 129, 0.8)'} + onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(157, 192, 76, 1)'} + onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(157, 192, 76, 0.83)'} > Entrar @@ -139,7 +139,7 @@ const LandingPage = ({ onModeSelect, hasRequestedPermission, setHasRequestedPerm display: 'flex', alignItems: 'center', gap: '12px', - backgroundColor: 'rgba(16, 185, 129, 0.8)', + backgroundColor: 'rgba(157, 192, 76, 0.83)', color: 'white', border: 'none', borderRadius: '8px', @@ -150,8 +150,8 @@ const LandingPage = ({ onModeSelect, hasRequestedPermission, setHasRequestedPerm transition: 'all 0.2s', textAlign: 'left' }} - onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(16, 185, 129, 1)'} - onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(16, 185, 129, 0.8)'} + onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(157, 192, 76, 1)'} + onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(157, 192, 76, 0.83)'} >
e.currentTarget.style.backgroundColor = 'rgba(59, 130, 246, 1)'} - onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(59, 130, 246, 0.8)'} + onMouseOver={(e) => e.currentTarget.style.backgroundColor = 'rgba(78, 78, 134, 1)'} + onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'rgba(78, 78, 134, 0.84)'} >
) : (
@@ -238,18 +238,18 @@ const LandingPage = ({ onModeSelect, hasRequestedPermission, setHasRequestedPerm style={{ width: '100%', padding: '12px 16px', - border: passwordError ? '2px solid #EF4444' : '2px solid #E5E7EB', + border: passwordError ? '2px solid #c24a6e' : '2px solid rgba(78,78,134,0.15)', borderRadius: '8px', fontSize: '14px', outline: 'none', marginBottom: '8px', boxSizing: 'border-box', - backgroundColor: '#fff' + backgroundColor: '#f0f1ec' }} /> {passwordError && (
e.currentTarget.style.backgroundColor = '#2563EB'} - onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#3B82F6'} + onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#3d3d6b'} + onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#4e4e86'} > Ingresar @@ -343,9 +343,9 @@ const LandingPage = ({ onModeSelect, hasRequestedPermission, setHasRequestedPerm bottom: '50%', left: '50%', transform: 'translate(-50%, 50%)', - backgroundColor: '#ffffffbf', + backgroundColor: 'rgba(220,225,235,0.78)', borderRadius: '16px', - boxShadow: 'rgb(157 58 58 / 30%) 0px 10px 30px', + boxShadow: 'rgba(78,78,134,0.25) 0px 10px 30px', backdropFilter: 'blur(12px)', width: '90%', maxWidth: '400px', @@ -374,7 +374,7 @@ const LandingPage = ({ onModeSelect, hasRequestedPermission, setHasRequestedPerm ✕ -
+

SoundWalk es un dispositivo para crear mapas sonoros comunitarios. Genera derivas sonoras que capturan la diversidad acustica y facilitan diff --git a/src/components/SessionHistoryPanel.jsx b/src/components/SessionHistoryPanel.jsx index 5107f40..ebfde32 100644 --- a/src/components/SessionHistoryPanel.jsx +++ b/src/components/SessionHistoryPanel.jsx @@ -59,9 +59,9 @@ const SessionHistoryPanel = ({ onClose, onViewSession, onExportSession, visibleS bottom: '190px', left: '50%', transform: 'translateX(-50%)', - backgroundColor: '#ffffffbf', + backgroundColor: 'rgba(220,225,235,0.78)', borderRadius: '16px', - boxShadow: 'rgb(157 58 58 / 30%) 0px 10px 30px', + boxShadow: 'rgba(78,78,134,0.25) 0px 10px 30px', width: '90%', maxWidth: '400px', maxHeight: '50vh', @@ -80,7 +80,7 @@ const SessionHistoryPanel = ({ onClose, onViewSession, onExportSession, visibleS borderBottom: '1px solid rgba(0,0,0,0.08)', position: 'relative' }}> -

+

Capas de Derivas

{/* GPS/Recenter Button */} @@ -591,7 +592,7 @@ class SharedTopBar extends React.Component { }} title="Seleccionar Capa de Mapa" > - + {this.state.layerMenuOpen && (
( )} @@ -745,7 +748,7 @@ class SharedTopBar extends React.Component { > ✕ -
+
Barra superior
@@ -784,6 +787,19 @@ class SharedTopBar extends React.Component { }}> Pellizca para zoom. Toca marcadores para escuchar. Cada grabacion incluye coordenadas, hora y metadatos.
+
+
Creditos
+
+ Leafletleafletjs.com + OpenStreetMapopenstreetmap.org + OpenTopoMapopentopomap.org + CARTOcarto.com + HOThotosm.org + Stadia Mapsstadiamaps.com + Esriarcgis.com + CyclOSMcyclosm.org +
+
)} @@ -810,9 +826,9 @@ class SharedTopBar extends React.Component { bottom: '190px', left: '50%', transform: 'translateX(-50%)', - backgroundColor: '#ffffffbf', + backgroundColor: 'rgba(220,225,235,0.78)', borderRadius: '16px', - boxShadow: 'rgb(157 58 58 / 30%) 0px 10px 30px', + boxShadow: 'rgba(78,78,134,0.25) 0px 10px 30px', padding: '20px', minWidth: '300px', maxWidth: '400px', @@ -852,7 +868,7 @@ class SharedTopBar extends React.Component { alignItems: 'center', gap: '6px', padding: '8px 20px', - backgroundColor: this.state.derivePaused ? '#F59E42' : '#3B82F6', + backgroundColor: this.state.derivePaused ? '#F59E42' : '#4e4e86', color: 'white', border: 'none', borderRadius: '8px', @@ -875,7 +891,7 @@ class SharedTopBar extends React.Component { style={{ width: '100%', padding: '10px 12px', - border: '1px solid #D1D5DB', + border: '1px solid rgba(78,78,134,0.22)', borderRadius: '8px', fontSize: '14px', outline: 'none', @@ -894,7 +910,7 @@ class SharedTopBar extends React.Component { style={{ flex: 1, padding: '10px', - backgroundColor: '#10B981', + backgroundColor: '#9dc04cd4', color: 'white', border: 'none', borderRadius: '8px', @@ -910,8 +926,8 @@ class SharedTopBar extends React.Component { style={{ padding: '10px 16px', backgroundColor: 'white', - color: '#374151', - border: '1px solid #D1D5DB', + color: 'rgb(1 9 2 / 84%)', + border: '1px solid rgba(78,78,134,0.22)', borderRadius: '8px', fontSize: '14px', cursor: 'pointer' diff --git a/src/components/TracklogImportModal.jsx b/src/components/TracklogImportModal.jsx index c677807..9ba2f69 100644 --- a/src/components/TracklogImportModal.jsx +++ b/src/components/TracklogImportModal.jsx @@ -143,9 +143,9 @@ const TracklogImportModal = ({ isVisible, onClose, onImportComplete }) => { bottom: '190px', left: '50%', transform: 'translateX(-50%)', - backgroundColor: '#ffffffbf', + backgroundColor: 'rgba(220,225,235,0.78)', borderRadius: '16px', - boxShadow: 'rgb(157 58 58 / 30%) 0px 10px 30px', + boxShadow: 'rgba(78,78,134,0.25) 0px 10px 30px', width: '90%', maxWidth: '400px', maxHeight: '60vh', @@ -167,7 +167,7 @@ const TracklogImportModal = ({ isVisible, onClose, onImportComplete }) => { margin: 0, fontSize: '16px', fontWeight: '600', - color: '#1F2937' + color: '#000000c9' }}> Importar Deriva @@ -194,7 +194,7 @@ const TracklogImportModal = ({ isVisible, onClose, onImportComplete }) => {
{ onDrop={handleDrop} onClick={() => fileInputRef.current?.click()} > - +

Selecciona un archivo .zip

@@ -244,14 +244,14 @@ const TracklogImportModal = ({ isVisible, onClose, onImportComplete }) => { }}>
{validationResult.valid ? ( - + ) : ( - + )} {validationResult.valid ? 'Archivo válido' : 'Error de validación'} @@ -277,7 +277,7 @@ const TracklogImportModal = ({ isVisible, onClose, onImportComplete }) => { margin: '0 0 16px 0', fontSize: '16px', fontWeight: '600', - color: '#1F2937' + color: '#000000c9' }}> ⚙️ Opciones de Importación @@ -290,7 +290,7 @@ const TracklogImportModal = ({ isVisible, onClose, onImportComplete }) => { gap: '8px', fontSize: '14px', fontWeight: '500', - color: '#374151', + color: 'rgb(1 9 2 / 84%)', cursor: 'pointer' }}> { padding: '16px', backgroundColor: '#F9FAFB', borderRadius: '8px', - border: '1px solid #E5E7EB' + border: '1px solid rgba(78,78,134,0.15)' }}>
- + Transformación de Ubicación `; @@ -126,6 +127,9 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer // Debug state const [showDebugInfo, setShowDebugInfo] = useState(false); + // Draggable reproductor + const { position: playerDragPos, handlePointerDown: onPlayerDragStart } = useDraggable(); + // Walk session & recording state const [showAliasPrompt, setShowAliasPrompt] = useState(false); const [isAudioRecorderVisible, setIsAudioRecorderVisible] = useState(false); @@ -151,7 +155,7 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer const prev = lastCenteredRef.current; const curr = userLocation; if (!prev) { - mapInstance.setView([curr.lat, curr.lng], 16); + mapInstance.setView([curr.lat, curr.lng], 15); lastCenteredRef.current = { lat: curr.lat, lng: curr.lng }; } else { const R = 6371e3; @@ -165,7 +169,7 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; if (distance > 10) { // 10 meters threshold - mapInstance.setView([curr.lat, curr.lng], 16); + mapInstance.setView([curr.lat, curr.lng], 15); lastCenteredRef.current = { lat: curr.lat, lng: curr.lng }; } } @@ -846,7 +850,6 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer const audioUrl = src.type === 'blob' ? URL.createObjectURL(src.blob) : src.url; const audio = new Audio(audioUrl); audio.volume = isMuted ? 0 : volume; - audio.loop = false; // NO LOOPING as requested audioRefs.current.push(audio); audioElements.push(audio); @@ -888,15 +891,30 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer }); await Promise.all(playPromises); - - // Set up global end handler (when any audio ends, stop all) - audioElements.forEach(audio => { - audio.addEventListener('ended', () => { - console.log('🏁 Audio ended in Jamm mode, stopping all'); - stopAllAudio(); - }); + + // Wait for all durations, then loop shorter files; longest is the leader + const durationsReady = audioElements.map(audio => + new Promise(resolve => { + if (audio.duration && isFinite(audio.duration)) resolve(audio.duration); + else audio.addEventListener('loadedmetadata', () => resolve(audio.duration), { once: true }); + }) + ); + const durations = await Promise.all(durationsReady); + const maxDuration = Math.max(...durations); + const leaderIndex = durations.indexOf(maxDuration); + + audioElements.forEach((audio, i) => { + if (i === leaderIndex) { + audio.loop = false; + audio.addEventListener('ended', () => { + console.log('🏁 Longest audio ended in Jamm mode, stopping all'); + stopAllAudio(); + }); + } else { + audio.loop = true; + } }); - + console.log('✅ Jamm mode playback started successfully'); } } catch (error) { @@ -1088,14 +1106,19 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer const sessions = walkSessionService.getCompletedSessions(); const lines = sessions .filter(s => s.breadcrumbs && s.breadcrumbs.length >= 2) - .map(s => ({ - sessionId: s.sessionId, - positions: s.breadcrumbs.map(b => [b.lat, b.lng]), - color: userAliasService.aliasToHexColor(s.userAlias), - alias: s.userAlias, - title: s.title || 'Deriva sin título', - recordingCount: s.recordingIds?.length || 0 - })); + .map((s, index) => { + // Golden-angle hue distribution: each session gets a visually distinct color + const hue = Math.round((index * 137.508) % 360); + const color = `hsl(${hue}, 70%, 50%)`; + return { + sessionId: s.sessionId, + positions: s.breadcrumbs.map(b => [b.lat, b.lng]), + color, + alias: s.userAlias, + title: s.title || 'Deriva sin título', + recordingCount: s.recordingIds?.length || 0 + }; + }); setSessionTracklines(lines); // Initialize all sessions as visible setVisibleSessionIds(new Set(sessions.map(s => s.sessionId))); @@ -1306,9 +1329,10 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer {/* Map */} {/* OpenStreetMap Layer */} @@ -1350,13 +1374,25 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer opacity={currentLayer === 'StadiaSatellite' ? 1 : 0} zIndex={currentLayer === 'StadiaSatellite' ? 1 : 0} /> + + {userLocation && ( <> )} @@ -1409,19 +1445,20 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer /> )} - {/* Saved session tracklines (per-user colored, filtered by visibility) */} + {/* Saved session tracklines (per-derive colored, filtered by visibility) */} {sessionTracklines.filter(track => visibleSessionIds.has(track.sessionId)).map(track => ( + {track.alias} — {track.title}
{track.recordingCount} grabacion(es) @@ -1436,7 +1473,7 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer onBackToLanding={handleBackToMenu} onLocationRefresh={() => { if (mapInstance && userLocation) { - mapInstance.setView([userLocation.lat, userLocation.lng], 16); + mapInstance.setView([userLocation.lat, userLocation.lng], 15); lastCenteredRef.current = { lat: userLocation.lat, lng: userLocation.lng }; } }} @@ -1477,10 +1514,10 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer width: '56px', height: '56px', borderRadius: '50%', - backgroundColor: '#EF4444', + backgroundColor: '#c24a6e', color: 'white', border: 'none', - boxShadow: '0 4px 12px rgba(239,68,68,0.4)', + boxShadow: '0 4px 12px rgba(194,74,110,0.4)', cursor: 'pointer', display: 'flex', alignItems: 'center', @@ -1507,7 +1544,7 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer width: '56px', height: '56px', borderRadius: '50%', - backgroundColor: '#10B981', + backgroundColor: '#9dc04cd4', color: 'white', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.3)', @@ -1530,10 +1567,10 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer position: 'fixed', bottom: '190px', left: '50%', - transform: 'translateX(-50%)', - backgroundColor: '#ffffffbf', + transform: `translate(calc(-50% + ${playerDragPos.x}px), ${playerDragPos.y}px)`, + backgroundColor: 'rgba(220,225,235,0.78)', borderRadius: '16px', - boxShadow: 'rgb(157 58 58 / 30%) 0px 10px 30px', + boxShadow: 'rgba(78,78,134,0.25) 0px 10px 30px', padding: '20px', minWidth: '300px', maxWidth: '400px', @@ -1552,13 +1589,17 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer color: '#6B7280', fontSize: '18px', padding: '4px', - lineHeight: 1 + lineHeight: 1, + zIndex: 1 }} title="Cerrar reproductor" > ✕ -
+

{sessionPlayback ? sessionPlayback.title : 'Reproductor'}

@@ -1579,9 +1620,9 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer Modo de Reproducción:
- - - + + +
{currentAudio && ( @@ -1603,22 +1644,34 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer }} style={{ display: 'flex', alignItems: 'center', gap: '8px', - backgroundColor: (nearbySpots.length > 0 || selectedSpot) ? '#3B82F6' : '#6B7280', + backgroundColor: (nearbySpots.length > 0 || selectedSpot) ? '#4e4e86' : '#6B7280', color: 'white', border: 'none', borderRadius: '8px', padding: '8px 16px', fontSize: '14px', cursor: (nearbySpots.length > 0 || selectedSpot) ? 'pointer' : 'not-allowed', transition: 'background-color 0.2s' }} > Reproducir - -
- + handleVolumeChange(Number(e.target.value))} /> +
)} @@ -1638,13 +1691,13 @@ const SoundWalkAndroid = ({ onBackToLanding, locationPermission: propLocationPer zIndex: 2000 }}>
-
-

Cargando audio...

+
+

Cargando audio...

)} diff --git a/src/hooks/useDraggable.js b/src/hooks/useDraggable.js new file mode 100644 index 0000000..68320a2 --- /dev/null +++ b/src/hooks/useDraggable.js @@ -0,0 +1,44 @@ +import { useState, useCallback, useRef } from 'react'; + +export default function useDraggable() { + const [position, setPosition] = useState({ x: 0, y: 0 }); + const dragging = useRef(false); + const startPos = useRef({ x: 0, y: 0 }); + const startOffset = useRef({ x: 0, y: 0 }); + + const handlePointerDown = useCallback((e) => { + // Only primary button + if (e.button !== 0) return; + dragging.current = true; + startPos.current = { x: e.clientX, y: e.clientY }; + startOffset.current = { ...position }; + e.currentTarget.setPointerCapture(e.pointerId); + e.currentTarget.style.cursor = 'grabbing'; + + const target = e.currentTarget; + + const handlePointerMove = (ev) => { + if (!dragging.current) return; + const dx = ev.clientX - startPos.current.x; + const dy = ev.clientY - startPos.current.y; + setPosition({ + x: startOffset.current.x + dx, + y: startOffset.current.y + dy + }); + }; + + const handlePointerUp = () => { + dragging.current = false; + target.style.cursor = 'grab'; + document.removeEventListener('pointermove', handlePointerMove); + document.removeEventListener('pointerup', handlePointerUp); + }; + + document.addEventListener('pointermove', handlePointerMove); + document.addEventListener('pointerup', handlePointerUp); + }, [position]); + + const resetPosition = useCallback(() => setPosition({ x: 0, y: 0 }), []); + + return { position, handlePointerDown, resetPosition }; +} diff --git a/src/index.css b/src/index.css index 68e9b32..4002f73 100644 --- a/src/index.css +++ b/src/index.css @@ -498,6 +498,33 @@ button, cursor: pointer; } +/* ── Nature-Tech Palette ── */ +:root { + --nt-primary: #4e4e86; + --nt-primary-alpha: rgba(78,78,134,0.84); + --nt-primary-light: #6a6aad; + --nt-success: #9dc04cd4; + --nt-success-alpha: rgba(157,192,76,0.83); + --nt-success-bg: rgba(157,192,76,0.12); + --nt-danger: #c24a6e; + --nt-danger-hover: #a83d5a; + --nt-danger-alpha-low: rgba(194,74,110,0.4); + --nt-danger-alpha-high: rgba(194,74,110,0.8); + --nt-text-primary: #000000c9; + --nt-text-secondary: rgb(1 9 2 / 84%); + --nt-panel-bg: rgba(220,225,235,0.78); + --nt-bar-btn-bg: rgba(201,206,177,0.50); + --nt-bar-bg: rgba(220,225,235,0.92); + --nt-card-bg: #f0f1ec; + --nt-card-overlay: rgb(20 50 20 / 35%); + --nt-modal-overlay: rgb(20 50 20 / 65%); + --nt-detail-gradient: rgb(20 50 20 / 85%); + --nt-border-light: rgba(78,78,134,0.15); + --nt-border-medium: rgba(78,78,134,0.22); + --nt-shadow: rgba(78,78,134,0.18); + --nt-shadow-panel: rgba(78,78,134,0.25); +} + table { border-collapse: collapse; } @@ -28708,23 +28735,23 @@ table { /* Custom marker styles */ .marker { - background-color: #4f46e5; + background-color: var(--nt-primary); border: 2px solid #fff; border-radius: 50%; cursor: pointer; width: 20px; height: 20px; - box-shadow: 0 2px 4px rgba(0,0,0,0.3); + box-shadow: 0 2px 4px var(--nt-shadow); } .marker.selected { - background-color: #dc2626; + background-color: var(--nt-danger); border-color: #fef2f2; transform: scale(1.2); } .marker.results { - background-color: #059669; + background-color: var(--nt-success); border-color: #ecfdf5; transform: scale(1.1); } @@ -28768,13 +28795,13 @@ table { @keyframes microphone-pulse { 0% { - box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), 0 4px 12px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 20px rgba(194, 74, 110, 0.6), 0 4px 12px var(--nt-shadow); } 50% { - box-shadow: 0 0 30px rgba(239, 68, 68, 0.8), 0 4px 12px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 30px rgba(194, 74, 110, 0.8), 0 4px 12px var(--nt-shadow); } 100% { - box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), 0 4px 12px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 20px rgba(194, 74, 110, 0.6), 0 4px 12px var(--nt-shadow); } } @@ -28786,19 +28813,19 @@ table { left: 0 !important; right: 0 !important; bottom: 0 !important; - background-color: rgba(0, 0, 0, 0.5) !important; + background-color: var(--nt-modal-overlay) !important; display: flex !important; align-items: center !important; justify-content: center !important; } .location-permission-content { - background: white !important; + background: var(--nt-card-bg) !important; border-radius: 8px !important; padding: 24px !important; max-width: 400px !important; margin: 16px !important; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04) !important; + box-shadow: 0 20px 25px -5px var(--nt-shadow), 0 10px 10px -5px var(--nt-shadow) !important; position: relative !important; z-index: 10000 !important; } @@ -28811,7 +28838,7 @@ table { /* Ensure other components appear above map */ .gradient { z-index: 1000; - background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 50%, transparent 100%); + background: linear-gradient(to top, var(--nt-detail-gradient) 0%, rgb(20 50 20 / 40%) 50%, transparent 100%); } /* TopBar positioning */ @@ -28819,40 +28846,27 @@ table { z-index: 1001; } -/* Leaflet attribution — compact dark */ -.leaflet-control-attribution { - font-size: 7px !important; - padding: 1px 4px !important; - line-height: 1.2 !important; - background: rgba(0, 0, 0, 0.45) !important; - color: rgba(255, 255, 255, 0.7) !important; - border-radius: 4px 0 0 0 !important; -} -.leaflet-control-attribution a { - color: rgba(255, 255, 255, 0.8) !important; -} - /* Leaflet Layer Control Styling */ .leaflet-control-layers { - background: white !important; - border: 2px solid rgba(0,0,0,0.2) !important; + background: var(--nt-card-bg) !important; + border: 2px solid var(--nt-border-light) !important; border-radius: 4px !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; + box-shadow: 0 2px 4px var(--nt-shadow) !important; padding: 8px !important; } .leaflet-control-layers-toggle { - background: white !important; - border: 2px solid rgba(0,0,0,0.2) !important; + background: var(--nt-card-bg) !important; + border: 2px solid var(--nt-border-light) !important; border-radius: 4px !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; + box-shadow: 0 2px 4px var(--nt-shadow) !important; } .leaflet-control-layers-expanded { - background: white !important; - border: 2px solid rgba(0,0,0,0.2) !important; + background: var(--nt-card-bg) !important; + border: 2px solid var(--nt-border-light) !important; border-radius: 4px !important; - box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important; + box-shadow: 0 2px 4px var(--nt-shadow) !important; padding: 8px !important; min-width: 200px !important; } @@ -28870,12 +28884,12 @@ table { /* Microphone icon styling */ .microphone-button { - background: #ef4444 !important; + background: var(--nt-danger) !important; color: white !important; border: 2px solid white !important; border-radius: 50% !important; padding: 8px !important; - box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important; + box-shadow: 0 4px 8px var(--nt-shadow) !important; transition: all 0.3s ease !important; } @@ -28914,22 +28928,22 @@ table { @keyframes microphone-pulse { 0% { transform: translate(-50%, 50%) scale(1); - box-shadow: 0 8px 25px rgba(239, 68, 68, 0.5), 0 4px 15px rgba(0, 0, 0, 0.3); + box-shadow: 0 8px 25px rgba(194, 74, 110, 0.5), 0 4px 15px var(--nt-shadow); } 50% { transform: translate(-50%, 50%) scale(1.05); - box-shadow: 0 12px 35px rgba(239, 68, 68, 0.7), 0 6px 20px rgba(0, 0, 0, 0.4); + box-shadow: 0 12px 35px rgba(194, 74, 110, 0.7), 0 6px 20px var(--nt-shadow); } 100% { transform: translate(-50%, 50%) scale(1); - box-shadow: 0 8px 25px rgba(239, 68, 68, 0.5), 0 4px 15px rgba(0, 0, 0, 0.3); + box-shadow: 0 8px 25px rgba(194, 74, 110, 0.5), 0 4px 15px var(--nt-shadow); } } .microphone-button:hover { - background: #dc2626 !important; + background: var(--nt-danger-hover) !important; transform: scale(1.05) !important; - box-shadow: 0 6px 12px rgba(0,0,0,0.4) !important; + box-shadow: 0 6px 12px var(--nt-shadow) !important; } .microphone-button svg { @@ -28940,13 +28954,13 @@ table { /* Animation for microphone pulse */ @keyframes microphone-pulse { 0% { - box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); + box-shadow: 0 0 0 0 rgba(194, 74, 110, 0.7); } 70% { - box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); + box-shadow: 0 0 0 10px rgba(194, 74, 110, 0); } 100% { - box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); + box-shadow: 0 0 0 0 rgba(194, 74, 110, 0); } } @@ -28999,7 +29013,7 @@ table { margin: 0 0 12px 0; font-size: 18px; font-weight: 600; - color: #1F2937; + color: #000000c9; text-align: center; } @@ -29019,7 +29033,7 @@ table { width: 100%; padding: 8px 12px; border-radius: 6px; - border: 2px solid #D1D5DB; + border: 2px solid rgba(78,78,134,0.22); font-size: 16px; background-color: white; cursor: pointer; diff --git a/src/services/AudioRecorder.tsx b/src/services/AudioRecorder.tsx index 77926be..ca765cc 100644 --- a/src/services/AudioRecorder.tsx +++ b/src/services/AudioRecorder.tsx @@ -3,6 +3,7 @@ import { Mic, Square, Play, Pause, Save, X } from 'lucide-react'; import audioService from './audioService.js'; import { VoiceRecorder } from 'capacitor-voice-recorder'; import breadcrumbService from './breadcrumbService.js'; +import useDraggable from '../hooks/useDraggable.js'; // Custom alert function for Android without localhost text const showAlert = (message) => { @@ -11,21 +12,21 @@ const showAlert = (message) => { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.7); z-index: 10000; + background: rgb(20 50 20 / 65%); z-index: 10000; display: flex; align-items: center; justify-content: center; `; const modal = document.createElement('div'); modal.style.cssText = ` - background: rgba(255, 255, 255, 0.85); border-radius: 8px; padding: 20px; + background: rgba(220,225,235,0.92); border-radius: 8px; padding: 20px; max-width: 300px; margin: 20px; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.3); `; modal.innerHTML = ` -

${message}

+

${message}

`; @@ -142,6 +143,7 @@ const AudioRecorder = ({ const [nativeRecordingPath, setNativeRecordingPath] = useState(null); const [audioBlob, setAudioBlob] = useState(null); + const { position: dragPos, handlePointerDown: onDragStart } = useDraggable(); const audioRef = useRef(null); const timerRef = useRef(null); @@ -581,14 +583,14 @@ const AudioRecorder = ({ position: 'fixed', bottom: '190px', left: '50%', - transform: 'translateX(-50%)', + transform: `translate(calc(-50% + ${dragPos.x}px), ${dragPos.y}px)`, zIndex: 999999, pointerEvents: 'auto' }}>
-

{isRecording ? 'Recording...' : nativeRecordingPath ? 'Review Recording' : 'Audio Recorder'} @@ -624,7 +630,7 @@ const AudioRecorder = ({ style={{ color: '#6B7280', backgroundColor: 'transparent', - border: '1px solid #D1D5DB', + border: '1px solid rgba(78,78,134,0.22)', borderRadius: '4px', cursor: 'pointer', padding: '4px 8px', @@ -677,16 +683,16 @@ const AudioRecorder = ({ {showLogViewer && (
- +