skywatch / index.html
vs4vijay's picture
Add 1 files
d329ea1 verified
<!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 -->
<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 Content -->
<main class="p-4">
<!-- Radar Map -->
<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>
<!-- Current Plane Info -->
<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>
<!-- Upcoming Planes -->
<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 -->
<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>
<!-- Notification Modal -->
<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>
<!-- Install Prompt -->
<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>
// Service Worker Registration
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);
});
});
}
// App state
const state = {
location: null,
planes: [],
notifications: [],
lastUpdated: null,
currentPlane: null,
deferredPrompt: null,
apiKeys: {
opensky: null, // OpenSky is free but rate limited
adsbexchange: null // ADSBExchange requires API key
}
};
// DOM Elements
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')
};
// Event Listeners
elements.notificationBtn.addEventListener('click', showNotifications);
elements.closeNotifications.addEventListener('click', hideNotifications);
elements.refreshBtn.addEventListener('click', refreshData);
elements.installBtn.addEventListener('click', installApp);
// Install PWA prompt
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;
});
}
}
// Get user location
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.';
// Default to New York if location access is denied
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();
}
}
// Fetch plane data from OpenSky Network API
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 {
// Calculate bounding box (1 degree = ~111km)
const range = 1; // ~111km radius
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);
// Try OpenSky Network API first (free but rate limited)
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) {
// If OpenSky fails, try ADSBExchange (requires API key)
throw new Error('OpenSky API failed, trying ADSBExchange');
}
const data = await response.json();
state.planes = data.states || [];
if (state.planes.length === 0) {
// If no planes from OpenSky, try ADSBExchange
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);
// Fallback to ADSBExchange if OpenSky fails
await getADSBExchangeData();
}
}
// Fetch plane data from ADSBExchange API
async function getADSBExchangeData() {
try {
// ADSBExchange API (requires API key - this is a public demo key that may be rate limited)
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', // Replace with your RapidAPI key
'X-RapidAPI-Host': 'adsbexchange-com1.p.rapidapi.com'
}
});
if (!response.ok) throw new Error('ADSBExchange API failed');
const data = await response.json();
// Convert ADSBExchange format to OpenSky-like format for consistency
state.planes = data.ac.map(plane => [
plane.hex, // icao24
plane.flight, // callsign
null, // origin country
null, // time position
null, // last contact
plane.lon, // longitude
plane.lat, // latitude
plane.altitude, // altitude (ft)
false, // on ground
plane.speed, // velocity (knots)
plane.track, // heading
plane.vrate, // vertical rate
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);
}
}
// Process plane data and update UI
function processPlaneData() {
const now = new Date();
state.lastUpdated = now;
elements.lastUpdated.textContent = `Last updated: ${now.toLocaleTimeString()}`;
// Filter and sort planes by distance and time
const nearbyPlanes = state.planes
.filter(plane => plane[5] && plane[6]) // has position
.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) // only airborne planes
.sort((a, b) => {
// Sort by distance to user
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;
});
// Check for planes directly overhead
const overheadPlane = nearbyPlanes.find(plane => {
const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
return distance < 0.5; // within 0.5 km
});
if (overheadPlane) {
showCurrentPlane(overheadPlane);
// Check if we've already notified about this plane
if (!state.notifications.some(n => n.icao24 === overheadPlane.icao24)) {
addNotification(overheadPlane);
showAlert(overheadPlane);
}
} else {
hideCurrentPlane();
}
// Display upcoming planes (next 5 closest)
const upcomingPlanes = nearbyPlanes.slice(0, 5);
displayUpcomingPlanes(upcomingPlanes);
// Update radar map
updateRadarMap(nearbyPlanes.slice(0, 10));
}
// Display current plane above user
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');
}
// Display upcoming planes list
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); // knots to m/s
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);
});
}
// Update radar map with plane positions
function updateRadarMap(planes) {
elements.mapPlanes.innerHTML = '';
// Radar range in km
const radarRange = 10;
planes.forEach(plane => {
const distance = getDistance(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
if (distance > radarRange) return;
// Calculate position on radar (relative to center)
const bearing = getBearing(state.location.lat, state.location.lon, plane.latitude, plane.longitude);
const scale = distance / radarRange;
// Position plane on radar (max 150px from center)
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);
});
}
// Add notification for a plane
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();
// Store notifications in localStorage
localStorage.setItem('skywatch-notifications', JSON.stringify(state.notifications));
}
// Show alert for a plane
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);
}
});
}
}
// Update notification badge
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');
}
}
// Show notifications modal
function showNotifications() {
// Mark all notifications as read
state.notifications.forEach(n => n.read = true);
updateNotificationBadge();
// Update notification list
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');
}
// Refresh data
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);
}
// Helper functions
function getDistance(lat1, lon1, lat2, lon2) {
// Haversine formula to calculate distance in km
const R = 6371; // Earth radius in km
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) {
// Calculate bearing (direction) between two points
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;
// Extract airline code from callsign (first 3 letters usually)
const airlineCode = callsign.substring(0, 3).toUpperCase();
// Airline database
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;
}
// Load notifications from localStorage
function loadNotifications() {
const savedNotifications = localStorage.getItem('skywatch-notifications');
if (savedNotifications) {
state.notifications = JSON.parse(savedNotifications);
updateNotificationBadge();
}
}
// Initialize app
function init() {
loadNotifications();
getLocation();
// Check for PWA installation
if (window.matchMedia('(display-mode: standalone)').matches) {
elements.installPrompt.classList.add('hidden');
}
// Auto-refresh every 30 seconds
setInterval(refreshData, 30000);
}
// Start the app
init();
</script>
<!-- Service Worker (would be in a separate file in production) -->
<script>
// This would normally be in a separate sw.js file
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>
<!-- Manifest (would be in a separate file in production) -->
<script>
// This would normally be in a separate manifest.json file
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>