mirror of
https://github.com/smittix/intercept.git
synced 2026-04-29 00:59:59 -07:00
Adding Spystations page and 2 small fixed for the vessel page
This commit is contained in:
@@ -208,6 +208,38 @@ body {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
/* Signal quality states */
|
||||
.strip-stat.signal-stat .strip-value {
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.strip-stat.signal-stat.good {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border-color: rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.strip-stat.signal-stat.good .strip-value {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.strip-stat.signal-stat.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.strip-stat.signal-stat.warning .strip-value {
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.strip-stat.signal-stat.poor {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.strip-stat.signal-stat.poor .strip-value {
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.strip-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.spy-stations-header {
|
||||
@@ -54,9 +55,8 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -66,8 +66,9 @@
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spy-station-card:hover {
|
||||
@@ -144,6 +145,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.spy-station-meta {
|
||||
@@ -211,10 +213,6 @@
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Card Footer */
|
||||
@@ -225,6 +223,49 @@
|
||||
padding: 10px 14px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* Frequency Selector Group */
|
||||
.spy-tune-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.spy-freq-select {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
padding: 6px 8px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary);
|
||||
min-width: 120px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spy-freq-select:hover {
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.spy-freq-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Clickable frequency items in details modal */
|
||||
.spy-freq-clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.spy-freq-clickable:hover {
|
||||
background: var(--accent-cyan);
|
||||
color: #000;
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Tune Button */
|
||||
@@ -295,6 +336,13 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MODE VISIBILITY - Ensure sidebar shows when active
|
||||
============================================ */
|
||||
#spystationsMode.active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FILTER CHECKBOX STYLING
|
||||
============================================ */
|
||||
@@ -321,6 +369,22 @@
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
/* Large desktop (1200px+) */
|
||||
@media (min-width: 1200px) {
|
||||
.spy-stations-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop/Tablet landscape (1024px) */
|
||||
@media (max-width: 1024px) {
|
||||
.spy-stations-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet portrait (768px) */
|
||||
@media (max-width: 768px) {
|
||||
.spy-stations-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -341,7 +405,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Small tablet / large phone (640px) */
|
||||
@media (max-width: 640px) {
|
||||
.spy-station-footer {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -351,5 +416,51 @@
|
||||
.spy-details-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.spy-tune-group {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spy-freq-select {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile (480px) */
|
||||
@media (max-width: 480px) {
|
||||
.spy-stations-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.spy-station-body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.spy-stations-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.spy-station-desc {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch device compliance */
|
||||
@media (pointer: coarse) {
|
||||
.spy-tune-btn,
|
||||
.spy-details-btn,
|
||||
.spy-freq-select {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.spy-freq-clickable {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,14 @@ const SpyStations = (function() {
|
||||
'EG': '\u{1F1EA}\u{1F1EC}',
|
||||
'KP': '\u{1F1F0}\u{1F1F5}',
|
||||
'TN': '\u{1F1F9}\u{1F1F3}',
|
||||
'US': '\u{1F1FA}\u{1F1F8}'
|
||||
'US': '\u{1F1FA}\u{1F1F8}',
|
||||
'PL': '\u{1F1F5}\u{1F1F1}',
|
||||
'IL': '\u{1F1EE}\u{1F1F1}',
|
||||
'CN': '\u{1F1E8}\u{1F1F3}',
|
||||
'MA': '\u{1F1F2}\u{1F1E6}',
|
||||
'FR': '\u{1F1EB}\u{1F1F7}',
|
||||
'RO': '\u{1F1F7}\u{1F1F4}',
|
||||
'DZ': '\u{1F1E9}\u{1F1FF}'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -121,6 +128,7 @@ const SpyStations = (function() {
|
||||
});
|
||||
|
||||
renderStations();
|
||||
updateStats(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +169,39 @@ const SpyStations = (function() {
|
||||
const freqList = station.frequencies.slice(0, 4).map(f => formatFrequency(f.freq_khz)).join(', ');
|
||||
const moreFreqs = station.frequencies.length > 4 ? ` +${station.frequencies.length - 4} more` : '';
|
||||
|
||||
// Build tune button with frequency selector if multiple frequencies
|
||||
let tuneSection;
|
||||
if (station.frequencies.length > 1) {
|
||||
const options = station.frequencies.map(f => {
|
||||
const label = formatFrequency(f.freq_khz) + (f.primary ? ' (primary)' : '');
|
||||
return `<option value="${f.freq_khz}">${label}</option>`;
|
||||
}).join('');
|
||||
tuneSection = `
|
||||
<div class="spy-tune-group">
|
||||
<select class="spy-freq-select" id="freq-select-${station.id}">
|
||||
${options}
|
||||
</select>
|
||||
<button class="spy-tune-btn" onclick="SpyStations.tuneToSelectedFreq('${station.id}')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
|
||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
||||
</svg>
|
||||
Tune In
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
tuneSection = `
|
||||
<button class="spy-tune-btn" onclick="SpyStations.tuneToStation('${station.id}', ${primaryFreq.freq_khz})">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
|
||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
||||
</svg>
|
||||
Tune In
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="spy-station-card" data-station-id="${station.id}">
|
||||
<div class="spy-station-header">
|
||||
@@ -189,13 +230,7 @@ const SpyStations = (function() {
|
||||
<div class="spy-station-desc">${station.description}</div>
|
||||
</div>
|
||||
<div class="spy-station-footer">
|
||||
<button class="spy-tune-btn" onclick="SpyStations.tuneToStation('${station.id}', ${primaryFreq.freq_khz})">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
|
||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
||||
</svg>
|
||||
Tune In
|
||||
</button>
|
||||
${tuneSection}
|
||||
<button class="spy-details-btn" onclick="SpyStations.showDetails('${station.id}')">
|
||||
Details
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;">
|
||||
@@ -217,20 +252,34 @@ const SpyStations = (function() {
|
||||
return freqKhz + ' kHz';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate SDR mode from station mode string
|
||||
*/
|
||||
function getModeFromStation(stationMode) {
|
||||
const mode = stationMode.toLowerCase();
|
||||
if (mode.includes('am') || mode.includes('ofdm')) return 'am';
|
||||
if (mode.includes('lsb')) return 'lsb';
|
||||
if (mode.includes('fm')) return 'fm';
|
||||
// Default to USB for most number stations and digital modes
|
||||
return 'usb';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tune to a station frequency
|
||||
*/
|
||||
function tuneToStation(stationId, freqKhz) {
|
||||
const freqMhz = freqKhz / 1000;
|
||||
sessionStorage.setItem('tuneFrequency', freqMhz.toString());
|
||||
sessionStorage.setItem('tuneMode', 'usb'); // Most number stations use USB
|
||||
|
||||
// Find the station for notification
|
||||
// Find the station and determine mode
|
||||
const station = stations.find(s => s.id === stationId);
|
||||
const tuneMode = station ? getModeFromStation(station.mode) : 'usb';
|
||||
sessionStorage.setItem('tuneMode', tuneMode);
|
||||
|
||||
const stationName = station ? station.name : 'Station';
|
||||
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Tuning to ' + stationName, formatFrequency(freqKhz));
|
||||
showNotification('Tuning to ' + stationName, formatFrequency(freqKhz) + ' (' + tuneMode.toUpperCase() + ')');
|
||||
}
|
||||
|
||||
// Switch to listening post mode
|
||||
@@ -241,6 +290,17 @@ const SpyStations = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tune to selected frequency from dropdown
|
||||
*/
|
||||
function tuneToSelectedFreq(stationId) {
|
||||
const select = document.getElementById('freq-select-' + stationId);
|
||||
if (select) {
|
||||
const freqKhz = parseInt(select.value, 10);
|
||||
tuneToStation(stationId, freqKhz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we arrived from another page with a tune request
|
||||
*/
|
||||
@@ -266,7 +326,7 @@ const SpyStations = (function() {
|
||||
const flag = countryFlags[station.country_code] || '';
|
||||
const allFreqs = station.frequencies.map(f => {
|
||||
const label = f.primary ? ' (primary)' : '';
|
||||
return `<span class="spy-freq-item">${formatFrequency(f.freq_khz)}${label}</span>`;
|
||||
return `<span class="spy-freq-item spy-freq-clickable" onclick="SpyStations.tuneToStation('${station.id}', ${f.freq_khz}); SpyStations.closeDetails();">${formatFrequency(f.freq_khz)}${label}</span>`;
|
||||
}).join('');
|
||||
|
||||
modal.innerHTML = `
|
||||
@@ -425,12 +485,14 @@ const SpyStations = (function() {
|
||||
|
||||
/**
|
||||
* Update sidebar stats
|
||||
* @param {boolean} useFiltered - If true, use filtered stations instead of all stations
|
||||
*/
|
||||
function updateStats() {
|
||||
const numberCount = stations.filter(s => s.type === 'number').length;
|
||||
const diplomaticCount = stations.filter(s => s.type === 'diplomatic').length;
|
||||
const countryCount = new Set(stations.map(s => s.country_code)).size;
|
||||
const freqCount = stations.reduce((sum, s) => sum + s.frequencies.length, 0);
|
||||
function updateStats(useFiltered) {
|
||||
const stationList = useFiltered ? filteredStations : stations;
|
||||
const numberCount = stationList.filter(s => s.type === 'number').length;
|
||||
const diplomaticCount = stationList.filter(s => s.type === 'diplomatic').length;
|
||||
const countryCount = new Set(stationList.map(s => s.country_code)).size;
|
||||
const freqCount = stationList.reduce((sum, s) => sum + s.frequencies.length, 0);
|
||||
|
||||
const numberEl = document.getElementById('spyStatsNumber');
|
||||
const diplomaticEl = document.getElementById('spyStatsDiplomatic');
|
||||
@@ -441,6 +503,12 @@ const SpyStations = (function() {
|
||||
if (diplomaticEl) diplomaticEl.textContent = diplomaticCount;
|
||||
if (countriesEl) countriesEl.textContent = countryCount;
|
||||
if (freqsEl) freqsEl.textContent = freqCount;
|
||||
|
||||
// Update visible count in header if element exists
|
||||
const visibleCountEl = document.getElementById('spyStationsVisibleCount');
|
||||
if (visibleCountEl) {
|
||||
visibleCountEl.textContent = stationList.length + ' stations';
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
@@ -448,6 +516,7 @@ const SpyStations = (function() {
|
||||
init,
|
||||
applyFilters,
|
||||
tuneToStation,
|
||||
tuneToSelectedFreq,
|
||||
showDetails,
|
||||
closeDetails,
|
||||
showHelp,
|
||||
|
||||
Reference in New Issue
Block a user