sound
This commit is contained in:
8
Makefile
8
Makefile
@@ -25,13 +25,13 @@ help:
|
|||||||
|
|
||||||
## install-deps: Installe les dépendances npm
|
## install-deps: Installe les dépendances npm
|
||||||
install-deps:
|
install-deps:
|
||||||
@echo "[1/2] Installation des dépendances npm..."
|
@echo "[1/2] Installation des dependances npm..."
|
||||||
cd $(FRONTEND_DIR) && npm install
|
cd $(FRONTEND_DIR) && npm install
|
||||||
@echo "Dépendances installées"
|
@echo "Dependances installees"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "[2/2] Vérification de Go..."
|
@echo "[2/2] Verification de Go..."
|
||||||
@go version
|
@go version
|
||||||
@echo "Go est installé"
|
@echo "Go est installe"
|
||||||
|
|
||||||
## frontend: Build le frontend Svelte
|
## frontend: Build le frontend Svelte
|
||||||
frontend:
|
frontend:
|
||||||
|
|||||||
BIN
flex.sqlite-journal
Normal file
BIN
flex.sqlite-journal
Normal file
Binary file not shown.
@@ -7,9 +7,11 @@
|
|||||||
import Sidebar from './components/Sidebar.svelte';
|
import Sidebar from './components/Sidebar.svelte';
|
||||||
import Toast from './components/Toast.svelte';
|
import Toast from './components/Toast.svelte';
|
||||||
import ErrorBanner from './components/ErrorBanner.svelte';
|
import ErrorBanner from './components/ErrorBanner.svelte';
|
||||||
|
import SoundManager from './components/SoundManager.svelte';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let spots = [];
|
let spots = [];
|
||||||
|
let previousSpots = []; // Pour détecter les nouveaux spots
|
||||||
let filteredSpots = [];
|
let filteredSpots = [];
|
||||||
let stats = {
|
let stats = {
|
||||||
totalSpots: 0,
|
totalSpots: 0,
|
||||||
@@ -23,13 +25,14 @@
|
|||||||
filters: { skimmer: false, ft8: false, ft4: false }
|
filters: { skimmer: false, ft8: false, ft4: false }
|
||||||
};
|
};
|
||||||
let topSpotters = [];
|
let topSpotters = [];
|
||||||
let watchlist = [];
|
let watchlist = []; // ✅ Initialisé vide, sera rempli par WebSocket
|
||||||
let recentQSOs = [];
|
let recentQSOs = [];
|
||||||
let logStats = { today: 0, thisWeek: 0, thisMonth: 0, total: 0 };
|
let logStats = { today: 0, thisWeek: 0, thisMonth: 0, total: 0 };
|
||||||
let dxccProgress = { worked: 0, total: 340, percentage: 0 };
|
let dxccProgress = { worked: 0, total: 340, percentage: 0 };
|
||||||
let solarData = { sfi: 'N/A', sunspots: 'N/A', aIndex: 'N/A', kIndex: 'N/A' };
|
let solarData = { sfi: 'N/A', sunspots: 'N/A', aIndex: 'N/A', kIndex: 'N/A' };
|
||||||
|
|
||||||
let activeTab = 'stats';
|
let activeTab = 'stats';
|
||||||
|
let showOnlyActive = false; // ✅ État global pour persister entre les onglets
|
||||||
let wsStatus = 'disconnected';
|
let wsStatus = 'disconnected';
|
||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
let toastMessage = '';
|
let toastMessage = '';
|
||||||
@@ -58,13 +61,15 @@
|
|||||||
band12M: false,
|
band12M: false,
|
||||||
band10M: false,
|
band10M: false,
|
||||||
band6M: false
|
band6M: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// WebSocket
|
// WebSocket
|
||||||
let ws;
|
let ws;
|
||||||
let reconnectTimer;
|
let reconnectTimer;
|
||||||
let reconnectAttempts = 0;
|
let reconnectAttempts = 0;
|
||||||
let maxReconnectAttempts = 10;
|
let maxReconnectAttempts = 10;
|
||||||
|
let soundEnabled = true;
|
||||||
|
let isShuttingDown = false; // ✅ Flag pour éviter les erreurs pendant le shutdown
|
||||||
|
|
||||||
// Reactive filtered spots
|
// Reactive filtered spots
|
||||||
$: {
|
$: {
|
||||||
@@ -75,6 +80,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Détecter les nouveaux spots et jouer les sons appropriés
|
||||||
|
$: if (spots.length > 0 && soundEnabled) {
|
||||||
|
checkForNewSpots(spots, previousSpots, watchlist);
|
||||||
|
previousSpots = [...spots];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ SUPPRIMÉ - La watchlist est gérée côté serveur via WebSocket
|
||||||
|
// Les fonctions addToWatchlist et removeFromWatchlist ne sont plus nécessaires
|
||||||
|
|
||||||
|
function checkForNewSpots(currentSpots, prevSpots, wl) {
|
||||||
|
// Ne pas jouer de sons au chargement initial
|
||||||
|
if (prevSpots.length === 0) return;
|
||||||
|
|
||||||
|
// Créer un Set des IDs précédents pour une recherche rapide
|
||||||
|
const previousIds = new Set(prevSpots.map(s => `${s.DX}-${s.Frequency}-${s.Time}`));
|
||||||
|
|
||||||
|
// Trouver les nouveaux spots
|
||||||
|
const newSpots = currentSpots.filter(spot => {
|
||||||
|
const spotId = `${spot.DX}-${spot.Frequency}-${spot.Time}`;
|
||||||
|
return !previousIds.has(spotId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newSpots.length === 0) return;
|
||||||
|
|
||||||
|
// Vérifier s'il y a un nouveau DXCC (priorité maximale)
|
||||||
|
const hasNewDXCC = newSpots.some(spot => spot.NewDXCC === true);
|
||||||
|
if (hasNewDXCC) {
|
||||||
|
playSound('newDXCC');
|
||||||
|
return; // Ne jouer qu'un seul son
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier s'il y a un spot de la watchlist
|
||||||
|
const hasWatchlistSpot = newSpots.some(spot =>
|
||||||
|
wl.some(pattern => spot.DX === pattern || spot.DX.startsWith(pattern))
|
||||||
|
);
|
||||||
|
if (hasWatchlistSpot) {
|
||||||
|
playSound('watchlist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSound(type) {
|
||||||
|
window.dispatchEvent(new CustomEvent('playSound', {
|
||||||
|
detail: { type }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function applyFilters(allSpots, filters, wl) {
|
function applyFilters(allSpots, filters, wl) {
|
||||||
const bandFiltersActive = filters.band160M || filters.band80M || filters.band60M ||
|
const bandFiltersActive = filters.band160M || filters.band80M || filters.band60M ||
|
||||||
filters.band40M || filters.band30M || filters.band20M || filters.band17M ||
|
filters.band40M || filters.band30M || filters.band20M || filters.band17M ||
|
||||||
@@ -255,6 +306,7 @@
|
|||||||
topSpotters = message.data || [];
|
topSpotters = message.data || [];
|
||||||
break;
|
break;
|
||||||
case 'watchlist':
|
case 'watchlist':
|
||||||
|
// ✅ La watchlist est mise à jour par WebSocket
|
||||||
watchlist = message.data || [];
|
watchlist = message.data || [];
|
||||||
break;
|
break;
|
||||||
case 'log':
|
case 'log':
|
||||||
@@ -344,13 +396,11 @@
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast('FlexDXCluster shutting down...', 'info');
|
showToast('FlexDXCluster shutting down...', 'info');
|
||||||
|
|
||||||
// Fermer le WebSocket et arrêter les tentatives de reconnexion
|
|
||||||
if (ws) ws.close();
|
if (ws) ws.close();
|
||||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||||
wsStatus = 'disconnected';
|
wsStatus = 'disconnected';
|
||||||
maxReconnectAttempts = 0; // Empêcher les reconnexions
|
maxReconnectAttempts = 0;
|
||||||
|
|
||||||
// Afficher la page de shutdown après 1 seconde
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.innerHTML = `
|
document.body.innerHTML = `
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
|
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
|
||||||
@@ -378,10 +428,8 @@
|
|||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
fetchSolarData();
|
fetchSolarData();
|
||||||
|
|
||||||
// Update solar data every 15 minutes
|
|
||||||
const solarInterval = setInterval(fetchSolarData, 15 * 60 * 1000);
|
const solarInterval = setInterval(fetchSolarData, 15 * 60 * 1000);
|
||||||
|
|
||||||
// Listen for sendSpot events from watchlist
|
|
||||||
const handleSendSpot = (e) => {
|
const handleSendSpot = (e) => {
|
||||||
sendCallsign(e.detail.callsign, e.detail.frequency, e.detail.mode);
|
sendCallsign(e.detail.callsign, e.detail.frequency, e.detail.mode);
|
||||||
};
|
};
|
||||||
@@ -397,6 +445,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white min-h-screen p-4">
|
<div class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white min-h-screen p-4">
|
||||||
|
<!-- Gestionnaire de sons -->
|
||||||
|
<SoundManager bind:enabled={soundEnabled} />
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<ErrorBanner message={errorMessage} on:close={() => errorMessage = ''} />
|
<ErrorBanner message={errorMessage} on:close={() => errorMessage = ''} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -424,8 +475,8 @@
|
|||||||
on:toggleFilter={(e) => toggleFilter(e.detail)}
|
on:toggleFilter={(e) => toggleFilter(e.detail)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="grid grid-cols-4 gap-3" style="height: calc(100vh - 360px);">
|
<div class="grid grid-cols-4 gap-3 overflow-hidden" style="height: calc(100vh - 360px); min-height: 500px;">
|
||||||
<div class="col-span-3">
|
<div class="col-span-3 overflow-hidden">
|
||||||
<SpotsTable
|
<SpotsTable
|
||||||
spots={filteredSpots}
|
spots={filteredSpots}
|
||||||
{watchlist}
|
{watchlist}
|
||||||
@@ -434,15 +485,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sidebar
|
<div class="overflow-hidden">
|
||||||
bind:activeTab
|
<Sidebar
|
||||||
{topSpotters}
|
bind:activeTab
|
||||||
{spots}
|
bind:showOnlyActive
|
||||||
{watchlist}
|
{topSpotters}
|
||||||
{recentQSOs}
|
{spots}
|
||||||
{logStats}
|
{watchlist}
|
||||||
{dxccProgress}
|
{recentQSOs}
|
||||||
on:toast={(e) => showToast(e.detail.message, e.detail.type)}
|
{logStats}
|
||||||
/>
|
{dxccProgress}
|
||||||
|
on:toast={(e) => showToast(e.detail.message, e.detail.type)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,11 +11,17 @@
|
|||||||
export let recentQSOs;
|
export let recentQSOs;
|
||||||
export let logStats;
|
export let logStats;
|
||||||
export let dxccProgress;
|
export let dxccProgress;
|
||||||
|
export let showOnlyActive = false; // ✅ Export pour persister l'état
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
// ✅ Propagation des évènements vers le parent
|
||||||
|
function handleToast(event) {
|
||||||
|
dispatch('toast', event.detail);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg border border-slate-700/50 flex flex-col overflow-hidden h-full">
|
<div class="bg-slate-800/50 backdrop-blur rounded-lg border border-slate-700/50 flex flex-col h-full" style="height: 100%; max-height: 100%;">
|
||||||
<!-- Tabs Header -->
|
<!-- Tabs Header -->
|
||||||
<div class="flex border-b border-slate-700/50 bg-slate-900/30 flex-shrink-0">
|
<div class="flex border-b border-slate-700/50 bg-slate-900/30 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
@@ -48,14 +54,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Content -->
|
<!-- Tab Content -->
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden" style="min-height: 0;">
|
||||||
{#if activeTab === 'stats'}
|
{#if activeTab === 'stats'}
|
||||||
<StatsTab {topSpotters} {spots} />
|
<StatsTab {topSpotters} {spots} />
|
||||||
{:else if activeTab === 'watchlist'}
|
{:else if activeTab === 'watchlist'}
|
||||||
<WatchlistTab
|
<WatchlistTab
|
||||||
{watchlist}
|
{watchlist}
|
||||||
{spots}
|
{spots}
|
||||||
on:toast={(e) => dispatch('toast', e.detail)}
|
bind:showOnlyActive
|
||||||
|
on:toast={handleToast}
|
||||||
/>
|
/>
|
||||||
{:else if activeTab === 'log'}
|
{:else if activeTab === 'log'}
|
||||||
<LogTab
|
<LogTab
|
||||||
|
|||||||
158
frontend/src/components/SoundManager.svelte
Normal file
158
frontend/src/components/SoundManager.svelte
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let enabled = true;
|
||||||
|
export let volume = 0.3;
|
||||||
|
export let showControls = false; // Masquer les contrôles par défaut
|
||||||
|
|
||||||
|
let audioContext;
|
||||||
|
let isMuted = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
window.addEventListener('playSound', handlePlaySound);
|
||||||
|
|
||||||
|
// Charger les préférences depuis localStorage
|
||||||
|
const savedMuted = localStorage.getItem('soundMuted');
|
||||||
|
const savedVolume = localStorage.getItem('soundVolume');
|
||||||
|
|
||||||
|
if (savedMuted !== null) isMuted = savedMuted === 'true';
|
||||||
|
if (savedVolume !== null) volume = parseFloat(savedVolume);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('playSound', handlePlaySound);
|
||||||
|
if (audioContext) audioContext.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handlePlaySound(event) {
|
||||||
|
if (!enabled || isMuted) return;
|
||||||
|
|
||||||
|
const { type } = event.detail;
|
||||||
|
|
||||||
|
if (type === 'newDXCC') {
|
||||||
|
playNewDXCCSound();
|
||||||
|
} else if (type === 'watchlist') {
|
||||||
|
playWatchlistSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNewDXCCSound() {
|
||||||
|
if (!audioContext) return;
|
||||||
|
|
||||||
|
const now = audioContext.currentTime;
|
||||||
|
const oscillator = audioContext.createOscillator();
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
|
||||||
|
// Mélodie joyeuse : C5 -> E5 -> G5 -> C6
|
||||||
|
oscillator.frequency.setValueAtTime(523.25, now);
|
||||||
|
oscillator.frequency.setValueAtTime(659.25, now + 0.15);
|
||||||
|
oscillator.frequency.setValueAtTime(783.99, now + 0.3);
|
||||||
|
oscillator.frequency.setValueAtTime(1046.50, now + 0.45);
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0, now);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(volume, now + 0.05);
|
||||||
|
gainNode.gain.setValueAtTime(volume * 0.8, now + 0.5);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.7);
|
||||||
|
|
||||||
|
oscillator.start(now);
|
||||||
|
oscillator.stop(now + 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playWatchlistSound() {
|
||||||
|
if (!audioContext) return;
|
||||||
|
|
||||||
|
const now = audioContext.currentTime;
|
||||||
|
playBeep(now, 800, 0.1);
|
||||||
|
playBeep(now + 0.15, 1000, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playBeep(startTime, frequency, duration) {
|
||||||
|
const oscillator = audioContext.createOscillator();
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
oscillator.frequency.setValueAtTime(frequency, startTime);
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0, startTime);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(volume, startTime + 0.01);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, startTime + duration);
|
||||||
|
|
||||||
|
oscillator.start(startTime);
|
||||||
|
oscillator.stop(startTime + duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMute() {
|
||||||
|
isMuted = !isMuted;
|
||||||
|
localStorage.setItem('soundMuted', isMuted.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVolume(newVolume) {
|
||||||
|
volume = newVolume;
|
||||||
|
localStorage.setItem('soundVolume', volume.toString());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showControls}
|
||||||
|
<div class="fixed bottom-4 right-4 flex items-center gap-2 bg-slate-800/90 backdrop-blur rounded-lg border border-slate-700/50 p-2 shadow-lg z-50">
|
||||||
|
<button
|
||||||
|
on:click={toggleMute}
|
||||||
|
class="p-2 rounded transition-colors {isMuted ? 'bg-red-600/20 text-red-400 hover:bg-red-600/30' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}"
|
||||||
|
title={isMuted ? 'Unmute sounds' : 'Mute sounds'}>
|
||||||
|
{#if isMuted}
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" clip-rule="evenodd"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2"></path>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 border-l border-slate-700 pl-2">
|
||||||
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072"></path>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={volume * 100}
|
||||||
|
on:input={(e) => updateVolume(e.target.value / 100)}
|
||||||
|
class="w-20 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||||
|
title="Volume: {Math.round(volume * 100)}%"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-slate-400 w-8">{Math.round(volume * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,25 +1,61 @@
|
|||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
export let watchlist;
|
export let watchlist;
|
||||||
export let spots;
|
export let spots;
|
||||||
|
export let showOnlyActive = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let newCallsign = '';
|
let newCallsign = '';
|
||||||
let showOnlyActive = false;
|
|
||||||
let watchlistSpots = [];
|
let watchlistSpots = [];
|
||||||
|
let refreshInterval;
|
||||||
|
|
||||||
$: matchingSpots = countWatchlistSpots(spots, watchlist);
|
$: matchingSpots = countWatchlistSpots(spots, watchlist);
|
||||||
$: displayList = showOnlyActive
|
|
||||||
? watchlist.filter(cs => getMatchingSpotsForCallsign(cs).length > 0)
|
|
||||||
: watchlist;
|
|
||||||
|
|
||||||
// Fetch watchlist spots with worked status from API
|
// ✅ Tri alphanumérique simple : 0-9 puis A-Z
|
||||||
|
$: displayList = getDisplayList(watchlist, watchlistSpots, showOnlyActive);
|
||||||
|
|
||||||
|
// ✅ Rafraîchir automatiquement les spots de la watchlist
|
||||||
$: if (watchlist.length > 0) {
|
$: if (watchlist.length > 0) {
|
||||||
fetchWatchlistSpots();
|
fetchWatchlistSpots();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Rafraîchir aussi quand les spots changent (temps réel)
|
||||||
|
$: if (spots.length > 0 && watchlist.length > 0) {
|
||||||
|
fetchWatchlistSpots();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Rafraîchir toutes les 10 secondes pour être sûr
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
if (watchlist.length > 0) {
|
||||||
|
fetchWatchlistSpots();
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getDisplayList(wl, wlSpots, activeOnly) {
|
||||||
|
let list = wl;
|
||||||
|
|
||||||
|
if (activeOnly) {
|
||||||
|
// Filtrer pour ne montrer que les callsigns avec des spots actifs
|
||||||
|
list = wl.filter(cs => {
|
||||||
|
const spots = wlSpots.filter(s => s.dx === cs || s.dx.startsWith(cs));
|
||||||
|
return spots.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tri alphanumérique : 0-9 puis A-Z
|
||||||
|
return [...list].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchWatchlistSpots() {
|
async function fetchWatchlistSpots() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/watchlist/spots');
|
const response = await fetch('/api/watchlist/spots');
|
||||||
@@ -110,14 +146,19 @@
|
|||||||
});
|
});
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Fonction pour le toggle
|
||||||
|
function toggleActiveOnly() {
|
||||||
|
showOnlyActive = !showOnlyActive;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col overflow-hidden">
|
<div class="h-full flex flex-col" style="height: 100%; max-height: 100%;">
|
||||||
<div class="p-3 border-b border-slate-700/50 flex-shrink-0">
|
<div class="p-3 border-b border-slate-700/50 flex-shrink-0">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<h2 class="text-lg font-bold">Watchlist</h2>
|
<h2 class="text-lg font-bold">Watchlist</h2>
|
||||||
<button
|
<button
|
||||||
on:click={() => showOnlyActive = !showOnlyActive}
|
on:click={toggleActiveOnly}
|
||||||
class="px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-2 {showOnlyActive ? 'bg-blue-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
class="px-3 py-1.5 text-xs rounded transition-colors flex items-center gap-2 {showOnlyActive ? 'bg-blue-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
|
||||||
@@ -143,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-3">
|
<div class="flex-1 p-3 overflow-y-auto" style="overflow-y: auto; min-height: 0; flex: 1 1 0;">
|
||||||
{#if displayList.length === 0}
|
{#if displayList.length === 0}
|
||||||
<div class="text-center py-8 text-slate-400">
|
<div class="text-center py-8 text-slate-400">
|
||||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-12 h-12 mx-auto mb-3 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -186,7 +227,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if count > 0}
|
{#if count > 0}
|
||||||
<div class="mt-2 space-y-1 max-h-48 overflow-y-auto">
|
<div class="mt-2 space-y-1 max-h-48 overflow-y-auto" style="overflow-y: auto;">
|
||||||
{#each matchingSpots.slice(0, 10) as spot}
|
{#each matchingSpots.slice(0, 10) as spot}
|
||||||
<button
|
<button
|
||||||
on:click={() => sendSpot(spot)}
|
on:click={() => sendSpot(spot)}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M","5X1XA","C5R","C5LT","EL2BG"]
|
["H44MS","5X2I","PY0FB","PY0FBS","XT2AW","ZL7IO","YJ0CA","FW5K","J38","E6AD","E51MWA","PJ6Y","5J0EA","5K0UA","VP2M","5X1XA","C5R","C5LT","EL2BG","4X6TT","V85NPV","YI1MB"]
|
||||||
Reference in New Issue
Block a user