diff --git a/PRIVACY.md b/PRIVACY.md
index 0bda008..087dfc2 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -1,6 +1,6 @@
# Privacy Policy
-**Last updated: March 18, 2026**
+**Last updated: June 4, 2026**
## Overview
@@ -14,7 +14,8 @@ Plane Alert collects and stores the following data locally on your device:
- **Alert settings** — the alerts you configure (e.g. registrations, flight numbers, aircraft types).
- **Notification preferences** — your notification and sound settings.
- **Caught aircraft** — ICAO hex addresses of aircraft you have marked as caught.
-- **Notification history** — a local log of triggered alerts (callsign, timestamp, flight details).
+- **Notification history** — a local log of triggered alerts (callsign, timestamp, flight details). This log is stored only on your current device and is intentionally excluded from backup exports, as it reflects activity specific to your device and is not meaningful to restore.
+- **Statistics** — all-time counters derived from triggered notifications: total notification count, date of first detection, aircraft type counts, and airline prefix counts. These are stored locally and can be reset at any time via the History tab.
## How Your Data Is Used
@@ -24,6 +25,12 @@ All data is used solely to provide the core functionality of the extension: poll
All data is stored **locally on your device**, so nothing is shared with the developer. With one exception: your approximate location and radius are sent to the airplanes.live API with each poll to retrieve nearby aircraft.
+## Backup & Export
+
+The backup export (Settings → Backup) includes: alerts, location, radius, units, notification preferences, sound settings, startup tab, caught aircraft, and statistics.
+
+**Notification history is excluded from backups by design.** It is an ephemeral device-local log and carries no value when restored to a different device or after a reinstall.
+
## Third-Party Services
Plane Alert uses the [airplanes.live](https://airplanes.live) API to retrieve real-time aircraft data. Your approximate location (latitude, longitude, radius) is sent to this API with each poll. Please refer to the [airplanes.live](https://airplanes.live) website for their own privacy practices.
@@ -34,7 +41,7 @@ We do not sell, trade, or share any of your data with third parties.
## Data Deletion
-You can delete all locally stored data at any time by removing the extension from Chrome, or by using the "Export backup" feature and clearing your settings manually.
+You can delete all locally stored data at any time by removing the extension from Chrome, or by using the "Export backup" feature and clearing your settings manually. Notification history and statistics can be cleared independently via the History tab.
## Changes to This Policy
diff --git a/README.md b/README.md
index f95b183..4b3bf1b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
A Chrome extension that notifies you when a tracked aircraft enters your radius — powered by the [airplanes.live](https://airplanes.live) API.
-
+


@@ -15,7 +15,7 @@ A Chrome extension that notifies you when a tracked aircraft enters your radius
- **Catch aircraft** — mark an aircraft as caught directly from the notification or the Live tab detail panel. Caught aircraft won't trigger future notifications. Manage your caught list in the History tab.
- **Alert sound** — choose from Ping, Radar, Alert or Chime with adjustable volume (soft, medium, loud)
- **Live tab** — see all aircraft in your radius in real time, with sorting and filtering options
-- **Detail dropdown** — click any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading
+- **Detail dropdown** — click any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading. Use the 🔔/🔕 bell buttons to toggle alerts for that registration or type directly from the dropdown.
- **Notification history** — a log of every triggered alert with callsign, time and flight details
- **Settings** — organised into collapsible cards: Location, Filters, Units, Notifications, Startup tab, and Backup & Test
- **Startup tab** — choose which tab opens on launch, or always return to the last tab you were on
@@ -60,18 +60,32 @@ Plane Alert polls the airplanes.live API in the background every minute. When an
---
+## Data & privacy
+
+All data is stored locally on your device. The only external call is to the airplanes.live API, which receives your approximate location and radius with each poll.
+
+**What is included in a backup export:** alerts, location, radius, units, notification preferences, sound settings, startup tab, caught aircraft, and statistics.
+
+**What is intentionally excluded from backups:** notification history (`notifHistory`). This log is ephemeral by design — it reflects what happened on your device and is not meaningful to restore on another machine or after a reinstall.
+
+See [PRIVACY.md](PRIVACY.md) for the full privacy policy.
+
+---
+
## File structure
```
plane-alert/
├── manifest.json
├── background.js — background service worker, polling & notifications
+├── shared.js — shared match logic (used by background & popup)
├── offscreen.html — offscreen document for audio playback
├── offscreen.js — Web Audio API sound engine
├── icons/
│ ├── icon16.png
│ ├── icon48.png
-│ └── icon128.png
+│ ├── icon128.png
+│ └── airplanes-live-logo.png
└── popup/
├── popup.html — extension UI skeleton
├── popup.css — all styles
@@ -104,4 +118,4 @@ This project is licensed under the **GNU General Public License v3.0**.
You are free to view, modify and redistribute the source code, but any derivative work must also be open source under the same license.
-**Publishing this extension or any derivative to a browser extension store (Chrome, Firefox, Edge, etc.) requires explicit written permission from the author.** See the [LICENSE](LICENSE) file for full details.
+**Publishing this extension or any derivative to a browser extension store (Chrome, Firefox, Edge, etc.) requires explicit written permission from the author.** See the [LICENSE](LICENSE) file for full details.
\ No newline at end of file
diff --git a/background.js b/background.js
index bb73f21..c40c054 100644
--- a/background.js
+++ b/background.js
@@ -1,4 +1,4 @@
-// background.js — runs in the background and polls the API
+// background.js v1.1.0 — runs in the background and polls the API
// Depends on shared.js (loaded via manifest.json background.scripts)
// ─── OFFSCREEN DOCUMENT ────────────────────────────────────────────────────
@@ -72,6 +72,40 @@ chrome.runtime.onMessage.addListener((msg) => {
}
});
+// ─── STATISTIEKEN BIJHOUDEN ────────────────────────────────────────────────
+
+async function recordStats(ac) {
+ const keys = ['statsTotalCount', 'statsFirstDetection', 'statsTypeCounts', 'statsAirlineCounts'];
+ const data = await chrome.storage.local.get(keys);
+
+ const totalCount = (data.statsTotalCount || 0) + 1;
+ const firstDetect = data.statsFirstDetection || new Date().toISOString();
+ const typeCounts = data.statsTypeCounts || {};
+ const airlineCounts = data.statsAirlineCounts || {};
+
+ // Type tellen (alleen als bekend)
+ if (ac.t) {
+ const t = ac.t.toUpperCase().trim();
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
+ }
+
+ // Airline tellen: eerste 3 letters van vluchtcode, alleen als vlucht minstens 4 tekens heeft
+ if (ac.flight && ac.flight.trim().length >= 4) {
+ const prefix = ac.flight.trim().toUpperCase().substring(0, 3);
+ // Alleen letters (geen cijfers-prefix zoals militairen of privé)
+ if (/^[A-Z]{3}$/.test(prefix)) {
+ airlineCounts[prefix] = (airlineCounts[prefix] || 0) + 1;
+ }
+ }
+
+ await chrome.storage.local.set({
+ statsTotalCount: totalCount,
+ statsFirstDetection: firstDetect,
+ statsTypeCounts: typeCounts,
+ statsAirlineCounts: airlineCounts
+ });
+}
+
async function pollAircraft() {
const { enabled = true } = await chrome.storage.local.get('enabled');
if (!enabled) return;
@@ -106,7 +140,6 @@ async function pollAircraft() {
for (const ac of aircraft) {
if (ac.hex && caughtAircraft.includes(ac.hex)) continue;
- // matchesAlert komt uit shared.js
const matchingAlert = config.alerts.find(alert => alert.active && matchesAlert(ac, alert));
if (!matchingAlert) continue;
@@ -119,6 +152,9 @@ async function pollAircraft() {
inRange[key] = true;
await chrome.storage.local.set({ inRange });
+ // Statistieken bijhouden voor elke nieuwe match
+ await recordStats(ac);
+
const { notificationsEnabled = true, notifShow = {} } =
await chrome.storage.local.get(['notificationsEnabled', 'notifShow']);
const show = Object.assign({ reg: false, type: true, alt: true, speed: true, route: true, dir: true }, notifShow);
@@ -175,7 +211,7 @@ chrome.notifications.onButtonClicked.addListener(async (notifId, btnIdx) => {
if (btnIdx === 0) {
const hexParts = notifId.split('_');
const hex = hexParts.length >= 3 ? hexParts[1] : '';
- chrome.tabs.create({ url: `https://globe.airplanes.live${hex ? `/?icao=${hex}` : ''}` });
+ chrome.tabs.create({ url: `https://globe.airplanes.live${hex ? `/?icao=${encodeURIComponent(hex)}` : ''}` });
}
if (btnIdx === 1) {
const parts = notifId.split('_');
diff --git a/fonts/outfit-latin-400-normal.woff2 b/fonts/outfit-latin-400-normal.woff2
new file mode 100644
index 0000000..2c401f5
Binary files /dev/null and b/fonts/outfit-latin-400-normal.woff2 differ
diff --git a/fonts/outfit-latin-600-normal.woff2 b/fonts/outfit-latin-600-normal.woff2
new file mode 100644
index 0000000..aa32da2
Binary files /dev/null and b/fonts/outfit-latin-600-normal.woff2 differ
diff --git a/fonts/outfit-latin-800-normal.woff2 b/fonts/outfit-latin-800-normal.woff2
new file mode 100644
index 0000000..fddb805
Binary files /dev/null and b/fonts/outfit-latin-800-normal.woff2 differ
diff --git a/fonts/space-mono-latin-400-normal.woff2 b/fonts/space-mono-latin-400-normal.woff2
new file mode 100644
index 0000000..2752f60
Binary files /dev/null and b/fonts/space-mono-latin-400-normal.woff2 differ
diff --git a/fonts/space-mono-latin-700-normal.woff2 b/fonts/space-mono-latin-700-normal.woff2
new file mode 100644
index 0000000..6563bad
Binary files /dev/null and b/fonts/space-mono-latin-700-normal.woff2 differ
diff --git a/manifest.json b/manifest.json
index fe30b31..b707e25 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Plane Alert",
"description": "Get notified when a tracked aircraft enters your area",
- "version": "1.5.0",
+ "version": "1.6.0",
"homepage_url": "https://github.com/PearMan95/plane-alert",
"permissions": [
"storage",
@@ -14,6 +14,9 @@
"host_permissions": [
"https://api.airplanes.live/*"
],
+ "content_security_policy": {
+ "extension_pages": "script-src 'self'; object-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'"
+ },
"background": {
"service_worker": "background.js"
},
@@ -29,16 +32,5 @@
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
- },
- "web_accessible_resources": [
- {
- "resources": [
- "offscreen.html",
- "offscreen.js"
- ],
- "matches": [
- "chrome-extension://*/*"
- ]
- }
- ]
+ }
}
\ No newline at end of file
diff --git a/popup/alerts.js b/popup/alerts.js
index 40413d0..6a882de 100644
--- a/popup/alerts.js
+++ b/popup/alerts.js
@@ -1,4 +1,4 @@
-// alerts.js — injecteert Alerts tab HTML en beheert alert logica
+// alerts.js v1.1.0 — injecteert Alerts tab HTML en beheert alert logica
// ─── HTML INJECTIE ──────────────────────────────────────────────────────────
@@ -62,6 +62,19 @@ const placeholders = {
// ─── EVENTS ────────────────────────────────────────────────────────────────
+function setNoteDisplay(el, noteText) {
+ el.innerHTML = '';
+ el.dataset.note = noteText || '';
+ if (noteText) {
+ el.textContent = noteText;
+ } else {
+ const emptySpan = document.createElement('span');
+ emptySpan.className = 'alert-note-empty';
+ emptySpan.textContent = '+ add note';
+ el.appendChild(emptySpan);
+ }
+}
+
function setupAlertsEvents() {
document.getElementById('alertType').addEventListener('change', (e) => {
@@ -108,6 +121,7 @@ function setupAlertsEvents() {
document.getElementById('alertValue').value = '';
document.getElementById('alertNote').value = '';
renderAlerts(alerts);
+ showSaved('Alert added');
});
}
@@ -129,15 +143,40 @@ async function renderAlerts(alerts) {
for (const alert of alerts) {
const item = document.createElement('div');
item.className = 'alert-item';
- item.innerHTML = `
-
-
-
${alert.type === 'dbflag' ? (flagLabels[alert.value] || alert.value) : alert.value}
-
${typeLabels[alert.type] || alert.type}
-
${alert.note || '+ add note '}
-
- X
- `;
+
+ const toggleBtn = document.createElement('button');
+ toggleBtn.className = `alert-toggle ${alert.active ? 'on' : ''}`;
+ toggleBtn.dataset.id = alert.id;
+
+ const info = document.createElement('div');
+ info.className = 'alert-info';
+
+ const valueDiv = document.createElement('div');
+ valueDiv.className = 'alert-value';
+ valueDiv.textContent = alert.type === 'dbflag' ? (flagLabels[alert.value] || alert.value) : alert.value;
+
+ const typeDiv = document.createElement('div');
+ typeDiv.className = 'alert-type-label';
+ typeDiv.textContent = typeLabels[alert.type] || alert.type;
+
+ const noteDiv = document.createElement('div');
+ noteDiv.className = 'alert-note';
+ noteDiv.dataset.id = alert.id;
+ noteDiv.title = 'Click to edit note';
+ setNoteDisplay(noteDiv, alert.note);
+
+ info.appendChild(valueDiv);
+ info.appendChild(typeDiv);
+ info.appendChild(noteDiv);
+
+ const removeBtn = document.createElement('button');
+ removeBtn.className = 'btn-remove';
+ removeBtn.dataset.id = alert.id;
+ removeBtn.textContent = 'X';
+
+ item.appendChild(toggleBtn);
+ item.appendChild(info);
+ item.appendChild(removeBtn);
list.appendChild(item);
}
@@ -145,10 +184,10 @@ async function renderAlerts(alerts) {
list.querySelectorAll('.alert-note').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
- if (el.querySelector('input')) return; // al in edit mode
+ if (el.querySelector('input')) return;
const id = el.dataset.id;
- const currentNote = el.textContent.trim() === '+ add note' ? '' : el.textContent.trim();
+ const currentNote = el.dataset.note || '';
const input = document.createElement('input');
input.type = 'text';
@@ -169,15 +208,13 @@ async function renderAlerts(alerts) {
alert.note = newNote;
await chrome.storage.local.set({ alerts });
}
- el.innerHTML = newNote
- ? newNote
- : '+ add note ';
+ setNoteDisplay(el, newNote);
}
input.addEventListener('blur', saveNote);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { input.blur(); }
- if (e.key === 'Escape') { el.innerHTML = currentNote || '+ add note '; }
+ if (e.key === 'Escape') { setNoteDisplay(el, currentNote); }
});
});
});
diff --git a/popup/history.js b/popup/history.js
index d3ae6ef..48352b6 100644
--- a/popup/history.js
+++ b/popup/history.js
@@ -1,9 +1,25 @@
-// history.js — injecteert History tab HTML en beheert notificatiegeschiedenis
+// history.js v1.2.1 — injecteert History tab HTML en beheert notificatiegeschiedenis
// ─── HTML INJECTIE ──────────────────────────────────────────────────────────
function initHistoryTab() {
document.getElementById('tab-history').innerHTML = `
+
+
+
+
+ 📊 Statistics
+ ▼
+
+
+
+
+
Notification history
Clear
@@ -44,9 +60,130 @@ function setupHistoryEvents() {
});
document.getElementById('btnClearCaught').addEventListener('click', async () => {
+ const btn = document.getElementById('btnClearCaught');
+ if (btn.dataset.confirm !== '1') {
+ btn.dataset.confirm = '1';
+ btn.textContent = 'Sure? Click again to confirm';
+ btn.style.color = '#ef4444';
+ btn.style.borderColor = '#ef4444';
+ setTimeout(() => {
+ if (btn.dataset.confirm === '1') {
+ btn.dataset.confirm = '';
+ btn.textContent = 'Release all';
+ btn.style.color = '';
+ btn.style.borderColor = '';
+ }
+ }, 2500);
+ return;
+ }
+ btn.dataset.confirm = '';
+ btn.textContent = 'Release all';
+ btn.style.color = '';
+ btn.style.borderColor = '';
await chrome.storage.local.set({ caughtAircraft: [], caughtAircraftLabels: {} });
renderCaughtList();
});
+
+ document.getElementById('btnToggleStats').addEventListener('click', () => {
+ const panel = document.getElementById('statsPanel');
+ const chevron = document.getElementById('statsChevron');
+ const isOpen = panel.style.display !== 'none';
+ panel.style.display = isOpen ? 'none' : 'block';
+ chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
+ if (!isOpen) renderStats();
+ });
+
+ document.getElementById('btnResetStats').addEventListener('click', async () => {
+ const btn = document.getElementById('btnResetStats');
+ if (btn.dataset.confirm !== '1') {
+ btn.dataset.confirm = '1';
+ btn.textContent = 'Sure? Click again to confirm';
+ btn.style.color = '#ef4444';
+ btn.style.borderColor = '#ef4444';
+ setTimeout(() => {
+ btn.dataset.confirm = '';
+ btn.textContent = 'Reset statistics';
+ btn.style.color = '';
+ btn.style.borderColor = '';
+ }, 2500);
+ return;
+ }
+ await chrome.storage.local.remove([
+ 'statsTotalCount', 'statsTypeCounts', 'statsAirlineCounts'
+ ]);
+ btn.dataset.confirm = '';
+ btn.textContent = 'Reset statistics';
+ btn.style.color = '';
+ btn.style.borderColor = '';
+ renderStats();
+ });
+}
+
+// ─── STATISTIEKEN RENDEREN ─────────────────────────────────────────────────
+
+async function renderStats() {
+ const container = document.getElementById('statsPanelInner');
+ if (!container) return;
+
+ const { statsTotalCount = 0, statsFirstDetection, statsTypeCounts = {}, statsAirlineCounts = {} } =
+ await chrome.storage.local.get(['statsTotalCount', 'statsFirstDetection', 'statsTypeCounts', 'statsAirlineCounts']);
+
+ if (statsTotalCount === 0 && !statsFirstDetection) {
+ container.innerHTML = '
No data yet. Stats are recorded once notifications start firing.
';
+ return;
+ }
+
+ function topN(obj, n) {
+ return Object.entries(obj)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, n);
+ }
+
+ const topTypes = topN(statsTypeCounts, 5);
+ const topAirlines = topN(statsAirlineCounts, 5);
+
+ const firstDate = statsFirstDetection
+ ? new Date(statsFirstDetection).toLocaleDateString([], { day: '2-digit', month: 'short', year: 'numeric' })
+ : '—';
+
+ function barRow(label, count, max) {
+ const pct = max > 0 ? Math.round((count / max) * 100) : 0;
+ return `
+
+
${label}
+
+
${count}
+
+ `;
+ }
+
+ const maxType = topTypes[0]?.[1] || 1;
+ const maxAirline = topAirlines[0]?.[1] || 1;
+
+ container.innerHTML = `
+
+
+ ${topTypes.length > 0 ? `
+
Top aircraft types
+ ${topTypes.map(([t, c]) => barRow(t, c, maxType)).join('')}
+ ` : ''}
+
+ ${topAirlines.length > 0 ? `
+
Top airlines
+ ${topAirlines.map(([a, c]) => barRow(a, c, maxAirline)).join('')}
+ ` : ''}
+ `;
}
async function renderCaughtList() {
@@ -64,14 +201,31 @@ async function renderCaughtList() {
const label = caughtAircraftLabels[hex] || hex;
const row = document.createElement('div');
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:5px 0;border-bottom:1px solid #1a2040;gap:8px';
- row.innerHTML = `
-
-
${label}
- ${label !== hex ? `
${hex}
` : ''}
-
-
↩️ Release
- `;
- row.querySelector('button').addEventListener('click', async () => {
+
+ const infoDiv = document.createElement('div');
+ infoDiv.style.minWidth = '0';
+
+ const labelDiv = document.createElement('div');
+ labelDiv.style.cssText = "font-family:'Space Mono',monospace;font-size:11px;color:#c8d4f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis";
+ labelDiv.textContent = label;
+ infoDiv.appendChild(labelDiv);
+
+ if (label !== hex) {
+ const hexDiv = document.createElement('div');
+ hexDiv.style.cssText = "font-family:'Space Mono',monospace;font-size:9px;color:#3a4560";
+ hexDiv.textContent = hex;
+ infoDiv.appendChild(hexDiv);
+ }
+
+ const releaseBtn = document.createElement('button');
+ releaseBtn.style.cssText = 'flex-shrink:0;background:none;border:1px solid #2a3060;color:#4b5680;font-size:10px;padding:2px 8px;border-radius:5px;cursor:pointer';
+ releaseBtn.dataset.hex = hex;
+ releaseBtn.textContent = '↩️ Release';
+
+ row.appendChild(infoDiv);
+ row.appendChild(releaseBtn);
+
+ releaseBtn.addEventListener('click', async () => {
const { caughtAircraft: current = [], caughtAircraftLabels: labels = {} } =
await chrome.storage.local.get(['caughtAircraft', 'caughtAircraftLabels']);
delete labels[hex];
@@ -103,18 +257,33 @@ async function loadHistory() {
const time = new Date(entry.ts);
const timeStr = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const dateStr = time.toLocaleDateString([], { day: '2-digit', month: 'short' });
- item.innerHTML = `
-
- ${entry.callsign}
- ${dateStr} ${timeStr}
-
-
${entry.detail}
- `;
+
+ const topRow = document.createElement('div');
+ topRow.className = 'history-item-top';
+
+ const callsignSpan = document.createElement('span');
+ callsignSpan.className = 'history-callsign';
+ callsignSpan.textContent = entry.callsign;
+
+ const timeSpan = document.createElement('span');
+ timeSpan.className = 'history-time';
+ timeSpan.textContent = `${dateStr} ${timeStr}`;
+
+ topRow.appendChild(callsignSpan);
+ topRow.appendChild(timeSpan);
+
+ const detailDiv = document.createElement('div');
+ detailDiv.className = 'history-detail';
+ detailDiv.textContent = entry.detail;
+
+ item.appendChild(topRow);
+ item.appendChild(detailDiv);
+
if (entry.hex) {
item.style.cursor = 'pointer';
item.title = 'Open on map';
item.addEventListener('click', () => {
- chrome.tabs.create({ url: `https://globe.airplanes.live/?icao=${entry.hex}` });
+ chrome.tabs.create({ url: `https://globe.airplanes.live/?icao=${encodeURIComponent(entry.hex)}` });
});
}
list.appendChild(item);
diff --git a/popup/live.js b/popup/live.js
index abe68af..d70516e 100644
--- a/popup/live.js
+++ b/popup/live.js
@@ -1,4 +1,4 @@
-// live.js v1.1.0 — injecteert Live tab HTML en beheert vliegtuigenlijst, filters, detail dropdown
+// live.js v1.3.1 — injecteert Live tab HTML en beheert vliegtuigenlijst, filters, detail dropdown
// ─── HTML INJECTIE ──────────────────────────────────────────────────────────
@@ -41,9 +41,7 @@ function initLiveTab() {
0 m
-
-
Press refresh to load aircraft.
-
+
`;
setupLiveEvents();
}
@@ -63,6 +61,7 @@ let searchQuery = '';
let liveSettingsCache = null;
let autoRefreshTimer = null;
let autoRefreshCountdown = null;
+let renderDebounceTimer = null;
// ─── AUTO-REFRESH ──────────────────────────────────────────────────────────
@@ -214,6 +213,9 @@ function setupLiveEvents() {
renderAircraftList();
});
+ // ── Gedebouncede storage listener ──────────────────────────────────────
+ // Voorkomt dat snelle opeenvolgende changes (bijv. unit-wissel) meerdere
+ // renders triggeren. Cache wordt direct geïnvalideerd; render wacht 50ms.
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'local') return;
if (!changes.units && !changes.hideGround && !changes.alerts && !changes.caughtAircraft) return;
@@ -227,14 +229,14 @@ function setupLiveEvents() {
}
liveSettingsCache = null;
- renderAircraftList();
+
+ clearTimeout(renderDebounceTimer);
+ renderDebounceTimer = setTimeout(() => renderAircraftList(), 50);
});
}
// ─── BELL BUTTON HELPER ────────────────────────────────────────────────────
-// Maakt een bell-knop aan voor een alert-type/waarde combinatie.
-// Grijs + doorgestreept = geen alert actief. Groen = alert bestaat. Klik togglet.
async function createBellBtn(type, value) {
if (!value) return null;
@@ -279,7 +281,6 @@ async function createBellBtn(type, value) {
await chrome.storage.local.set({ alerts });
await syncState();
- // Alerts tab direct bijwerken
if (typeof renderAlerts === 'function') {
renderAlerts(alerts);
}
@@ -356,7 +357,9 @@ async function renderAircraftList() {
matchEl.textContent = aircraft.filter(isMatch).length;
if (aircraft.length === 0) {
- list.innerHTML = 'No aircraft match the current filters.
';
+ list.innerHTML = lastAcData.length === 0
+ ? 'Press refresh to load aircraft.
'
+ : 'No aircraft match the current filters.
';
currentDetailHex = null;
return;
}
@@ -381,23 +384,64 @@ async function renderAircraftList() {
const item = document.createElement('div');
item.className = `ac-item${match ? ' match' : ''}${isOpen ? ' open' : ''}`;
- item.innerHTML = `
-
-
${flight}${match ? 'MATCH ' : ''}${caught ? 'CAUGHT ' : ''}
-
${route}
- ${dist ? `
${dist}
` : ''}
-
-
-
${altitude}
-
${isOpen ? '▲' : '▼'}
-
- `;
+
+ // ── Linker kolom: flight, route, distance ──
+ const leftDiv = document.createElement('div');
+ leftDiv.style.minWidth = '0';
+
+ const flightDiv = document.createElement('div');
+ flightDiv.className = 'ac-flight';
+ flightDiv.appendChild(document.createTextNode(flight));
+ if (match) {
+ const matchBadge = document.createElement('span');
+ matchBadge.className = 'match-badge';
+ matchBadge.textContent = 'MATCH';
+ flightDiv.appendChild(matchBadge);
+ }
+ if (caught) {
+ const caughtBadge = document.createElement('span');
+ caughtBadge.className = 'caught-badge';
+ caughtBadge.textContent = 'CAUGHT';
+ flightDiv.appendChild(caughtBadge);
+ }
+
+ const detailDiv = document.createElement('div');
+ detailDiv.className = 'ac-detail';
+ detailDiv.textContent = route;
+
+ leftDiv.appendChild(flightDiv);
+ leftDiv.appendChild(detailDiv);
+
+ if (dist) {
+ const distDiv = document.createElement('div');
+ distDiv.className = 'ac-distance';
+ distDiv.textContent = dist;
+ leftDiv.appendChild(distDiv);
+ }
+
+ // ── Rechter kolom: altitude, chevron ──
+ const rightDiv = document.createElement('div');
+ rightDiv.style.cssText = 'display:flex;align-items:center;gap:8px';
+
+ const altDiv = document.createElement('div');
+ altDiv.className = 'ac-altitude';
+ altDiv.textContent = altitude;
+
+ const chevronSpan = document.createElement('span');
+ chevronSpan.className = 'ac-chevron';
+ chevronSpan.textContent = isOpen ? '▲' : '▼';
+
+ rightDiv.appendChild(altDiv);
+ rightDiv.appendChild(chevronSpan);
+
+ item.appendChild(leftDiv);
+ item.appendChild(rightDiv);
const dropdown = document.createElement('div');
dropdown.className = `ac-dropdown${isOpen ? ' open' : ''}`;
if (isOpen) {
- await buildDropdownContent(dropdown, ac, units, caught);
+ await buildDropdownContent(dropdown, ac, units);
}
item.addEventListener('click', async () => {
@@ -421,7 +465,7 @@ async function renderAircraftList() {
item.classList.add('open');
item.querySelector('.ac-chevron').textContent = '▲';
dropdown.classList.add('open');
- await buildDropdownContent(dropdown, ac, units, caught);
+ await buildDropdownContent(dropdown, ac, units);
wrapper.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
@@ -434,8 +478,12 @@ async function renderAircraftList() {
// ─── DROPDOWN CONTENT ──────────────────────────────────────────────────────
-async function buildDropdownContent(dropdown, ac, units, caught) {
- // Cellen met optionele bell-knop (bell: null = geen knop)
+// Storage reads samengevoegd tot één call bovenaan de functie.
+// De catch-knop hergebruikt dezelfde data en doet geen losse get meer.
+async function buildDropdownContent(dropdown, ac, units) {
+ const { caughtAircraft: caughtList = [], caughtAircraftLabels: labels = {} } =
+ await chrome.storage.local.get(['caughtAircraft', 'caughtAircraftLabels']);
+
const cellDefs = [
{ label: 'Registration', val: ac.r || '—', bell: { type: 'registration', value: ac.r } },
{ label: 'Type', val: ac.t || '—', bell: { type: 'type', value: ac.t } },
@@ -466,7 +514,6 @@ async function buildDropdownContent(dropdown, ac, units, caught) {
valEl.textContent = def.val;
valRow.appendChild(valEl);
- // Voeg bell-knop toe als het veld een waarde heeft
if (def.bell && def.bell.value) {
const bellBtn = await createBellBtn(def.bell.type, def.bell.value);
if (bellBtn) valRow.appendChild(bellBtn);
@@ -482,33 +529,29 @@ async function buildDropdownContent(dropdown, ac, units, caught) {
mapBtn.textContent = '🗺️ Open on map';
mapBtn.addEventListener('click', (e) => {
e.stopPropagation();
- chrome.tabs.create({ url: `https://globe.airplanes.live/?icao=${ac.hex}` });
+ chrome.tabs.create({ url: `https://globe.airplanes.live/?icao=${encodeURIComponent(ac.hex)}` });
});
const catchBtn = document.createElement('button');
catchBtn.className = 'detail-catch-btn';
- async function updateCatchBtn() {
- const { caughtAircraft: current = [] } = await chrome.storage.local.get('caughtAircraft');
- const isCaughtNow = current.includes(ac.hex);
- catchBtn.textContent = isCaughtNow ? '↩️ Release this aircraft' : '🎯 Catch this aircraft';
- }
-
- await updateCatchBtn();
+ // Gebruik de al opgehaalde caughtList -- geen extra storage read nodig
+ const isCaughtNow = caughtList.includes(ac.hex);
+ catchBtn.textContent = isCaughtNow ? '↩️ Release this aircraft' : '🎯 Catch this aircraft';
catchBtn.addEventListener('click', async (e) => {
e.stopPropagation();
- const { caughtAircraft: current = [], caughtAircraftLabels: labels = {} } =
+ const { caughtAircraft: current = [], caughtAircraftLabels: currentLabels = {} } =
await chrome.storage.local.get(['caughtAircraft', 'caughtAircraftLabels']);
const idx = current.indexOf(ac.hex);
if (idx === -1) {
current.push(ac.hex);
- labels[ac.hex] = buildCaughtLabel(ac);
+ currentLabels[ac.hex] = buildCaughtLabel(ac);
} else {
current.splice(idx, 1);
- delete labels[ac.hex];
+ delete currentLabels[ac.hex];
}
- await chrome.storage.local.set({ caughtAircraft: current, caughtAircraftLabels: labels });
+ await chrome.storage.local.set({ caughtAircraft: current, caughtAircraftLabels: currentLabels });
currentDetailHex = null;
renderAircraftList();
});
diff --git a/popup/popup.css b/popup/popup.css
index 02f820c..ea91cd4 100644
--- a/popup/popup.css
+++ b/popup/popup.css
@@ -1,9 +1,64 @@
-* { box-sizing: border-box; margin: 0; padding: 0; }
+/* ─── LOKALE FONTS ─────────────────────────────────────────────────────────── */
+@font-face {
+ font-family: 'Space Mono';
+ src: url('../fonts/space-mono-latin-400-normal.woff2') format('woff2');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Space Mono';
+ src: url('../fonts/space-mono-latin-700-normal.woff2') format('woff2');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Outfit';
+ src: url('../fonts/outfit-latin-400-normal.woff2') format('woff2');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Outfit';
+ src: url('../fonts/outfit-latin-600-normal.woff2') format('woff2');
+ font-weight: 600;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Outfit';
+ src: url('../fonts/outfit-latin-800-normal.woff2') format('woff2');
+ font-weight: 800;
+ font-style: normal;
+ font-display: swap;
+}
+
+/* ─── CSS VARIABELEN ───────────────────────────────────────────────────────── */
+:root {
+ --bg: #08090f;
+ --surface: #0f1322;
+ --surface-alt: #0d1020;
+ --surface-dark:#080a12;
+ --border: #1e2840;
+ --border-blue: #2a4a8a;
+ --blue: #60a5fa;
+ --blue-dim: #1a3a6a;
+ --green: #22c55e;
+ --text: #e8e4f0;
+ --subtext: #8b9cc8;
+ --muted: #7a8ab0;
+ --muted-dark: #5a6a90;
+}
+
+/* ─── RESET ────────────────────────────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
width: 380px;
- background: #08090f;
- color: #e8e4f0;
+ background: var(--bg);
+ color: var(--text);
font-family: 'Outfit', sans-serif;
font-size: 13px;
display: flex;
@@ -12,11 +67,11 @@ body {
overflow: hidden;
}
-/* ── Header ── */
+/* ─── HEADER ───────────────────────────────────────────────────────────────── */
.header {
background: linear-gradient(135deg, #0f1729 0%, #0a1020 100%);
padding: 16px 18px;
- border-bottom: 1px solid #1a2040;
+ border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
@@ -25,16 +80,15 @@ body {
.header-icon { font-size: 22px; }
.header-title {
- font-family: 'Outfit', sans-serif;
font-weight: 800;
font-size: 17px;
color: #fff;
letter-spacing: -0.4px;
}
-.header-title span { color: #60a5fa; }
+.header-title span { color: var(--blue); }
-/* ── Master toggle ── */
+/* ─── MASTER TOGGLE ────────────────────────────────────────────────────────── */
.master-toggle {
margin-left: auto;
display: flex;
@@ -48,8 +102,7 @@ body {
color: #4b5680;
transition: color 0.2s;
}
-
-.master-toggle-label.on { color: #22c55e; }
+.master-toggle-label.on { color: var(--green); }
.toggle-pill {
width: 40px;
@@ -62,7 +115,6 @@ body {
transition: background 0.25s, border-color 0.25s;
flex-shrink: 0;
}
-
.toggle-pill::after {
content: '';
position: absolute;
@@ -74,19 +126,10 @@ body {
left: 3px;
transition: all 0.25s;
}
+.toggle-pill.on { background: #14532d; border-color: #22c55e44; }
+.toggle-pill.on::after { background: var(--green); left: 21px; box-shadow: 0 0 6px #22c55e88; }
-.toggle-pill.on {
- background: #14532d;
- border-color: #22c55e44;
-}
-
-.toggle-pill.on::after {
- background: #22c55e;
- left: 21px;
- box-shadow: 0 0 6px #22c55e88;
-}
-
-/* ── Status dot ── */
+/* ─── STATUS DOT ───────────────────────────────────────────────────────────── */
.status-dot {
display: flex;
align-items: center;
@@ -96,26 +139,22 @@ body {
font-family: 'Space Mono', monospace;
}
-.dot {
- width: 7px; height: 7px;
- border-radius: 50%;
- background: #1e3a1e;
-}
-.dot.active { background: #22c55e; box-shadow: 0 0 6px #22c55e88; animation: pulse 2s infinite; }
+.dot { width: 7px; height: 7px; border-radius: 50%; background: #1e3a1e; }
+.dot.active { background: var(--green); box-shadow: 0 0 6px #22c55e88; animation: pulse 2s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
+ 50% { opacity: 0.5; }
}
-/* ── Help button ── */
+/* ─── HELP BUTTON ──────────────────────────────────────────────────────────── */
.btn-help {
width: 22px;
height: 22px;
border-radius: 50%;
background: #1a2a4a;
border: 1px solid #2a3a6a;
- color: #60a5fa;
+ color: var(--blue);
font-size: 12px;
font-weight: 700;
display: flex;
@@ -127,7 +166,7 @@ body {
}
.btn-help:hover { background: #1e3255; }
-/* ── Help overlay ── */
+/* ─── HELP OVERLAY ─────────────────────────────────────────────────────────── */
.help-overlay {
display: none;
position: fixed;
@@ -141,8 +180,8 @@ body {
.help-overlay.visible { display: flex; }
.help-box {
- background: #0d1020;
- border: 1px solid #1e2840;
+ background: var(--surface-alt);
+ border: 1px solid var(--border);
border-radius: 12px;
width: 348px;
max-height: calc(100% - 32px);
@@ -156,8 +195,7 @@ body {
justify-content: space-between;
align-items: center;
padding: 12px 16px;
- border-bottom: 1px solid #1e2840;
- font-family: 'Outfit', sans-serif;
+ border-bottom: 1px solid var(--border);
font-weight: 700;
font-size: 14px;
color: #fff;
@@ -188,23 +226,14 @@ body {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
- color: #60a5fa;
+ color: var(--blue);
margin-bottom: 8px;
}
-.help-section p {
- font-size: 11px;
- color: #8b9cc8;
- line-height: 1.6;
-}
-
+.help-section p { font-size: 11px; color: var(--subtext); line-height: 1.6; }
.help-section p strong { color: #c0cce8; }
-.help-table {
- display: flex;
- flex-direction: column;
- gap: 5px;
-}
+.help-table { display: flex; flex-direction: column; gap: 5px; }
.help-row {
display: grid;
@@ -213,17 +242,15 @@ body {
font-size: 11px;
line-height: 1.5;
}
-
.help-row.faq { grid-template-columns: 1fr; gap: 2px; }
-.help-row.faq span:first-child, .help-q { color: #c0cce8; font-weight: 600; }
-
+.help-row.faq span:first-child,
+.help-q { color: #c0cce8; font-weight: 600; }
.help-key {
font-family: 'Space Mono', monospace;
font-size: 10px;
- color: #e8e4f0;
+ color: var(--text);
padding-top: 1px;
}
-
.help-row span:last-child { color: #6b7ca8; }
.help-row code {
background: #1a2040;
@@ -234,10 +261,10 @@ body {
color: #7dd3fc;
}
-/* ── Tabs ── */
+/* ─── TABS ─────────────────────────────────────────────────────────────────── */
.tabs {
display: flex;
- border-bottom: 1px solid #1a2040;
+ border-bottom: 1px solid var(--border);
background: #0a0c14;
}
@@ -252,11 +279,10 @@ body {
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
+.tab:hover { color: var(--subtext); }
+.tab.active { color: var(--blue); border-bottom-color: var(--blue); }
-.tab:hover { color: #8b9cc8; }
-.tab.active { color: #60a5fa; border-bottom-color: #60a5fa; }
-
-/* ── Panels wrapper ── */
+/* ─── PANELS ───────────────────────────────────────────────────────────────── */
.panels-wrapper { position: relative; flex: 1; min-height: 0; overflow-y: auto; }
.disabled-overlay {
@@ -270,32 +296,20 @@ body {
flex-direction: column;
gap: 10px;
}
+.disabled-overlay.visible { display: flex; }
+.disabled-overlay-icon { font-size: 32px; opacity: 0.5; }
+.disabled-overlay-text { font-size: 13px; font-weight: 600; color: var(--subtext); text-align: center; line-height: 1.7; }
-.disabled-overlay.visible { display: flex; }
-
-.disabled-overlay-icon {
- font-size: 32px;
- opacity: 0.5;
-}
-
-.disabled-overlay-text {
- font-size: 13px;
- font-weight: 600;
- color: #8b9cc8;
- text-align: center;
- line-height: 1.7;
-}
-
-.panel { display: none; padding: 16px 18px; }
+.panel { display: none; padding: 16px 18px; }
.panel.active { display: block; }
-/* ── Shared components ── */
+/* ─── SHARED COMPONENTS ────────────────────────────────────────────────────── */
.section-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
- color: #7a8ab0;
+ color: var(--muted);
margin-bottom: 10px;
}
@@ -310,73 +324,52 @@ button {
.btn-add {
width: 100%;
- background: #1a3a6a;
- color: #60a5fa;
+ background: var(--blue-dim);
+ color: var(--blue);
padding: 9px;
font-size: 13px;
- border: 1px solid #2a4a8a;
+ border: 1px solid var(--border-blue);
}
.btn-add:hover { background: #1e4278; }
-.btn-group {
- display: flex;
- gap: 6px;
- margin-bottom: 6px;
-}
+.btn-group { display: flex; gap: 6px; margin-bottom: 6px; }
.btn-option {
flex: 1;
padding: 7px 4px;
font-size: 12px;
- font-family: 'Outfit', sans-serif;
font-weight: 600;
- background: #0f1322;
- border: 1px solid #1e2840;
- color: #7a8ab0; /* was #4b5680 */
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--muted);
border-radius: 7px;
cursor: pointer;
transition: all 0.15s;
text-align: center;
}
+.btn-option:hover { color: var(--subtext); border-color: #2a3860; }
+.btn-option.active { background: var(--blue-dim); border-color: var(--border-blue); color: var(--blue); }
-.btn-option:hover { color: #8b9cc8; border-color: #2a3860; }
-.btn-option.active { background: #1a3a6a; border-color: #2a4a8a; color: #60a5fa; }
-
-.custom-slider-row {
- display: none;
- flex-direction: column;
- gap: 4px;
- margin-bottom: 4px;
-}
-
+.custom-slider-row { display: none; flex-direction: column; gap: 4px; margin-bottom: 4px; }
.custom-slider-row.visible { display: flex; }
-.slider-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
+.slider-header { display: flex; justify-content: space-between; align-items: center; }
-input[type="range"] {
- width: 100%;
- accent-color: #60a5fa;
- cursor: pointer;
-}
+input[type="range"] { width: 100%; accent-color: var(--blue); cursor: pointer; }
select, input[type="text"] {
- background: #080a12;
- border: 1px solid #1e2840;
+ background: var(--surface-dark);
+ border: 1px solid var(--border);
border-radius: 6px;
- color: #e8e4f0;
+ color: var(--text);
font-family: 'Outfit', sans-serif;
font-size: 12px;
padding: 7px 10px;
outline: none;
transition: border-color 0.15s;
}
-
-select:focus, input[type="text"]:focus { border-color: #60a5fa; }
-select { flex: 0 0 130px; cursor: pointer; }
+select:focus, input[type="text"]:focus { border-color: var(--blue); }
+select { flex: 0 0 130px; cursor: pointer; }
input[type="text"] { flex: 1; }
input::placeholder { color: #3a4560; }
@@ -390,46 +383,35 @@ input::placeholder { color: #3a4560; }
line-height: 1.6;
}
-.empty-state {
- text-align: center;
- color: #5a6a90; /* was #2a3060 */
- padding: 20px;
- font-size: 12px;
- line-height: 1.7;
-}
+.empty-state { text-align: center; color: var(--muted-dark); padding: 20px; font-size: 12px; line-height: 1.7; }
-/* ── Alert items ── */
+/* ─── ALERTS TAB ───────────────────────────────────────────────────────────── */
.alert-form {
- background: #0f1322;
- border: 1px solid #1e2840;
+ background: var(--surface);
+ border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
}
-.form-row {
- display: flex;
- gap: 8px;
- margin-bottom: 8px;
-}
-
+.form-row { display: flex; gap: 8px; margin-bottom: 8px; }
.alert-list { display: flex; flex-direction: column; gap: 6px; }
.alert-item {
display: flex;
align-items: center;
gap: 10px;
- background: #0f1322;
- border: 1px solid #1e2840;
+ background: var(--surface);
+ border: 1px solid var(--border);
border-radius: 8px;
padding: 9px 12px;
transition: border-color 0.15s;
}
-
.alert-item:hover { border-color: #2a3860; }
.alert-toggle {
- width: 32px; height: 17px;
+ width: 32px;
+ height: 17px;
background: #1a2040;
border-radius: 20px;
cursor: pointer;
@@ -438,76 +420,82 @@ input::placeholder { color: #3a4560; }
border: none;
transition: background 0.2s;
}
-
.alert-toggle::after {
content: '';
position: absolute;
- width: 11px; height: 11px;
+ width: 11px;
+ height: 11px;
background: #4b5680;
border-radius: 50%;
- top: 3px; left: 3px;
+ top: 3px;
+ left: 3px;
transition: all 0.2s;
}
-
-.alert-toggle.on { background: #1a3a2a; }
-.alert-toggle.on::after { background: #22c55e; left: 18px; }
+.alert-toggle.on { background: #1a3a2a; }
+.alert-toggle.on::after { background: var(--green); left: 18px; }
.alert-info { flex: 1; min-width: 0; }
.alert-value {
font-weight: 600;
font-size: 13px;
- color: #e8e4f0;
+ color: var(--text);
font-family: 'Space Mono', monospace;
}
.alert-type-label {
font-size: 10px;
- color: #5a6a90; /* was #3a4560 */
+ color: var(--muted-dark);
text-transform: uppercase;
letter-spacing: 0.5px;
}
-.btn-remove {
- background: none;
- color: #2a3060;
- font-size: 16px;
- padding: 2px 6px;
- flex-shrink: 0;
-}
+.btn-remove { background: none; color: #2a3060; font-size: 16px; padding: 2px 6px; flex-shrink: 0; }
.btn-remove:hover { color: #ef4444; }
-/* ── Live tab ── */
-.live-stats {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
- margin-bottom: 14px;
-}
+/* ── Alert note ── */
+.alert-note-input { width: 100%; margin-bottom: 8px; }
-.stat-card {
- background: #0f1322;
- border: 1px solid #1e2840;
- border-radius: 8px;
- padding: 10px 12px;
+.alert-note {
+ font-size: 10px;
+ color: var(--muted);
+ margin-top: 3px;
+ cursor: pointer;
+ min-height: 14px;
+ transition: color 0.15s;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
+.alert-note:hover { color: var(--subtext); }
-.stat-card .label {
- font-size: 9px;
- text-transform: uppercase;
- letter-spacing: 0.8px;
- color: #5a6a90; /* was #3a4560 */
- margin-bottom: 4px;
-}
+.alert-note-empty { color: #4b5680; font-style: italic; }
+.alert-note:hover .alert-note-empty { color: var(--muted); }
-.stat-card .value {
- font-family: 'Space Mono', monospace;
- font-size: 18px;
- font-weight: 700;
- color: #60a5fa;
+.alert-note-field {
+ width: 100%;
+ font-size: 10px;
+ padding: 2px 5px;
+ background: var(--surface-dark);
+ border: 1px solid var(--blue);
+ border-radius: 4px;
+ color: var(--text);
+ font-family: 'Outfit', sans-serif;
+ outline: none;
}
-.stat-card .value.green { color: #22c55e; }
+/* ─── LIVE TAB ─────────────────────────────────────────────────────────────── */
+.live-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 14px; }
+
+.stat-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 10px 12px;
+}
+.stat-card .label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted-dark); margin-bottom: 4px; }
+.stat-card .value { font-family: 'Space Mono', monospace; font-size: 18px; font-weight: 700; color: var(--blue); }
+.stat-card .value.green { color: var(--green); }
.ac-list { display: flex; flex-direction: column; gap: 5px; }
@@ -515,206 +503,115 @@ input::placeholder { color: #3a4560; }
display: flex;
justify-content: space-between;
align-items: center;
- background: #0f1322;
- border: 1px solid #1e2840;
+ background: var(--surface);
+ border: 1px solid var(--border);
border-radius: 6px;
padding: 7px 10px;
cursor: pointer;
transition: border-color 0.15s;
}
+.ac-item:hover { border-color: var(--blue); }
+.ac-item.match { border-color: #22c55e44; background: #0f1f12; }
+.ac-item.open { border-color: var(--border-blue); border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
-.ac-item:hover { border-color: #60a5fa; }
-.ac-item.match { border-color: #22c55e44; background: #0f1f12; }
-
-.ac-flight {
- font-family: 'Space Mono', monospace;
- font-size: 12px;
- color: #e8e4f0;
- font-weight: 700;
-}
-
-.ac-detail {
- font-size: 10px;
- color: #7a8ab0; /* was #4b5680 */
- margin-top: 2px;
-}
-
-.ac-altitude {
- font-family: 'Space Mono', monospace;
- font-size: 11px;
- color: #7a8ab0; /* was #4b5680 */
- text-align: right;
-}
-
-.match-badge {
- font-size: 9px;
- background: #14532d;
- color: #4ade80;
- padding: 2px 6px;
- border-radius: 10px;
- margin-left: 6px;
-}
+.ac-flight { font-family: 'Space Mono', monospace; font-size: 12px; color: var(--text); font-weight: 700; }
+.ac-detail { font-size: 10px; color: var(--muted); margin-top: 2px; }
+.ac-altitude { font-family: 'Space Mono', monospace; font-size: 11px; color: var(--muted); text-align: right; }
+.ac-distance { font-family: 'Space Mono', monospace; font-size: 10px; color: var(--muted-dark); margin-top: 2px; }
+.ac-chevron { font-size: 8px; color: var(--muted-dark); flex-shrink: 0; }
-.caught-badge {
+.match-badge, .caught-badge {
font-size: 9px;
- background: #0a2a1a;
color: #4ade80;
padding: 2px 6px;
border-radius: 10px;
margin-left: 6px;
}
+.match-badge { background: #14532d; }
+.caught-badge { background: #0a2a1a; }
-.detail-catch-btn {
- width: 100%;
- background: #1a1a2a;
- color: #6b7ca8;
- border: 1px solid #2a2a4a;
- padding: 8px;
- font-size: 12px;
- font-family: 'Outfit', sans-serif;
- font-weight: 600;
- border-radius: 7px;
- margin-top: 6px;
- cursor: pointer;
- transition: all 0.15s;
-}
-.detail-catch-btn:hover { color: #4ade80; border-color: #22c55e44; background: #0f1f12; }
-
-.live-controls {
- margin-bottom: 10px;
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.control-row {
- display: flex;
- gap: 6px;
- align-items: center;
-}
+/* ── Live controls ── */
+.live-controls { margin-bottom: 10px; display: flex; flex-direction: column; gap: 6px; }
-.control-row select {
- flex: 0 0 auto;
- font-size: 11px;
- padding: 5px 8px;
-}
+.control-row { display: flex; gap: 6px; align-items: center; }
+.control-row select { flex: 0 0 auto; font-size: 11px; padding: 5px 8px; }
-.filter-toggles {
- display: flex;
- gap: 5px;
- flex-wrap: wrap;
-}
+.filter-toggles { display: flex; gap: 5px; flex-wrap: wrap; }
.filter-btn {
font-size: 10px;
- font-family: 'Outfit', sans-serif;
font-weight: 600;
padding: 5px 9px;
- background: #0f1322;
- border: 1px solid #1e2840;
- color: #7a8ab0; /* was #4b5680 */
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--muted);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
+.filter-btn:hover { color: var(--subtext); border-color: #2a3860; }
+.filter-btn.active { background: var(--blue-dim); border-color: var(--border-blue); color: var(--blue); }
-.filter-btn:hover { color: #8b9cc8; border-color: #2a3860; }
-.filter-btn.active { background: #1a3a6a; border-color: #2a4a8a; color: #60a5fa; }
-
-.alt-label {
- font-size: 10px;
- color: #5a6a90; /* was #3a4560 */
- white-space: nowrap;
-}
+.alt-label { font-size: 10px; color: var(--muted-dark); white-space: nowrap; }
.live-search {
flex: 1;
font-size: 11px;
- font-family: 'Outfit', sans-serif;
padding: 5px 8px;
- background: #0f1322;
- border: 1px solid #1e2840;
+ background: var(--surface);
+ border: 1px solid var(--border);
border-radius: 6px;
- color: #e8e4f0;
+ color: var(--text);
outline: none;
transition: border-color 0.15s;
}
-
.live-search::placeholder { color: #3a4560; }
-.live-search:focus { border-color: #2a4a8a; }
+.live-search:focus { border-color: var(--border-blue); }
.alt-value {
font-family: 'Space Mono', monospace;
font-size: 10px;
- color: #60a5fa;
+ color: var(--blue);
white-space: nowrap;
min-width: 42px;
text-align: right;
}
-.ac-distance {
- font-family: 'Space Mono', monospace;
- font-size: 10px;
- color: #5a6a90; /* was #3a4560 */
- margin-top: 2px;
-}
-
.refresh-btn {
flex: 1;
- background: #0f1322;
- border: 1px solid #1e2840;
- color: #7a8ab0;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--muted);
padding: 8px;
font-size: 12px;
}
-.refresh-btn:hover { color: #60a5fa; border-color: #2a3860; }
+.refresh-btn:hover { color: var(--blue); border-color: #2a3860; }
-.refresh-row {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 10px;
-}
+.refresh-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.auto-refresh-countdown {
font-family: 'Space Mono', monospace;
font-size: 10px;
- color: #5a6a90;
+ color: var(--muted-dark);
white-space: nowrap;
min-width: 44px;
text-align: right;
}
-/* ── Aircraft wrapper & dropdown ── */
-.ac-wrapper {
- display: flex;
- flex-direction: column;
-}
-
-.ac-item.open {
- border-color: #2a4a8a;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-
-.ac-chevron {
- font-size: 8px;
- color: #5a6a90; /* was #3a4560 */
- flex-shrink: 0;
-}
+/* ── Aircraft dropdown ── */
+.ac-wrapper { display: flex; flex-direction: column; }
.ac-dropdown {
display: none;
background: #0d1120;
- border: 1px solid #2a4a8a;
+ border: 1px solid var(--border-blue);
border-top: none;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
padding: 10px;
animation: slideIn 0.15s ease;
}
-
.ac-dropdown.open { display: block; }
@keyframes slideIn {
@@ -722,172 +619,137 @@ input::placeholder { color: #3a4560; }
to { opacity: 1; transform: translateY(0); }
}
-.detail-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 6px;
- margin-bottom: 10px;
-}
+.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 10px; }
.detail-cell {
- background: #080a12;
- border: 1px solid #1e2840;
+ background: var(--surface-dark);
+ border: 1px solid var(--border);
border-radius: 6px;
padding: 7px 10px;
}
+.detail-cell .label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted-dark); margin-bottom: 3px; }
+.detail-cell .val { font-family: 'Space Mono', monospace; font-size: 12px; color: var(--text); }
-.detail-cell .label {
- font-size: 9px;
- text-transform: uppercase;
- letter-spacing: 0.8px;
- color: #5a6a90; /* was #3a4560 */
- margin-bottom: 3px;
-}
-
-.detail-cell .val {
- font-family: 'Space Mono', monospace;
+.detail-map-btn {
+ width: 100%;
+ background: var(--blue-dim);
+ color: var(--blue);
+ border: 1px solid var(--border-blue);
+ padding: 8px;
font-size: 12px;
- color: #e8e4f0;
+ font-weight: 600;
+ border-radius: 7px;
}
+.detail-map-btn:hover { background: #1e4278; }
-.detail-map-btn {
+.detail-catch-btn {
width: 100%;
- background: #1a3a6a;
- color: #60a5fa;
- border: 1px solid #2a4a8a;
+ background: #1a1a2a;
+ color: #6b7ca8;
+ border: 1px solid #2a2a4a;
padding: 8px;
font-size: 12px;
- font-family: 'Outfit', sans-serif;
font-weight: 600;
border-radius: 7px;
+ margin-top: 6px;
+ cursor: pointer;
+ transition: all 0.15s;
}
-.detail-map-btn:hover { background: #1e4278; }
+.detail-catch-btn:hover { color: #4ade80; border-color: #22c55e44; background: #0f1f12; }
-/* ── History tab ── */
-.history-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
+.detail-bell-btn {
+ background: none;
+ border: none;
+ font-size: 14px;
+ cursor: pointer;
+ padding: 0 2px;
+ line-height: 1;
+ transition: opacity 0.15s;
+ flex-shrink: 0;
}
+.detail-bell-btn:hover { opacity: 1 !important; }
+
+/* ─── HISTORY TAB ──────────────────────────────────────────────────────────── */
+.history-list { display: flex; flex-direction: column; gap: 6px; }
.history-item {
- background: #0f1322;
- border: 1px solid #1e2840;
+ background: var(--surface);
+ border: 1px solid var(--border);
border-radius: 8px;
padding: 9px 12px;
transition: border-color 0.15s;
}
-
.history-item[title]:hover { border-color: #2a3860; }
-.history-item-top {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 3px;
-}
-
-.history-callsign {
- font-family: 'Space Mono', monospace;
- font-size: 12px;
- font-weight: 700;
- color: #e8e4f0;
-}
-
-.history-time {
- font-family: 'Space Mono', monospace;
- font-size: 10px;
- color: #5a6a90; /* was #3a4560 */
-}
-
-.history-detail {
- font-size: 11px;
- color: #7a8ab0; /* was #4b5680 */
-}
+.history-item-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; }
+.history-callsign { font-family: 'Space Mono', monospace; font-size: 12px; font-weight: 700; color: var(--text); }
+.history-time { font-family: 'Space Mono', monospace; font-size: 10px; color: var(--muted-dark); }
+.history-detail { font-size: 11px; color: var(--muted); }
.btn-clear-history {
font-size: 11px;
- font-family: 'Outfit', sans-serif;
font-weight: 600;
background: none;
border: 1px solid #2a3060;
- color: #7a8ab0; /* was #4b5680 */
+ color: var(--muted);
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
}
.btn-clear-history:hover { color: #ef4444; border-color: #ef4444; }
-/* ── Notification builder ── */
-.notif-builder {
- background: #0f1322;
- border: 1px solid #1e2840;
- border-radius: 10px;
- padding: 12px;
- margin-bottom: 12px;
-}
+/* ─── STATISTICS ───────────────────────────────────────────────────────────── */
+.stats-dropdown { border: 1px solid #1a2a4a; border-radius: 8px; overflow: hidden; margin-bottom: 14px; }
-.notif-builder-preview {
- background: #080a12;
- border: 1px solid #1e2840;
- border-radius: 8px;
- padding: 10px 12px;
- margin-bottom: 10px;
+.stats-toggle-btn {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 9px 12px;
+ background: #0d1530;
+ border: none;
+ cursor: pointer;
+ color: #c8d4f0;
+ font-family: 'Space Mono', monospace;
+ font-size: 11px;
}
+.stats-toggle-btn:hover { background: #101a38; }
-.notif-preview-title {
- font-size: 12px;
- font-weight: 700;
- color: #e8e4f0;
- margin-bottom: 3px;
-}
+.stats-chevron { font-size: 10px; transition: transform 0.2s; color: var(--muted-dark); }
-.notif-preview-body {
- font-size: 11px;
- color: #7a8ab0; /* was #4b5680 */
- font-family: 'Space Mono', monospace;
- line-height: 1.5;
-}
+.stats-panel-inner { padding: 10px 12px 4px; background: #080f25; }
-.notif-toggle-list {
- display: flex;
- flex-direction: column;
- gap: 7px;
-}
+.stats-meta-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; }
-.notif-toggle-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 2px;
+.stats-meta-cell {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 8px 10px;
}
+.stats-meta-label { font-family: 'Space Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted-dark); margin-bottom: 4px; }
+.stats-meta-value { font-family: 'Space Mono', monospace; font-size: 15px; font-weight: 700; color: var(--blue); }
-.notif-toggle-label {
- font-size: 12px;
- color: #8b9cc8;
-}
+.stats-section-label { font-family: 'Space Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 0.8px; color: var(--muted-dark); margin-bottom: 6px; }
-.notif-toggle-row .alert-toggle { flex-shrink: 0; }
+.stats-bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
+.stats-bar-label { font-family: 'Space Mono', monospace; font-size: 10px; color: #c8d4f0; width: 44px; flex-shrink: 0; }
+.stats-bar-track { flex: 1; height: 5px; background: #1a2040; border-radius: 3px; overflow: hidden; }
+.stats-bar-fill { height: 100%; background: #2a5aaa; border-radius: 3px; transition: width 0.3s ease; }
+.stats-bar-count { font-family: 'Space Mono', monospace; font-size: 10px; color: var(--muted); width: 24px; text-align: right; flex-shrink: 0; }
+
+#btnResetStats { margin: 0 12px 10px; width: calc(100% - 24px); }
-/* ── Settings kaarten ── */
+/* ─── SETTINGS TAB ─────────────────────────────────────────────────────────── */
.settings-card {
- background: #0d1020;
- border: 1px solid #1e2840;
+ background: var(--surface-alt);
+ border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 10px;
}
-.settings-card-title {
- font-family: 'Outfit', sans-serif;
- font-size: 11px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.06em;
- color: #7a8ab0;
- margin-bottom: 12px;
-}
-
.settings-card-toggle {
width: 100%;
display: flex;
@@ -897,27 +759,17 @@ input::placeholder { color: #3a4560; }
border: none;
cursor: pointer;
padding: 0;
- font-family: 'Outfit', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
- color: #7a8ab0;
-}
-
-.settings-card-toggle:hover {
- color: #a0b0d0;
+ color: var(--muted);
}
+.settings-card-toggle:hover { color: #a0b0d0; }
-.settings-chevron {
- font-size: 9px;
- transition: transform 0.2s;
- color: #5a6a90; /* was #4b5680 */
-}
+.settings-chevron { font-size: 9px; transition: transform 0.2s; color: var(--muted-dark); }
-.settings-card-body {
- margin-top: 12px;
-}
+.settings-card-body { margin-top: 12px; }
.settings-sublabel {
font-family: 'Space Mono', monospace;
@@ -925,139 +777,87 @@ input::placeholder { color: #3a4560; }
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
- color: #7a8ab0;
+ color: var(--muted);
margin-bottom: 7px;
margin-top: 2px;
}
-.settings-toggle-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
-}
+.settings-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+.settings-toggle-info { display: flex; flex-direction: column; gap: 2px; }
+.settings-toggle-label { font-size: 13px; color: #c8d4f0; font-weight: 500; }
+.settings-toggle-sub { font-family: 'Space Mono', monospace; font-size: 9px; color: var(--muted); }
-.settings-toggle-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
+.radius-value { font-family: 'Space Mono', monospace; font-size: 13px; color: var(--blue); font-weight: 700; }
-.settings-toggle-label {
- font-family: 'Outfit', sans-serif;
- font-size: 13px;
- color: #c8d4f0;
- font-weight: 500;
-}
-
-.settings-toggle-sub {
- font-family: 'Space Mono', monospace;
- font-size: 9px;
- color: #7a8ab0; /* was #4b5680 */
-}
-
-.radius-value {
- font-family: 'Space Mono', monospace;
- font-size: 13px;
- color: #60a5fa;
- font-weight: 700;
-}
-
-.location-row {
- display: flex;
- gap: 8px;
- align-items: center;
- margin-bottom: 14px;
-}
+.location-row { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; }
.coord-display {
flex: 1;
- background: #0f1322;
- border: 1px solid #1e2840;
+ background: var(--surface);
+ border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
font-family: 'Space Mono', monospace;
font-size: 11px;
- color: #60a5fa;
+ color: var(--blue);
}
+.coord-display.empty { color: var(--muted-dark); }
-.coord-display.empty { color: #5a6a90; /* was #3a4560 */ }
-
-.btn-location {
- background: #1a2a4a;
- color: #60a5fa;
- padding: 8px 12px;
- font-size: 12px;
- white-space: nowrap;
-}
+.btn-location { background: #1a2a4a; color: var(--blue); padding: 8px 12px; font-size: 12px; white-space: nowrap; }
.btn-location:hover { background: #1e3255; }
-/* ── Saved toast ── */
-.saved-toast {
- position: fixed;
- bottom: 12px;
- left: 50%;
- transform: translateX(-50%) translateY(8px);
- background: #14532d;
- border: 1px solid #22c55e44;
- color: #4ade80;
- font-size: 11px;
- font-family: 'Space Mono', monospace;
- padding: 6px 14px;
- border-radius: 20px;
- opacity: 0;
- transition: opacity 0.2s, transform 0.2s;
- pointer-events: none;
- z-index: 200;
- white-space: nowrap;
-}
-.saved-toast.visible {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
+/* ─── NOTIFICATIONS ────────────────────────────────────────────────────────── */
+.notif-builder {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px;
+ margin-bottom: 12px;
}
-/* ── Attribution footer ── */
-.attribution-footer {
- border-top: 1px solid #1a2040;
- padding: 7px 18px;
- background: #08090f;
+.notif-os-preview {
display: flex;
- align-items: center;
- justify-content: space-between;
- flex-shrink: 0;
-}
-
-.attribution-logo {
- height: 20px;
- opacity: 1;
- transition: opacity 0.2s;
- display: block;
+ align-items: flex-start;
+ gap: 10px;
+ background: #1c2033;
+ border: 1px solid #2a3a5a;
+ border-radius: 10px;
+ padding: 10px 12px;
+ margin-bottom: 12px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
+.notif-os-icon { width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; margin-top: 1px; }
+.notif-os-body { flex: 1; min-width: 0; }
-.attribution-brand:hover .attribution-logo {
- opacity: 0.75;
+.notif-os-preview .notif-preview-title {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--text);
+ margin-bottom: 3px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-
-.attribution-coverage {
- font-family: 'Outfit', sans-serif;
- font-size: 11px;
- font-weight: 600;
- color: #60a5fa;
- text-decoration: none;
- opacity: 0.75;
- transition: opacity 0.2s;
+.notif-os-preview .notif-preview-body {
+ font-family: 'Space Mono', monospace;
+ font-size: 10px;
+ color: var(--subtext);
+ line-height: 1.5;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.attribution-coverage:hover {
- opacity: 1;
- text-decoration: underline;
-}
+.notif-toggle-list { display: flex; flex-direction: column; gap: 7px; }
+.notif-toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 0 2px; }
+.notif-toggle-label { font-size: 12px; color: var(--subtext); }
+.notif-toggle-row .alert-toggle { flex-shrink: 0; }
-/* ── Donate footer ── */
+/* ─── FOOTERS ──────────────────────────────────────────────────────────────── */
.donate-footer {
- border-top: 1px solid #1a2040;
+ border-top: 1px solid var(--border);
padding: 10px 18px;
- background: #08090f;
+ background: var(--bg);
display: flex;
justify-content: center;
flex-shrink: 0;
@@ -1067,147 +867,76 @@ input::placeholder { color: #3a4560; }
display: inline-flex;
align-items: center;
gap: 8px;
- background: #0f1322;
+ background: var(--surface);
border: 1px solid #2a3a6a;
- color: #8b9cc8;
+ color: var(--subtext);
text-decoration: none;
padding: 8px 18px;
border-radius: 20px;
- font-family: 'Outfit', sans-serif;
font-size: 12px;
font-weight: 600;
transition: all 0.2s;
width: 100%;
justify-content: center;
}
-
-.kofi-btn:hover {
- background: #1a2a50;
- border-color: #60a5fa;
- color: #e8e4f0;
-}
-
+.kofi-btn:hover { background: #1a2a50; border-color: var(--blue); color: var(--text); }
.kofi-icon { font-size: 14px; }
-
.kofi-text { flex: 1; text-align: center; }
-
.kofi-badge {
font-family: 'Space Mono', monospace;
font-size: 9px;
font-weight: 700;
background: #1e3a6a;
- color: #60a5fa;
+ color: var(--blue);
padding: 2px 7px;
border-radius: 10px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
-
-/* ── Alert note ── */
-.alert-note-input {
- width: 100%;
- margin-bottom: 8px;
-}
-
-.alert-note {
- font-size: 10px;
- color: #7a8ab0; /* was #4b5680 */
- margin-top: 3px;
- cursor: pointer;
- min-height: 14px;
- transition: color 0.15s;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.alert-note:hover { color: #8b9cc8; }
-
-.alert-note-empty {
- color: #4b5680; /* was #2a3060 */
- font-style: italic;
-}
-
-.alert-note:hover .alert-note-empty { color: #7a8ab0; /* was #4b5680 */ }
-
-.alert-note-field {
- width: 100%;
- font-size: 10px;
- padding: 2px 5px;
- background: #080a12;
- border: 1px solid #60a5fa;
- border-radius: 4px;
- color: #e8e4f0;
- font-family: 'Outfit', sans-serif;
- outline: none;
-}
-
-/* ── Scrollbar ── */
-::-webkit-scrollbar { width: 4px; }
-::-webkit-scrollbar-track { background: #0a0c14; }
-::-webkit-scrollbar-thumb { background: #1e2840; border-radius: 2px; }
-
-/* ── Detail dropdown: add as alert ── */
-.detail-add-alert {
- margin-bottom: 6px;
- padding: 8px 10px;
- background: #080a12;
- border: 1px solid #1e2840;
- border-radius: 7px;
-}
-
-.detail-add-alert-label {
- font-family: 'Space Mono', monospace;
- font-size: 9px;
- text-transform: uppercase;
- letter-spacing: 0.8px;
- color: #5a6a90;
- margin-bottom: 7px;
+.attribution-footer {
+ border-top: 1px solid var(--border);
+ padding: 7px 18px;
+ background: var(--bg);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-shrink: 0;
}
-
-.detail-add-alert-note {
- width: 100%;
+.attribution-logo { height: 20px; opacity: 1; transition: opacity 0.2s; display: block; }
+.attribution-brand:hover .attribution-logo { opacity: 0.75; }
+.attribution-coverage {
font-size: 11px;
- font-family: 'Outfit', sans-serif;
- padding: 5px 8px;
- background: #0f1322;
- border: 1px solid #1e2840;
- border-radius: 6px;
- color: #e8e4f0;
- outline: none;
- margin-bottom: 7px;
- transition: border-color 0.15s;
-}
-.detail-add-alert-note::placeholder { color: #3a4560; }
-.detail-add-alert-note:focus { border-color: #2a4a8a; }
-
-.detail-add-alert-btns {
- display: flex;
- gap: 5px;
+ font-weight: 600;
+ color: var(--blue);
+ text-decoration: none;
+ opacity: 0.75;
+ transition: opacity 0.2s;
}
+.attribution-coverage:hover { opacity: 1; text-decoration: underline; }
-.detail-add-alert-btn {
- flex: 1;
- padding: 6px 4px;
+/* ─── TOAST ────────────────────────────────────────────────────────────────── */
+.saved-toast {
+ position: fixed;
+ bottom: 12px;
+ left: 50%;
+ transform: translateX(-50%) translateY(8px);
+ background: #14532d;
+ border: 1px solid #22c55e44;
+ color: #4ade80;
font-size: 11px;
- font-family: 'Outfit', sans-serif;
- font-weight: 600;
- background: #1a2a4a;
- border: 1px solid #2a3a6a;
- color: #60a5fa;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.15s;
- text-align: center;
+ font-family: 'Space Mono', monospace;
+ padding: 6px 14px;
+ border-radius: 20px;
+ opacity: 0;
+ transition: opacity 0.2s, transform 0.2s;
+ pointer-events: none;
+ z-index: 200;
white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
}
-.detail-add-alert-btn:hover:not(:disabled) { background: #1e3255; }
-.detail-add-alert-btn:disabled {
- background: #0f1a2a;
- border-color: #1a2a3a;
- color: #22c55e;
- cursor: default;
-}
\ No newline at end of file
+.saved-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
+
+/* ─── SCROLLBAR ────────────────────────────────────────────────────────────── */
+::-webkit-scrollbar { width: 4px; }
+::-webkit-scrollbar-track { background: #0a0c14; }
+::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
\ No newline at end of file
diff --git a/popup/popup.html b/popup/popup.html
index d087db3..bd2e7d8 100644
--- a/popup/popup.html
+++ b/popup/popup.html
@@ -3,8 +3,6 @@
Plane Alert
-
-
@@ -69,7 +67,9 @@
Matching only Hide aircraft that don't match any active alert
Airborne only Temporarily hides ground traffic in the Live tab only. Does not affect notifications.
Min. altitude Only show aircraft above a set altitude
- Detail dropdown Click any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading. Use 🎯 Catch to mark it as caught — it won't trigger notifications again. Click again to collapse.
+ Detail dropdown Click any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading. Click again to collapse.
+ 🔔 / 🔕 buttons Each detail dropdown shows a bell button next to the registration and aircraft type. 🔔 means an alert is active for that value — click to remove it. 🔕 means no alert exists — click to add one instantly without going to the Alerts tab.
+ 🎯 Catch Mark an aircraft as caught from the detail dropdown. It won't trigger notifications again until you release it.
@@ -84,12 +84,12 @@
Radius Choose 25, 50 or 100 km (or nm in imperial mode), or set a custom value with the slider
Hide ground traffic Global setting — suppresses both notifications and Live tab entries for aircraft on the ground
-
Units Switch between metric (km · m · km/h) and imperial (nm · ft · kts). Affects the Live tab, detail panel, and notifications.
+
Units Switch between metric (km · m · km/h) and imperial (nm · ft · kts). Affects the Live tab, detail dropdown, and notifications.
Notifications Toggle desktop notifications on or off. Polling continues either way.
Sound Choose an alert sound and volume. A preview plays when you select a sound.
Content Choose which details appear in each notification: aircraft type, altitude, speed, route and/or direction
Startup tab Choose which tab opens when you launch the extension. Set it to a fixed tab or to Last used to always return to where you left off.
-
Backup Export all settings (alerts, location, radius, notification preferences) to a JSON file. Import restores everything at once. Old alerts-only backups are still supported.
+
Backup Export all settings (alerts, location, radius, notification preferences) to a JSON file. Import restores everything at once. Old alerts-only backups are still supported. Note: notification history is not included in exports.
diff --git a/popup/popup.js b/popup/popup.js
index 86ea3bf..2a96783 100644
--- a/popup/popup.js
+++ b/popup/popup.js
@@ -1,4 +1,16 @@
-// popup.js — init, tab switching, help overlay, master toggle, status
+// popup.js v1.1.0 — init, tab switching, help overlay, master toggle, status
+
+// ─── TOAST (gedeeld door alle popup-scripts) ───────────────────────────────
+
+let toastTimer = null;
+function showSaved(label = 'Saved') {
+ const toast = document.getElementById('savedToast');
+ if (!toast) return;
+ toast.textContent = `✓ ${label}`;
+ toast.classList.add('visible');
+ clearTimeout(toastTimer);
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 1800);
+}
// ─── TAB SWITCHING ─────────────────────────────────────────────────────────
diff --git a/popup/settings.js b/popup/settings.js
index 716126a..6e8f5d8 100644
--- a/popup/settings.js
+++ b/popup/settings.js
@@ -1,4 +1,5 @@
-// settings.js — injecteert Settings tab HTML en beheert alle instellingen
+// settings.js v1.2.0 — injecteert Settings tab HTML en beheert alle instellingen
+// showSaved() is beschikbaar via popup.js
// ─── HTML INJECTIE ──────────────────────────────────────────────────────────
@@ -34,6 +35,21 @@ function initSettingsTab() {
+
+
+
+ 📐 Units
+ ▼
+
+
+
+ 📏 Metric
+ ✈️ Imperial
+
+
Metric: km · m · km/h · Imperial: nm · ft · kts
+
+
+
@@ -51,21 +67,6 @@ function initSettingsTab() {
-
-
-
- 📐 Units
- ▼
-
-
-
- 📏 Metric
- ✈️ Imperial
-
-
Metric: km · m · km/h · Imperial: nm · ft · kts
-
-
-
@@ -73,32 +74,22 @@ function initSettingsTab() {
▼
-
+
Desktop notifications
Polling continues when off
-
Sound
-
- 🔕 Off
- 🔔 Ping
- 📡 Radar
- 🚨 Alert
- 🎵 Chime
-
-
- 🔈 Soft
- 🔉 Medium
- 🔊 Loud
- ▶ Test
-
-
Content
+
+
Content
-
-
✈️ PH-BXA (B744) spotted!
-
8500m · 850 km/h · AMS→JFK · from the west
+
+
+
+
✈️ PH-BXA (B744) spotted!
+
8500m · 850 km/h · AMS→JFK · from the west
+
@@ -127,6 +118,31 @@ function initSettingsTab() {
+
+
🔔 Send test notification
+
+
+
+
+
+
+ 🔊 Sound
+ ▼
+
+
+
+ 🔕 Off
+ 🔔 Ping
+ 📡 Radar
+ 🚨 Alert
+ 🎵 Chime
+
+
+ 🔈 Soft
+ 🔉 Medium
+ 🔊 Loud
+ ▶ Test
+
@@ -148,37 +164,25 @@ function initSettingsTab() {
-
+
- 💾 Backup & Test
+ 💾 Backup
▼
Exports all settings including alerts, location and notifications.
-
+
⬆️ Export backup
⬇️ Import backup
-
🔔 Send test notification
`;
setupSettingsEvents();
}
-// ─── SAVED TOAST ───────────────────────────────────────────────────────────
-
-let toastTimer = null;
-function showSaved(label = 'Saved') {
- const toast = document.getElementById('savedToast');
- toast.textContent = `✓ ${label}`;
- toast.classList.add('visible');
- clearTimeout(toastTimer);
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 1800);
-}
-
// ─── LOCATIE ───────────────────────────────────────────────────────────────
function updateCoordDisplay(lat, lon) {
@@ -327,7 +331,6 @@ function setupSettingsEvents() {
// ─── INSTELLINGEN HERLADEN (na backup import) ──────────────────────────────
async function loadSettings() {
- // Onthoud welke kaarten open waren voor we de HTML herinjekten
const openCards = [];
document.querySelectorAll('.settings-card-body').forEach(body => {
if (body.style.display !== 'none') openCards.push(body.id);
@@ -341,7 +344,6 @@ async function loadSettings() {
initRadiusButtons(radius, units);
await initSettings();
- // Herstel open kaarten
openCards.forEach(id => {
const body = document.getElementById(id);
if (body) {
@@ -517,7 +519,8 @@ async function initSettings() {
'hideGround', 'notificationsEnabled', 'notifShow',
'alertSound', 'alertVolume',
'startupTab', 'lastTab',
- 'caughtAircraft', 'caughtAircraftLabels'
+ 'caughtAircraft', 'caughtAircraftLabels',
+ 'statsTotalCount', 'statsFirstDetection', 'statsTypeCounts', 'statsAirlineCounts'
];
document.getElementById('btnExportAlerts').addEventListener('click', async () => {
diff --git a/uninstall.html b/uninstall.html
index 079884e..07a79e8 100644
--- a/uninstall.html
+++ b/uninstall.html
@@ -4,9 +4,42 @@
Plane Alert — Goodbye!
-
-