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. -![Version](https://img.shields.io/badge/version-1.4.0-blue) +![Version](https://img.shields.io/badge/version-1.5.0-blue) ![Manifest](https://img.shields.io/badge/manifest-v3-green) ![License](https://img.shields.io/badge/license-GPL%20v3-lightgrey) @@ -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..5d12427 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); diff --git a/popup/alerts.js b/popup/alerts.js index 40413d0..a478543 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 ────────────────────────────────────────────────────────── @@ -108,6 +108,7 @@ function setupAlertsEvents() { document.getElementById('alertValue').value = ''; document.getElementById('alertNote').value = ''; renderAlerts(alerts); + showSaved('Alert added'); }); } @@ -145,7 +146,7 @@ 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(); diff --git a/popup/history.js b/popup/history.js index d3ae6ef..516e1aa 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.0 — injecteert History tab HTML en beheert notificatiegeschiedenis // ─── HTML INJECTIE ────────────────────────────────────────────────────────── function initHistoryTab() { document.getElementById('tab-history').innerHTML = ` + + +
+ + +
+ +
Notification history
@@ -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 = ` +
+
+
Total notifications
+
${statsTotalCount}
+
+
+
First detection
+
${firstDate}
+
+
+ + ${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() { diff --git a/popup/live.js b/popup/live.js index abe68af..f1d391a 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.0 — 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; } @@ -397,7 +400,7 @@ async function renderAircraftList() { 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 +424,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 +437,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 +473,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); @@ -488,27 +494,23 @@ async function buildDropdownContent(dropdown, ac, units, caught) { 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..1cfaee3 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -1,9 +1,27 @@ -* { box-sizing: border-box; margin: 0; padding: 0; } +/* ─── 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 +30,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 +43,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 +65,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 +78,6 @@ body { transition: background 0.25s, border-color 0.25s; flex-shrink: 0; } - .toggle-pill::after { content: ''; position: absolute; @@ -74,19 +89,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 +102,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 +129,7 @@ body { } .btn-help:hover { background: #1e3255; } -/* ── Help overlay ── */ +/* ─── HELP OVERLAY ─────────────────────────────────────────────────────────── */ .help-overlay { display: none; position: fixed; @@ -141,8 +143,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 +158,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 +189,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 +205,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 +224,10 @@ body { color: #7dd3fc; } -/* ── Tabs ── */ +/* ─── TABS ─────────────────────────────────────────────────────────────────── */ .tabs { display: flex; - border-bottom: 1px solid #1a2040; + border-bottom: 1px solid var(--border); background: #0a0c14; } @@ -252,11 +242,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 +259,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 +287,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 +346,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 +383,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 +466,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; -} +/* ── Live controls ── */ +.live-controls { margin-bottom: 10px; display: flex; flex-direction: column; gap: 6px; } -.control-row { - display: flex; - gap: 6px; - align-items: center; -} +.control-row { display: flex; gap: 6px; align-items: center; } +.control-row select { flex: 0 0 auto; font-size: 11px; padding: 5px 8px; } -.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 +582,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 +722,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; + color: var(--muted); } +.settings-card-toggle:hover { color: #a0b0d0; } -.settings-card-toggle:hover { - color: #a0b0d0; -} +.settings-chevron { font-size: 9px; transition: transform 0.2s; color: var(--muted-dark); } -.settings-chevron { - font-size: 9px; - transition: transform 0.2s; - color: #5a6a90; /* was #4b5680 */ -} - -.settings-card-body { - margin-top: 12px; -} +.settings-card-body { margin-top: 12px; } .settings-sublabel { font-family: 'Space Mono', monospace; @@ -925,139 +740,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-info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.settings-toggle-label { - font-family: 'Outfit', sans-serif; - font-size: 13px; - color: #c8d4f0; - font-weight: 500; -} +.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-sub { - font-family: 'Space Mono', monospace; - font-size: 9px; - color: #7a8ab0; /* was #4b5680 */ -} +.radius-value { font-family: 'Space Mono', monospace; font-size: 13px; color: var(--blue); font-weight: 700; } -.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 +830,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..1903aff 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -69,7 +69,9 @@
Matching onlyHide aircraft that don't match any active alert
Airborne onlyTemporarily hides ground traffic in the Live tab only. Does not affect notifications.
Min. altitudeOnly show aircraft above a set altitude
-
Detail dropdownClick 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 dropdownClick any aircraft to expand its details inline: registration, type, altitude, speed, route, squawk and heading. Click again to collapse.
+
🔔 / 🔕 buttonsEach 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.
+
🎯 CatchMark an aircraft as caught from the detail dropdown. It won't trigger notifications again until you release it.
@@ -84,12 +86,12 @@
RadiusChoose 25, 50 or 100 km (or nm in imperial mode), or set a custom value with the slider
Hide ground trafficGlobal setting — suppresses both notifications and Live tab entries for aircraft on the ground
-
UnitsSwitch between metric (km · m · km/h) and imperial (nm · ft · kts). Affects the Live tab, detail panel, and notifications.
+
UnitsSwitch between metric (km · m · km/h) and imperial (nm · ft · kts). Affects the Live tab, detail dropdown, and notifications.
NotificationsToggle desktop notifications on or off. Polling continues either way.
SoundChoose an alert sound and volume. A preview plays when you select a sound.
ContentChoose which details appear in each notification: aircraft type, altitude, speed, route and/or direction
Startup tabChoose 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.
-
BackupExport all settings (alerts, location, radius, notification preferences) to a JSON file. Import restores everything at once. Old alerts-only backups are still supported.
+
BackupExport 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() { + +
+ + +
+
- -
- - -
-
- +
`; 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 () => {