| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>SkyWatch - Real-time Plane Tracker</title> |
| <meta name="description" content="Track real planes flying above your location"> |
| <meta name="theme-color" content="#1e40af"> |
| <link rel="manifest" href="/manifest.json"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| } |
| .animate-pulse { |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; |
| } |
| .notification-badge { |
| position: absolute; |
| top: -5px; |
| right: -5px; |
| background-color: #ef4444; |
| color: white; |
| border-radius: 9999px; |
| width: 20px; |
| height: 20px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 12px; |
| font-weight: bold; |
| } |
| .plane-icon { |
| transform: rotate(var(--rotation)); |
| } |
| .map-container { |
| height: 300px; |
| background-color: #e5e7eb; |
| position: relative; |
| overflow: hidden; |
| } |
| .map-plane { |
| position: absolute; |
| transition: all 1s ease-out; |
| } |
| .radar-sweep { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| width: 100%; |
| height: 100%; |
| border-radius: 50%; |
| transform: translate(-50%, -50%); |
| background: conic-gradient(from 0deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.3) 50%, transparent 50%); |
| animation: radar-rotate 4s linear infinite; |
| pointer-events: none; |
| } |
| @keyframes radar-rotate { |
| from { transform: translate(-50%, -50%) rotate(0deg); } |
| to { transform: translate(-50%, -50%) rotate(360deg); } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen"> |
| <div class="max-w-md mx-auto bg-white shadow-lg rounded-b-xl overflow-hidden"> |
| |
| <header class="bg-blue-800 text-white p-4 relative"> |
| <div class="flex justify-between items-center"> |
| <div> |
| <h1 class="text-xl font-bold">SkyWatch</h1> |
| <p class="text-sm opacity-80" id="location-status">Detecting your location...</p> |
| </div> |
| <div class="flex space-x-3"> |
| <button id="notification-btn" class="relative p-2 rounded-full hover:bg-blue-700 transition"> |
| <i class="fas fa-bell"></i> |
| <span id="notification-count" class="notification-badge hidden">0</span> |
| </button> |
| <button id="refresh-btn" class="p-2 rounded-full hover:bg-blue-700 transition"> |
| <i class="fas fa-sync-alt"></i> |
| </button> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <main class="p-4"> |
| |
| <div class="map-container rounded-lg mb-4 relative"> |
| <div id="radar-center" class="absolute top-1/2 left-1/2 w-3 h-3 bg-blue-600 rounded-full transform -translate-x-1/2 -translate-y-1/2 z-10"></div> |
| <div id="map-planes" class="relative z-0"></div> |
| <div class="radar-sweep"></div> |
| </div> |
|
|
| |
| <div id="current-plane" class="bg-blue-50 rounded-lg p-3 mb-4 hidden"> |
| <div class="flex justify-between items-center mb-2"> |
| <h3 class="font-bold text-blue-800">Plane Above You Now</h3> |
| <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">LIVE</span> |
| </div> |
| <div class="flex items-center space-x-3"> |
| <div class="bg-blue-100 p-2 rounded-full"> |
| <i class="fas fa-plane text-blue-600"></i> |
| </div> |
| <div class="flex-1"> |
| <div class="flex justify-between"> |
| <span class="font-medium" id="current-callsign">--</span> |
| <span class="text-sm" id="current-altitude">-- ft</span> |
| </div> |
| <div class="text-sm text-gray-600" id="current-airline">--</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="mb-2 flex justify-between items-center"> |
| <h2 class="font-bold text-gray-800">Upcoming Planes</h2> |
| <span class="text-xs bg-gray-200 text-gray-800 px-2 py-1 rounded-full" id="planes-count">0 planes</span> |
| </div> |
| <div id="upcoming-planes" class="space-y-3"> |
| <div class="text-center py-8 text-gray-500" id="loading-planes"> |
| <i class="fas fa-plane animate-pulse text-2xl mb-2"></i> |
| <p>Scanning for planes...</p> |
| </div> |
| </div> |
| </main> |
|
|
| |
| <footer class="bg-gray-50 p-3 border-t flex justify-between text-xs text-gray-500"> |
| <div> |
| <span id="last-updated">Last updated: --</span> |
| </div> |
| <div> |
| <span id="api-status">API: Offline</span> |
| </div> |
| </footer> |
| </div> |
|
|
| |
| <div id="notification-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg w-full max-w-sm mx-4 max-h-[80vh] overflow-y-auto"> |
| <div class="p-4 border-b flex justify-between items-center"> |
| <h3 class="font-bold">Plane Alerts</h3> |
| <button id="close-notifications" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div id="notification-list" class="divide-y"> |
| <div class="p-4 text-center text-gray-500"> |
| No alerts yet |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="install-prompt" class="fixed bottom-4 left-0 right-0 flex justify-center hidden"> |
| <div class="bg-blue-600 text-white rounded-lg shadow-lg p-4 mx-4 max-w-md flex items-center justify-between"> |
| <div class="flex items-center"> |
| <i class="fas fa-plane-departure text-xl mr-3"></i> |
| <div> |
| <p class="font-medium">Install SkyWatch</p> |
| <p class="text-sm opacity-90">Get real-time plane alerts</p> |
| </div> |
| </div> |
| <button id="install-btn" class="bg-white text-blue-600 px-3 py-1 rounded-full text-sm font-medium">Install</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| if ('serviceWorker' in navigator) { |
| window.addEventListener('load', () => { |
| navigator.serviceWorker.register('/sw.js').then(registration => { |
| console.log('ServiceWorker registration successful'); |
| }).catch(err => { |
| console.log('ServiceWorker registration failed: ', err); |
| }); |
| }); |
| } |
| |
| |
| const state = { |
| location: null, |
| planes: [], |
| notifications: [], |
| lastUpdated: null, |
| currentPlane: null, |
| deferredPrompt: null, |
| apiKeys: { |
| opensky: null, |
| adsbexchange: null |
| } |
| }; |
| |
| |
| const elements = { |
| locationStatus: document.getElementById('location-status'), |
| currentPlaneCard: document.getElementById('current-plane'), |
| currentCallsign: document.getElementById('current-callsign'), |
| currentAltitude: document.getElementById('current-altitude'), |
| currentAirline: document.getElementById('current-airline'), |
| upcomingPlanes: document.getElementById('upcoming-planes'), |
| loadingPlanes: document.getElementById('loading-planes'), |
| planesCount: document.getElementById('planes-count'), |
| lastUpdated: document.getElementById('last-updated'), |
| apiStatus: document.getElementById('api-status'), |
| notificationBtn: document.getElementById('notification-btn'), |
| notificationCount: document.getElementById('notification-count'), |
| notificationModal: document.getElementById('notification-modal'), |
| notificationList: document.getElementById('notification-list'), |
| closeNotifications: document.getElementById('close-notifications'), |
| refreshBtn: document.getElementById('refresh-btn'), |
| installPrompt: document.getElementById('install-prompt'), |
| installBtn: document.getElementById('install-btn'), |
| mapPlanes: document.getElementById('map-planes'), |
| radarCenter: document.getElementById('radar-center') |
| }; |
| |
| |
| elements.notificationBtn.addEventListener('click', showNotifications); |
| elements.closeNotifications.addEventListener('click', hideNotifications); |
| elements.refreshBtn.addEventListener('click', refreshData); |
| elements.installBtn.addEventListener('click', installApp); |
| |
| |
| window.addEventListener('beforeinstallprompt', (e) => { |
| e.preventDefault(); |
| state.deferredPrompt = e; |
| elements.installPrompt.classList.remove('hidden'); |
| }); |
| |
| function installApp() { |
| if (state.deferredPrompt) { |
| state.deferredPrompt.prompt(); |
| state.deferredPrompt.userChoice.then((choiceResult) => { |
| if (choiceResult.outcome === 'accepted') { |
| elements.installPrompt.classList.add('hidden'); |
| } |
| state.deferredPrompt = null; |
| }); |
| } |
| } |
| |
| |
| function getLocation() { |
| elements.locationStatus.textContent = 'Detecting your location...'; |
| |
| if (navigator.geolocation) { |
| navigator.geolocation.getCurrentPosition( |
| position => { |
| state.location = { |
| lat: position.coords.latitude, |
| lon: position.coords.longitude, |
| accuracy: position.coords.accuracy |
| }; |
| elements.locationStatus.textContent = `📍 ${state.location.lat.toFixed(4)}, ${state.location.lon.toFixed(4)}`; |
| getPlaneData(); |
| }, |
| error => { |
| console.error('Geolocation error:', error); |
| elements.locationStatus.textContent = 'Location access denied. Using default location.'; |
| |
| state.location = { lat: 40.7128, lon: -74.0060 }; |
| getPlaneData(); |
| }, |
| { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } |
| ); |
| } else { |
| elements.locationStatus.textContent = 'Geolocation not supported. Using default location.'; |
| state.location = { lat: 40.7128, lon: -74.0060 }; |
| getPlaneData(); |
| } |
| } |
| |
| |
| async function getPlaneData() { |
| elements.loadingPlanes.classList.remove('hidden'); |
| elements.upcomingPlanes.innerHTML = ''; |
| elements.upcomingPlanes.appendChild(elements.loadingPlanes); |
| elements.apiStatus.textContent = 'API: Loading...'; |
| |
| if(!state.location) { |
| return; |
| } |
| |
| try { |
| |
| const range = 1; |
| const bbox = [ |
| state.location.lat - range, |
| state.location.lon - range, |
| state.location.lat + range, |
| state.location.lon + range |
| ]; |
| |
| console.log('---state', state); |
| console.log('---bbox', bbox); |
| |
| |
| |
| let response = await fetch(`https://opensky-network.org/api/states/all?lamin=${bbox[0]}&lomin=${bbox[1]}&lamax=${bbox[2]}&lomax=${bbox[3]}`); |
| |
| if (!response.ok) { |
| |
| throw new Error('OpenSky API failed, trying ADSBExchange'); |
| } |
| |
| const data = await response.json(); |
| state.planes = data.states || []; |
| |
| if (state.planes.length === 0) { |
| |
| await getADSBExchangeData(); |
| } else { |
| processPlaneData(); |
| elements.apiStatus.textContent = 'API: OpenSky'; |
| elements.apiStatus.className = elements.apiStatus.className.replace('text-red-500', 'text-green-500'); |
| } |
| } catch (error) { |
| console.error('Error fetching plane data:', error); |
| |
| await getADSBExchangeData(); |
| } |
| } |
| |
| |
| async function getADSBExchangeData() { |
| try { |
| |
| const response = await fetch(`https://adsbexchange-com1.p.rapidapi.com/v2/lat/${state.location.lat}/lon/${state.location.lon}/dist/50/`, { |
| headers: { |
| 'X-RapidAPI-Key': 'your-rapidapi-key-here', |
| 'X-RapidAPI-Host': 'adsbexchange-com1.p.rapidapi.com' |
| } |
| }); |
| |
| if (!response.ok) throw new Error('ADSBExchange API failed'); |
| |
| const data = await response.json(); |
| |
| state.planes = data.ac.map(plane => [ |
| plane.hex, |
| plane.flight, |
| null, |
| null, |
| null, |
| plane.lon, |
| plane.lat, |
| plane.altitude, |
| false, |
| plane.speed, |
| plane.track, |
| plane.vrate, |
| null, null, null, null, null, null |
| ]); |
| |
| processPlaneData(); |
| elements.apiStatus.textContent = 'API: ADSBExchange'; |
| elements.apiStatus.className = elements.apiStatus.className.replace('text-red-500', 'text-green-500'); |
| } catch (error) { |
| console.error('Error fetching ADSBExchange data:', error); |
| elements.apiStatus.textContent = 'API: Offline'; |
| elements.apiStatus.className += ' text-red-500'; |
| |
| elements.loadingPlanes.innerHTML = ` |
| <div class="text-center py-8 text-red-500"> |
| <i class="fas fa-exclamation-triangle text-2xl mb-2"></i> |
| <p>Failed to load plane data</p> |
| <button class="mt-2 text-blue-600 text-sm font-medium">Retry</button> |
| </div> |
| `; |
| elements.loadingPlanes.querySelector('button').addEventListener('click', getPlaneData); |
| } |
| } |
| |
| |
| function processPlaneData() { |
| const now = new Date(); |
| state.lastUpdated = now; |
| elements.lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`; |
| |
| |
| const nearbyPlanes = state.planes |
| .filter(plane => plane[5] && plane[6]) |
| .map(plane => ({ |
| icao24: plane[0], |
| callsign: plane[1]?.trim() || 'N/A', |
| origin: plane[2] || 'N/A', |
| time: plane[3] || Date.now() / 1000, |
| lastContact: plane[4] || Date.now() / 1000, |
| longitude: plane[5], |
| latitude: plane[6], |
| altitude: plane[7] || 0, |
| onGround: plane[8], |
| velocity: plane[9] || 0, |
| heading: plane[10] || 0, |
| verticalRate: plane[11] || 0, |
| airline: getAirlineFromCallsign(plane[1]?.trim()) |
| })) |
| .filter(plane => !plane.onGround && plane.altitude > 1000) |
| .sort((a, b) => { |
| |
| const distA = getDistance(state.location.lat, state.location.lon, a.latitude, a.longitude); |
| const distB = getDistance(state.location.lat, state.location.lon, b.latitude, b.longitude); |
| return distA - distB; |
| }); |
| |
| |
| const overheadPlane = nearbyPlanes.find(plane => { |
| const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
| return distance < 0.5; |
| }); |
| |
| if (overheadPlane) { |
| showCurrentPlane(overheadPlane); |
| |
| |
| if (!state.notifications.some(n => n.icao24 === overheadPlane.icao24)) { |
| addNotification(overheadPlane); |
| showAlert(overheadPlane); |
| } |
| } else { |
| hideCurrentPlane(); |
| } |
| |
| |
| const upcomingPlanes = nearbyPlanes.slice(0, 5); |
| displayUpcomingPlanes(upcomingPlanes); |
| |
| |
| updateRadarMap(nearbyPlanes.slice(0, 10)); |
| } |
| |
| |
| function showCurrentPlane(plane) { |
| state.currentPlane = plane; |
| elements.currentPlaneCard.classList.remove('hidden'); |
| elements.currentCallsign.textContent = plane.callsign; |
| elements.currentAltitude.textContent = `${Math.round(plane.altitude * 0.3048)} m (${Math.round(plane.altitude)} ft)`; |
| elements.currentAirline.textContent = plane.airline || 'Unknown airline'; |
| } |
| |
| function hideCurrentPlane() { |
| state.currentPlane = null; |
| elements.currentPlaneCard.classList.add('hidden'); |
| } |
| |
| |
| function displayUpcomingPlanes(planes) { |
| elements.loadingPlanes.classList.add('hidden'); |
| elements.planesCount.textContent = `${planes.length} ${planes.length === 1 ? 'plane' : 'planes'}`; |
| |
| if (planes.length === 0) { |
| elements.upcomingPlanes.innerHTML = ` |
| <div class="text-center py-8 text-gray-500"> |
| <i class="fas fa-plane-slash text-2xl mb-2"></i> |
| <p>No planes detected nearby</p> |
| </div> |
| `; |
| return; |
| } |
| |
| elements.upcomingPlanes.innerHTML = ''; |
| |
| planes.forEach(plane => { |
| const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
| const timeToOverhead = distance / (plane.velocity * 0.514444); |
| |
| const planeEl = document.createElement('div'); |
| planeEl.className = 'bg-white rounded-lg p-3 shadow-sm border'; |
| planeEl.innerHTML = ` |
| <div class="flex items-center space-x-3"> |
| <div class="bg-blue-100 p-2 rounded-full"> |
| <i class="fas fa-plane text-blue-600 plane-icon" style="--rotation: ${plane.heading}deg"></i> |
| </div> |
| <div class="flex-1"> |
| <div class="flex justify-between"> |
| <span class="font-medium">${plane.callsign}</span> |
| <span class="text-sm">${Math.round(plane.altitude * 0.3048)} m</span> |
| </div> |
| <div class="text-sm text-gray-600">${plane.airline || 'Unknown airline'}</div> |
| <div class="flex justify-between mt-1 text-xs"> |
| <span>${distance < 1 ? `${Math.round(distance * 1000)} m away` : `${distance.toFixed(1)} km away`}</span> |
| <span>ETA: ${timeToOverhead > 0 ? `${Math.round(timeToOverhead / 60)} min` : 'now'}</span> |
| </div> |
| </div> |
| </div> |
| `; |
| elements.upcomingPlanes.appendChild(planeEl); |
| }); |
| } |
| |
| |
| function updateRadarMap(planes) { |
| elements.mapPlanes.innerHTML = ''; |
| |
| |
| const radarRange = 10; |
| |
| planes.forEach(plane => { |
| const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
| if (distance > radarRange) return; |
| |
| |
| const bearing = getBearing(state.location.lat, state.location.lon, plane.latitude, plane.longitude); |
| const scale = distance / radarRange; |
| |
| |
| const x = Math.cos(bearing * Math.PI / 180) * scale * 150; |
| const y = Math.sin(bearing * Math.PI / 180) * scale * 150; |
| |
| const planeEl = document.createElement('div'); |
| planeEl.className = 'map-plane'; |
| planeEl.style.left = `calc(50% + ${x}px)`; |
| planeEl.style.top = `calc(50% + ${y}px)`; |
| planeEl.innerHTML = ` |
| <div class="w-6 h-6 flex items-center justify-center"> |
| <i class="fas fa-plane text-blue-600 text-xs plane-icon" style="--rotation: ${plane.heading}deg"></i> |
| </div> |
| `; |
| elements.mapPlanes.appendChild(planeEl); |
| }); |
| } |
| |
| |
| function addNotification(plane) { |
| const notification = { |
| id: Date.now(), |
| icao24: plane.icao24, |
| callsign: plane.callsign, |
| altitude: plane.altitude, |
| time: new Date(), |
| read: false |
| }; |
| |
| state.notifications.unshift(notification); |
| updateNotificationBadge(); |
| |
| |
| localStorage.setItem('skywatch-notifications', JSON.stringify(state.notifications)); |
| } |
| |
| |
| function showAlert(plane) { |
| if (Notification.permission === 'granted') { |
| new Notification(`✈️ Plane overhead: ${plane.callsign}`, { |
| body: `Altitude: ${Math.round(plane.altitude)} ft\nAirline: ${plane.airline || 'Unknown'}`, |
| icon: '/icon-192x192.png' |
| }); |
| } else if (Notification.permission !== 'denied') { |
| Notification.requestPermission().then(permission => { |
| if (permission === 'granted') { |
| showAlert(plane); |
| } |
| }); |
| } |
| } |
| |
| |
| function updateNotificationBadge() { |
| const unreadCount = state.notifications.filter(n => !n.read).length; |
| if (unreadCount > 0) { |
| elements.notificationCount.textContent = unreadCount; |
| elements.notificationCount.classList.remove('hidden'); |
| } else { |
| elements.notificationCount.classList.add('hidden'); |
| } |
| } |
| |
| |
| function showNotifications() { |
| |
| state.notifications.forEach(n => n.read = true); |
| updateNotificationBadge(); |
| |
| |
| elements.notificationList.innerHTML = ''; |
| |
| if (state.notifications.length === 0) { |
| elements.notificationList.innerHTML = '<div class="p-4 text-center text-gray-500">No alerts yet</div>'; |
| } else { |
| state.notifications.forEach(notification => { |
| const notificationEl = document.createElement('div'); |
| notificationEl.className = 'p-4'; |
| notificationEl.innerHTML = ` |
| <div class="flex items-start space-x-3"> |
| <div class="bg-blue-100 p-2 rounded-full mt-1"> |
| <i class="fas fa-plane text-blue-600"></i> |
| </div> |
| <div class="flex-1"> |
| <div class="font-medium">${notification.callsign}</div> |
| <div class="text-sm text-gray-600">Altitude: ${Math.round(notification.altitude)} ft</div> |
| <div class="text-xs text-gray-500 mt-1">${new Date(notification.time).toLocaleString()}</div> |
| </div> |
| </div> |
| `; |
| elements.notificationList.appendChild(notificationEl); |
| }); |
| } |
| |
| elements.notificationModal.classList.remove('hidden'); |
| } |
| |
| function hideNotifications() { |
| elements.notificationModal.classList.add('hidden'); |
| } |
| |
| |
| function refreshData() { |
| elements.refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; |
| getPlaneData(); |
| setTimeout(() => { |
| elements.refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i>'; |
| }, 1000); |
| } |
| |
| |
| function getDistance(lat1, lon1, lat2, lon2) { |
| |
| const R = 6371; |
| const dLat = (lat2 - lat1) * Math.PI / 180; |
| const dLon = (lon2 - lon1) * Math.PI / 180; |
| const a = |
| Math.sin(dLat/2) * Math.sin(dLat/2) + |
| Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * |
| Math.sin(dLon/2) * Math.sin(dLon/2); |
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); |
| return R * c; |
| } |
| |
| function getBearing(lat1, lon1, lat2, lon2) { |
| |
| const y = Math.sin(lon2 - lon1) * Math.cos(lat2); |
| const x = Math.cos(lat1) * Math.sin(lat2) - |
| Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); |
| return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; |
| } |
| |
| function getAirlineFromCallsign(callsign) { |
| if (!callsign) return null; |
| |
| |
| const airlineCode = callsign.substring(0, 3).toUpperCase(); |
| |
| |
| const airlines = { |
| 'UAL': 'United Airlines', |
| 'AAL': 'American Airlines', |
| 'DAL': 'Delta Air Lines', |
| 'SWA': 'Southwest Airlines', |
| 'JBU': 'JetBlue', |
| 'FDX': 'FedEx', |
| 'UPS': 'UPS Airlines', |
| 'AFR': 'Air France', |
| 'BAW': 'British Airways', |
| 'DLH': 'Lufthansa', |
| 'KLM': 'KLM Royal Dutch Airlines', |
| 'QFA': 'Qantas', |
| 'SIA': 'Singapore Airlines', |
| 'THY': 'Turkish Airlines', |
| 'UAE': 'Emirates', |
| 'VIR': 'Virgin Atlantic', |
| 'RYR': 'Ryanair', |
| 'EZY': 'EasyJet', |
| 'WZZ': 'Wizz Air', |
| 'AFL': 'Aeroflot', |
| 'ANA': 'All Nippon Airways', |
| 'JAL': 'Japan Airlines', |
| 'CAL': 'China Airlines', |
| 'CPA': 'Cathay Pacific', |
| 'CES': 'China Eastern', |
| 'CSN': 'China Southern', |
| 'KAL': 'Korean Air', |
| 'MAS': 'Malaysia Airlines', |
| 'QTR': 'Qatar Airways', |
| 'SVA': 'Saudia', |
| 'THA': 'Thai Airways' |
| }; |
| |
| return airlines[airlineCode] || null; |
| } |
| |
| |
| function loadNotifications() { |
| const savedNotifications = localStorage.getItem('skywatch-notifications'); |
| if (savedNotifications) { |
| state.notifications = JSON.parse(savedNotifications); |
| updateNotificationBadge(); |
| } |
| } |
| |
| |
| function init() { |
| loadNotifications(); |
| getLocation(); |
| |
| |
| if (window.matchMedia('(display-mode: standalone)').matches) { |
| elements.installPrompt.classList.add('hidden'); |
| } |
| |
| |
| setInterval(refreshData, 30000); |
| } |
| |
| |
| init(); |
| </script> |
|
|
| |
| <script> |
| |
| const CACHE_NAME = 'skywatch-v1'; |
| const ASSETS = [ |
| '/', |
| '/index.html', |
| '/icon-192x192.png', |
| '/icon-512x512.png', |
| '/manifest.json' |
| ]; |
| |
| self.addEventListener('install', event => { |
| event.waitUntil( |
| caches.open(CACHE_NAME) |
| .then(cache => cache.addAll(ASSETS)) |
| .then(() => self.skipWaiting()) |
| ); |
| }); |
| |
| self.addEventListener('activate', event => { |
| event.waitUntil( |
| caches.keys().then(keys => |
| Promise.all( |
| keys.filter(key => key !== CACHE_NAME) |
| .map(key => caches.delete(key)) |
| ) |
| ).then(() => self.clients.claim()) |
| ); |
| }); |
| |
| self.addEventListener('fetch', event => { |
| event.respondWith( |
| caches.match(event.request) |
| .then(response => response || fetch(event.request)) |
| ); |
| }); |
| </script> |
|
|
| |
| <script> |
| |
| const manifest = { |
| "name": "SkyWatch", |
| "short_name": "SkyWatch", |
| "description": "Track planes flying above your location in real-time", |
| "start_url": "/", |
| "display": "standalone", |
| "background_color": "#1e40af", |
| "theme_color": "#1e40af", |
| "icons": [ |
| { |
| "src": "/icon-192x192.png", |
| "sizes": "192x192", |
| "type": "image/png" |
| }, |
| { |
| "src": "/icon-512x512.png", |
| "sizes": "512x512", |
| "type": "image/png" |
| } |
| ] |
| }; |
| |
| const blob = new Blob([JSON.stringify(manifest)], { type: 'application/json' }); |
| const manifestUrl = URL.createObjectURL(blob); |
| const link = document.createElement('link'); |
| link.rel = 'manifest'; |
| link.href = manifestUrl; |
| document.head.appendChild(link); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=vs4vijay/skywatch" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |