feat: add 3-way audio mode, viral share API, offline road-ready indicator, multi-app navigation (Google/Apple/Waze), and PWA car HUD install (v25)

This commit is contained in:
2026-09-12 18:00:41 -07:00
parent fbe0bcea9f
commit 6783a0b3ba
4 changed files with 509 additions and 57 deletions
+232
View File
@@ -850,6 +850,95 @@ body {
transform: scale(0.98); transform: scale(0.98);
} }
/* Audio Alert Mode 3-Way Selector */
.audio-mode-selector {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
background: rgba(15, 23, 42, 0.7);
padding: 3px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
margin-top: 2px;
}
.btn-audio-mode {
padding: 6px 4px;
font-size: 0.72rem;
font-weight: 600;
border-radius: 6px;
background: transparent;
border: none;
color: #94a3b8;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
transition: all 0.18s ease;
white-space: nowrap;
}
.btn-audio-mode.active {
background: #0ea5e9;
color: #ffffff;
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.4);
}
.btn-audio-mode:hover:not(.active) {
color: #f1f5f9;
background: rgba(255, 255, 255, 0.05);
}
/* Offline Road-Ready Status Pill */
.tools-offline-status-pill {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: 8px;
font-size: 0.72rem;
color: #a7f3d0;
font-weight: 500;
margin-bottom: 2px;
}
.tools-offline-status-pill.offline-mode {
background: rgba(245, 158, 11, 0.15);
border-color: rgba(245, 158, 11, 0.35);
color: #fcd34d;
}
.dot-online {
width: 7px;
height: 7px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 6px #10b981;
flex-shrink: 0;
}
.dot-offline {
width: 7px;
height: 7px;
border-radius: 50%;
background: #f59e0b;
box-shadow: 0 0 6px #f59e0b;
flex-shrink: 0;
}
.btn-share-accent {
background: rgba(56, 189, 248, 0.12) !important;
border-color: rgba(56, 189, 248, 0.3) !important;
color: #38bdf8 !important;
}
.btn-share-accent:hover {
background: rgba(56, 189, 248, 0.22) !important;
}
.btn-hud-action { .btn-hud-action {
padding: 9px 16px; padding: 9px 16px;
background: var(--bg-hud); background: var(--bg-hud);
@@ -3636,6 +3725,137 @@ body {
background: rgba(148, 163, 184, 0.25); background: rgba(148, 163, 184, 0.25);
} }
/* Multi-App Navigation Row & Pin Share */
.proof-nav-apps-block {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
gap: 8px;
background: rgba(15, 23, 42, 0.7);
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
margin-top: 4px;
}
.proof-nav-label {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.8px;
color: #64748b;
text-transform: uppercase;
}
.proof-nav-btn-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.btn-nav-app-link {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
padding: 8px 6px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
border: 1px solid transparent;
}
.nav-google {
background: rgba(66, 133, 244, 0.15);
border-color: rgba(66, 133, 244, 0.35);
color: #93c5fd;
}
.nav-google:hover {
background: rgba(66, 133, 244, 0.28);
color: #ffffff;
}
.nav-apple {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
color: #f1f5f9;
}
.nav-apple:hover {
background: rgba(255, 255, 255, 0.18);
color: #ffffff;
}
.nav-waze {
background: rgba(51, 204, 255, 0.15);
border-color: rgba(51, 204, 255, 0.35);
color: #67e8f9;
}
.nav-waze:hover {
background: rgba(51, 204, 255, 0.28);
color: #ffffff;
}
.nav-share {
background: rgba(16, 185, 129, 0.15);
border-color: rgba(16, 185, 129, 0.35);
color: #6ee7b7;
}
.nav-share:hover {
background: rgba(16, 185, 129, 0.28);
color: #ffffff;
}
/* Support Modal Viral Share Banner */
.support-share-banner {
margin: 18px 0 12px;
padding: 14px 16px;
background: linear-gradient(135deg, rgba(30, 41, 59, 0.7), rgba(15, 23, 42, 0.85));
border: 1px dashed rgba(56, 189, 248, 0.35);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.share-banner-title {
display: block;
font-size: 0.88rem;
font-weight: 700;
color: #f8fafc;
margin-bottom: 2px;
}
.share-banner-desc {
font-size: 0.74rem;
color: #94a3b8;
margin: 0;
}
.btn-support-share {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #0284c7;
border: none;
border-radius: 8px;
color: #ffffff;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.btn-support-share:hover {
background: #0369a1;
transform: translateY(-1px);
}
.photo-preview-box.hidden { .photo-preview-box.hidden {
display: none !important; display: none !important;
} }
@@ -3645,6 +3865,18 @@ body {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 10px; gap: 10px;
} }
.proof-nav-btn-grid {
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.support-share-banner {
flex-direction: column;
align-items: flex-start;
}
.btn-support-share {
width: 100%;
justify-content: center;
}
} }
/* Photo Verified Pin & Contested Pin on Map */ /* Photo Verified Pin & Contested Pin on Map */
+75 -13
View File
@@ -54,7 +54,7 @@
<meta name="apple-mobile-web-app-title" content="FlockRadar"> <meta name="apple-mobile-web-app-title" content="FlockRadar">
<!-- Main Stylesheet --> <!-- Main Stylesheet -->
<link rel="stylesheet" href="css/styles.css?v=24"> <link rel="stylesheet" href="css/styles.css?v=25">
</head> </head>
<body> <body>
@@ -365,6 +365,42 @@
</button> </button>
</div> </div>
<!-- Driving Audio Alert Mode Selector -->
<div class="tools-group">
<div class="tools-group-label">AUDIO ALERT STYLE</div>
<div class="audio-mode-selector" id="audio-mode-selector">
<button type="button" class="btn-audio-mode active" id="btn-audio-mode-full" data-mode="full" title="Chimes and spoken voice warnings">
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
<span>Voice+Chime</span>
</button>
<button type="button" class="btn-audio-mode" id="btn-audio-mode-chime" data-mode="chime" title="Subtle radar beeps only (no speech)">
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
<span>Chime Only</span>
</button>
<button type="button" class="btn-audio-mode" id="btn-audio-mode-silent" data-mode="silent" title="Visual HUD alerts only (mute audio)">
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="1" y1="1" x2="23" y2="23"/><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/></svg>
<span>Silent HUD</span>
</button>
</div>
</div>
<!-- Offline Status & Viral Sharing -->
<div class="tools-group">
<div class="tools-group-label">ROAD READINESS & SHARING</div>
<div class="tools-offline-status-pill" id="tools-offline-status-pill">
<span class="status-indicator-dot dot-online" id="offline-dot"></span>
<span id="tools-offline-status-text">36 Cameras Cached • Ready for Dead Zones</span>
</div>
<button type="button" id="btn-share-radar-tools" class="btn-hud-action btn-tool-item btn-share-accent" title="Share FlockRadar with other drivers">
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
<span>Share Radar with Drivers</span>
</button>
<button type="button" id="btn-install-hud-app" class="btn-hud-action btn-tool-item" title="Add FlockRadar to Home Screen as a Car HUD App">
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
<span id="install-app-label">Install Car HUD App</span>
</button>
</div>
<div class="tools-group"> <div class="tools-group">
<div class="tools-group-label">DIAGNOSTICS & SYSTEM</div> <div class="tools-group-label">DIAGNOSTICS & SYSTEM</div>
<button id="btn-test-alert" class="btn-hud-action btn-tool-item" title="Immediately test voice alert and radar siren"> <button id="btn-test-alert" class="btn-hud-action btn-tool-item" title="Immediately test voice alert and radar siren">
@@ -1032,12 +1068,27 @@
<span>Contest</span> <span>Contest</span>
</button> </button>
<a id="btn-proof-navigate" href="#" target="_blank" rel="noopener noreferrer" class="btn-proof-action btn-nav-maps" title="Open navigation in Apple/Google Maps"> <div class="proof-nav-apps-block">
<span class="btn-icon"> <div class="proof-nav-label">RE-ROUTE AROUND CAMERA:</div>
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg> <div class="proof-nav-btn-grid">
</span> <a id="btn-proof-nav-google" href="#" target="_blank" rel="noopener noreferrer" class="btn-nav-app-link nav-google" title="Open in Google Maps">
<span>Maps ↗</span> <svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 12 8 12s8-6.75 8-12a8 8 0 0 0-8-8z"/><circle cx="12" cy="10" r="3"/></svg>
</a> <span>Google</span>
</a>
<a id="btn-proof-nav-apple" href="#" target="_blank" rel="noopener noreferrer" class="btn-nav-app-link nav-apple" title="Open in Apple Maps">
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>
<span>Apple Maps</span>
</a>
<a id="btn-proof-nav-waze" href="#" target="_blank" rel="noopener noreferrer" class="btn-nav-app-link nav-waze" title="Open in Waze">
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
<span>Waze</span>
</a>
<button type="button" id="btn-proof-share-cam" class="btn-nav-app-link nav-share" title="Share this camera pin">
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
<span>Share Pin</span>
</button>
</div>
</div>
</div> </div>
<!-- Specific Agency Directory Records & Surveillance Policy --> <!-- Specific Agency Directory Records & Surveillance Policy -->
@@ -1281,7 +1332,18 @@
<span>Choose Amount</span> <span>Choose Amount</span>
<svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg> <svg class="flock-svg flock-svg-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</div> </div>
</a> </div>
<!-- Viral Share Alternative -->
<div class="support-share-banner">
<div class="share-banner-text">
<span class="share-banner-title">Can't donate today? Help us grow!</span>
<p class="share-banner-desc">Share FlockRadar with 3 local drivers or friends to spread surveillance transparency.</p>
</div>
<button type="button" id="btn-support-share-app" class="btn-support-share" title="Share FlockRadar with drivers">
<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
<span>Share FlockRadar</span>
</button>
</div> </div>
<div class="support-security-footer"> <div class="support-security-footer">
@@ -1313,11 +1375,11 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Data & Application Logic with Cache Busting --> <!-- Data & Application Logic with Cache Busting -->
<script src="data/portals.js?v=24"></script> <script src="data/portals.js?v=25"></script>
<script src="js/icons.js?v=24"></script> <script src="js/icons.js?v=25"></script>
<script src="js/auth.js?v=24"></script> <script src="js/auth.js?v=25"></script>
<script src="js/radar.js?v=24"></script> <script src="js/radar.js?v=25"></script>
<script src="js/app.js?v=24"></script> <script src="js/app.js?v=25"></script>
<!-- Service Worker Instant Cache Invalidation --> <!-- Service Worker Instant Cache Invalidation -->
<script> <script>
+189 -31
View File
@@ -95,6 +95,8 @@
let isUserInteractingWithMap = false; let isUserInteractingWithMap = false;
let touchInteractionTimer = null; let touchInteractionTimer = null;
let radarRangeMode = 'city'; // 'city' (1,000 ft) or 'highway' (2,500 ft) let radarRangeMode = 'city'; // 'city' (1,000 ft) or 'highway' (2,500 ft)
let radarAudioMode = localStorage.getItem('flock_radar_audio_mode') || 'full'; // 'full' | 'chime' | 'silent'
let deferredInstallPrompt = null;
let lastCautionSpokenCamId = null; let lastCautionSpokenCamId = null;
// Photo Evidence & Consensus State // Photo Evidence & Consensus State
@@ -602,8 +604,8 @@
el.hudZoneBadge.className = 'hud-badge badge-clear'; el.hudZoneBadge.className = 'hud-badge badge-clear';
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> ALL CLEAR`; el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> ALL CLEAR`;
el.hudCamAgency.textContent = 'Monitoring Highway Network'; el.hudCamAgency.textContent = 'Monitoring Highway Network';
const user = window.FlockAuth ? window.FlockAuth.getUser() : null; setAudioMode(radarAudioMode, false);
if (user) updateSoundUi(user.soundEnabled); updateOfflineStatus();
updateVehicleSupporterAura(); updateVehicleSupporterAura();
} }
@@ -861,6 +863,7 @@
let stationaryChirpCount = 0; let stationaryChirpCount = 0;
function playRadarTick(freq = 800, volume = 0.22) { function playRadarTick(freq = 800, volume = 0.22) {
if (radarAudioMode === 'silent') return;
const user = window.FlockAuth ? window.FlockAuth.getUser() : null; const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user && !user.soundEnabled) return; if (user && !user.soundEnabled) return;
@@ -892,6 +895,7 @@
} }
function handleProximityChirps(distFt) { function handleProximityChirps(distFt) {
if (radarAudioMode === 'silent') return;
const user = window.FlockAuth ? window.FlockAuth.getUser() : null; const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user && !user.soundEnabled) return; if (user && !user.soundEnabled) return;
@@ -945,42 +949,72 @@
playRadarTick(pitchHz, chirpVol); playRadarTick(pitchHz, chirpVol);
} }
function updateSoundUi(soundEnabled) { function setAudioMode(mode, showFeedback = true) {
radarAudioMode = mode;
localStorage.setItem('flock_radar_audio_mode', mode);
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user) {
user.soundEnabled = (mode !== 'silent');
window.FlockAuth.saveUser();
}
document.querySelectorAll('.btn-audio-mode').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
if (el.btnMuteSound) { if (el.btnMuteSound) {
const volIcon = soundEnabled const volIcon = mode !== 'silent'
? (window.FlockIcons ? FlockIcons.volume('flock-svg-sm') : '') ? (window.FlockIcons ? FlockIcons.volume('flock-svg-sm') : '')
: (window.FlockIcons ? FlockIcons.volumeX('flock-svg-sm') : ''); : (window.FlockIcons ? FlockIcons.volumeX('flock-svg-sm') : '');
el.btnMuteSound.innerHTML = `${volIcon}<span>Sound: ${soundEnabled ? 'ON' : 'OFF'}</span>`; el.btnMuteSound.innerHTML = `${volIcon}<span>Sound: ${mode.toUpperCase()}</span>`;
el.btnMuteSound.classList.toggle('active', !soundEnabled); el.btnMuteSound.classList.toggle('active', mode === 'silent');
} }
if (el.btnHudSoundToggle) { if (el.btnHudSoundToggle) {
el.btnHudSoundToggle.classList.toggle('is-muted', !soundEnabled); el.btnHudSoundToggle.classList.toggle('is-muted', mode === 'silent');
const iconWrap = document.getElementById('hud-sound-icon'); const iconWrap = document.getElementById('hud-sound-icon');
if (iconWrap) { const label = el.btnHudSoundToggle.querySelector('.capsule-sound-label');
iconWrap.innerHTML = soundEnabled
? `<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>` if (mode === 'full') {
: `<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>`; if (iconWrap) iconWrap.innerHTML = `<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>`;
if (label) label.textContent = 'Audio';
el.btnHudSoundToggle.title = 'Audio Mode: Full Voice + Radar Chimes • Tap to switch to Chime Only';
if (showFeedback) {
playRadarTick(880, 0.25);
showToast('🔊 Audio Mode: Full Voice + Radar Chimes');
}
} else if (mode === 'chime') {
if (iconWrap) iconWrap.innerHTML = `<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>`;
if (label) label.textContent = 'Chime';
el.btnHudSoundToggle.title = 'Audio Mode: Discreet Chime Only (No Speech) • Tap to Mute';
if (showFeedback) {
playRadarTick(1000, 0.25);
showToast('🔔 Audio Mode: Discreet Radar Chimes Only');
}
} else {
if (iconWrap) iconWrap.innerHTML = `<svg class="flock-svg flock-svg-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>`;
if (label) label.textContent = 'Muted';
el.btnHudSoundToggle.title = 'Audio Mode: Silent (Visual HUD Only) • Tap to Unmute';
if (showFeedback) {
showToast('🔇 Audio Mode: Silent HUD Alerts Only');
}
} }
el.btnHudSoundToggle.title = soundEnabled ? 'Audio Radar Alerts: ON (Tap to Mute)' : 'Audio Radar Alerts: MUTED (Tap to Unmute)';
} }
} }
function toggleSound() { function cycleAudioMode() {
const user = window.FlockAuth ? window.FlockAuth.getUser() : null; if (radarAudioMode === 'full') {
if (user) { setAudioMode('chime', true);
user.soundEnabled = !user.soundEnabled; } else if (radarAudioMode === 'chime') {
window.FlockAuth.saveUser(); setAudioMode('silent', true);
updateSoundUi(user.soundEnabled); } else {
if (user.soundEnabled) { setAudioMode('full', true);
playRadarTick(880, 0.25);
showToast('🔊 Audio Radar Alerts: Active');
} else {
showToast('🔇 Audio Radar Alerts: Muted');
}
} }
} }
function playRadarChime() { function playRadarChime() {
if (radarAudioMode === 'silent') return;
const user = window.FlockAuth ? window.FlockAuth.getUser() : null; const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user && !user.soundEnabled) return; if (user && !user.soundEnabled) return;
@@ -1001,9 +1035,9 @@
const gain = audioContext.createGain(); const gain = audioContext.createGain();
osc.type = 'sine'; osc.type = 'sine';
osc.frequency.setValueAtTime(880, audioContext.currentTime); // High tone osc.frequency.setValueAtTime(880, audioContext.currentTime);
osc.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.12); // Tone 2 osc.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.12);
osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.24); // Return tone osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.24);
gain.gain.setValueAtTime(0.35, audioContext.currentTime); gain.gain.setValueAtTime(0.35, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.45); gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.45);
@@ -1019,6 +1053,7 @@
} }
function speakVoiceAlert(text) { function speakVoiceAlert(text) {
if (radarAudioMode !== 'full') return;
const user = window.FlockAuth ? window.FlockAuth.getUser() : null; const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user && !user.soundEnabled) return; if (user && !user.soundEnabled) return;
if (!window.speechSynthesis) return; if (!window.speechSynthesis) return;
@@ -1040,6 +1075,75 @@
speakVoiceAlert("Warning. Flock camera detected ahead. You are entering a recorded surveillance zone."); speakVoiceAlert("Warning. Flock camera detected ahead. You are entering a recorded surveillance zone.");
} }
// --- VIRAL SHARING HELPER ---
async function shareApp(customTitle, customText, customUrl) {
const shareData = {
title: customTitle || 'FlockRadar • Live Surveillance Camera Detector',
text: customText || 'Check out FlockRadar — real-time live map & radar detector for Flock Safety surveillance cameras with driving alerts:',
url: customUrl || 'https://flockradar.org/'
};
if (navigator.share) {
try {
await navigator.share(shareData);
showToast('Thanks for sharing FlockRadar!');
return;
} catch (err) {
if (err.name === 'AbortError') return;
}
}
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(shareData.url);
showToast('Link copied to clipboard! Share with drivers.');
return;
} catch (err) {
// Fallback below
}
}
prompt('Copy FlockRadar link to share:', shareData.url);
}
// --- OFFLINE ROAD-READINESS & NETWORK MONITOR ---
function updateOfflineStatus() {
const pill = document.getElementById('tools-offline-status-pill');
const dot = document.getElementById('offline-dot');
const text = document.getElementById('tools-offline-status-text');
if (!pill || !text) return;
if (navigator.onLine) {
pill.classList.remove('offline-mode');
if (dot) dot.className = 'status-indicator-dot dot-online';
text.textContent = '36 Cameras Cached • Ready for Dead Zones';
} else {
pill.classList.add('offline-mode');
if (dot) dot.className = 'status-indicator-dot dot-offline';
text.textContent = '⚡ Offline Driving Active • 36 Cameras in Local Cache';
}
}
// --- PWA CAR HUD APP INSTALLATION ---
function handleInstallApp() {
if (deferredInstallPrompt) {
deferredInstallPrompt.prompt();
deferredInstallPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
showToast('FlockRadar added to your Home Screen!');
}
deferredInstallPrompt = null;
});
} else {
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
if (isIos) {
alert("To Install on iPhone:\n\n1. Tap the Share button in Safari (square with up arrow at bottom)\n2. Tap 'Add to Home Screen'\n\nFlockRadar will launch fullscreen on your car dashboard!");
} else {
showToast("To install, tap your browser's menu (⋮) and select 'Install app' or 'Add to Home Screen'.");
}
}
}
// --- TEST ALERT SOUND & VOICE NOW --- // --- TEST ALERT SOUND & VOICE NOW ---
function testAlertSoundAndVoice() { function testAlertSoundAndVoice() {
playRadarTick(950, 0.3); playRadarTick(950, 0.3);
@@ -1852,9 +1956,23 @@
} }
} }
// Navigate link // Multi-App Navigation & Pin Share Links
if (el.btnProofNavigate) { const elGoogle = document.getElementById('btn-proof-nav-google');
el.btnProofNavigate.href = `https://www.google.com/maps/dir/?api=1&destination=${cam.lat},${cam.lng}`; const elApple = document.getElementById('btn-proof-nav-apple');
const elWaze = document.getElementById('btn-proof-nav-waze');
const elShareCam = document.getElementById('btn-proof-share-cam');
if (elGoogle) elGoogle.href = `https://www.google.com/maps/dir/?api=1&destination=${cam.lat},${cam.lng}`;
if (elApple) elApple.href = `https://maps.apple.com/?daddr=${cam.lat},${cam.lng}&dirflg=d`;
if (elWaze) elWaze.href = `https://waze.com/ul?ll=${cam.lat},${cam.lng}&navigate=yes`;
if (elShareCam) {
elShareCam.onclick = () => {
shareApp(
`Flock Camera: ${cam.name}`,
`Flock surveillance camera spotted at ${cam.name} (${cam.agency || 'Public Roadway'}). Check specs on FlockRadar:`,
`https://flockradar.org/?cam=${cam.id}`
);
};
} }
// Lookup Directory Intelligence for this camera & agency // Lookup Directory Intelligence for this camera & agency
@@ -2399,9 +2517,49 @@
el.btnMuteSound.addEventListener('click', toggleSound); el.btnMuteSound.addEventListener('click', toggleSound);
} }
if (el.btnHudSoundToggle) { if (el.btnHudSoundToggle) {
el.btnHudSoundToggle.addEventListener('click', toggleSound); el.btnHudSoundToggle.addEventListener('click', cycleAudioMode);
} }
// Audio Alert Mode Pill Buttons
document.querySelectorAll('.btn-audio-mode').forEach(btn => {
btn.addEventListener('click', () => {
setAudioMode(btn.dataset.mode, true);
});
});
// Viral Share Buttons
const btnShareTools = document.getElementById('btn-share-radar-tools');
if (btnShareTools) {
btnShareTools.addEventListener('click', () => shareApp());
}
const btnShareSupport = document.getElementById('btn-support-share-app');
if (btnShareSupport) {
btnShareSupport.addEventListener('click', () => {
shareApp(
'Support FlockRadar',
'Help us keep local streets transparent and free of unconstitutional surveillance. Check out FlockRadar live map & driving detector:',
'https://flockradar.org/'
);
});
}
// PWA Install Button
const btnInstallApp = document.getElementById('btn-install-hud-app');
if (btnInstallApp) {
btnInstallApp.addEventListener('click', handleInstallApp);
}
// Offline / Online Network Listeners
window.addEventListener('online', () => {
updateOfflineStatus();
showToast('🟢 Back Online: Cloud database sync active.');
});
window.addEventListener('offline', () => {
updateOfflineStatus();
showToast('⚡ Offline Driving Active: All 36 cameras running from local cache.');
});
// Report Camera FAB // Report Camera FAB
if (el.btnReportCameraFab) el.btnReportCameraFab.addEventListener('click', openReportModal); if (el.btnReportCameraFab) el.btnReportCameraFab.addEventListener('click', openReportModal);
+13 -13
View File
@@ -1,20 +1,20 @@
// Service Worker for FlockRadar Offline Driving & Live Updates // Service Worker for FlockRadar Offline Driving & Live Updates
const CACHE_NAME = 'flockradar-v24'; const CACHE_NAME = 'flockradar-v25';
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
'./', './',
'./index.html', './index.html',
'./css/styles.css?v=24', './css/styles.css?v=25',
'./js/icons.js?v=24', './js/icons.js?v=25',
'./js/app.js?v=24', './js/app.js?v=25',
'./js/auth.js?v=24', './js/auth.js?v=25',
'./js/radar.js?v=24', './js/radar.js?v=25',
'./data/portals.js?v=24', './data/portals.js?v=25',
'./favicon.svg?v=24', './favicon.svg?v=25',
'./favicon.ico?v=24', './favicon.ico?v=25',
'./apple-touch-icon.png?v=24', './apple-touch-icon.png?v=25',
'./icon-192.png?v=24', './icon-192.png?v=25',
'./icon-512.png?v=24', './icon-512.png?v=25',
'./og-image.png?v=24', './og-image.png?v=25',
'./assets/img/logo.svg', './assets/img/logo.svg',
'./assets/img/flock_pole_cam_palmdale.jpg', './assets/img/flock_pole_cam_palmdale.jpg',
'./manifest.json' './manifest.json'