This commit is contained in:
2025-10-15 00:28:53 +05:30
parent 5b46ac98ad
commit af52fe8c72
12 changed files with 433 additions and 290 deletions

View File

@@ -79,15 +79,17 @@
filteredSpots = applyFilters(spots, spotFilters, watchlist);
}
}
$: if (typeof localStorage !== 'undefined') {
localStorage.setItem('soundEnabled', soundEnabled.toString());
}
// Détecter les nouveaux spots et jouer les sons appropriés
$: if (spots.length > 0 && soundEnabled) {
checkForNewSpots(spots, previousSpots, watchlist);
previousSpots = [...spots];
// ✅ Ne garder que les 100 derniers spots pour la comparaison
previousSpots = spots.slice(0, 100);
}
// ✅ 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
@@ -318,6 +320,14 @@
case 'dxccProgress':
dxccProgress = message.data || { worked: 0, total: 340, percentage: 0 };
break;
case 'milestone': // ✅ AJOUTER
const milestoneData = message.data;
const toastType = milestoneData.type === 'qso' ? 'milestone' : 'band';
showToast(milestoneData.message, toastType);
if (soundEnabled) {
playSound('milestone');
}
break;
}
}
@@ -425,11 +435,22 @@
}
onMount(() => {
const savedSoundEnabled = localStorage.getItem('soundEnabled');
if (savedSoundEnabled !== null) {
soundEnabled = savedSoundEnabled === 'true';
}
connectWebSocket();
fetchSolarData();
const solarInterval = setInterval(fetchSolarData, 15 * 60 * 1000);
const cleanupInterval = setInterval(() => {
// Nettoyer previousSpots
if (previousSpots.length > 100) {
previousSpots = previousSpots.slice(0, 100);
}
}, 60000);
const handleSendSpot = (e) => {
sendCallsign(e.detail.callsign, e.detail.frequency, e.detail.mode);
};
@@ -444,7 +465,7 @@
});
</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-2">
<!-- Gestionnaire de sons -->
<SoundManager bind:enabled={soundEnabled} />
@@ -460,7 +481,9 @@
{stats}
{solarData}
{wsStatus}
on:shutdown={shutdownApp}
{soundEnabled}
on:shutdown={shutdownApp}
on:toggleSound={() => soundEnabled = !soundEnabled}
/>
<StatsCards
@@ -475,9 +498,9 @@
on:toggleFilter={(e) => toggleFilter(e.detail)}
/>
<div class="grid grid-cols-4 gap-3 overflow-hidden" style="height: calc(100vh - 360px); min-height: 500px;">
<div class="col-span-3 overflow-hidden">
<SpotsTable
<div class="grid grid-cols-[2.8fr_1.2fr] gap-3 overflow-hidden" style="height: calc(100vh - 280px); min-height: 500px;">
<div class="overflow-hidden">
<SpotsTable
spots={filteredSpots}
{watchlist}
myCallsign={stats.myCallsign}

View File

@@ -1,6 +1,7 @@
<script>
import { createEventDispatcher } from 'svelte';
export let soundEnabled = true;
export let stats;
export let solarData;
export let wsStatus;
@@ -50,7 +51,7 @@
FlexDXCluster
</h1>
<div class="flex items-center gap-3 text-xs text-slate-400">
<span>F4BPO<span>{stats.totalContacts}</span> Contacts</span>
<span>{stats.myCallsign || 'N/A'}<span>{stats.totalContacts}</span> Contacts</span>
<span class="text-slate-600">|</span>
<span class="flex items-center gap-1">
<span class="font-semibold text-amber-400">SFI:</span>
@@ -100,6 +101,25 @@
Flex
</span>
<!-- Bouton Son -->
<button
on:click={() => dispatch('toggleSound')}
class="px-3 py-1.5 rounded-lg transition-colors flex items-center gap-2 text-sm {soundEnabled ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-slate-700/50 hover:bg-slate-700 text-slate-300'}"
title={soundEnabled ? 'Mute sounds' : 'Enable sounds'}>
{#if soundEnabled}
<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="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>
<span>Sound On</span>
{:else}
<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="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>
<span>Sound Off</span>
{/if}
</button>
<button
on:click={() => dispatch('shutdown')}
class="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors flex items-center gap-1">

View File

@@ -26,7 +26,7 @@
});
function handlePlaySound(event) {
if (!enabled || isMuted) return;
if (!enabled) return; // ✅ Utiliser 'enabled' (qui vient de App.svelte) au lieu de 'isMuted'
const { type } = event.detail;
@@ -89,10 +89,20 @@
oscillator.start(startTime);
oscillator.stop(startTime + duration);
}
function playMilestoneSound() {
if (!audioContext) return;
const now = audioContext.currentTime;
// Mélodie festive : E5 -> G5 -> A5 -> C6
playBeep(now, 659.25, 0.15);
playBeep(now + 0.15, 783.99, 0.15);
playBeep(now + 0.3, 880.00, 0.15);
playBeep(now + 0.45, 1046.50, 0.2);
}
function toggleMute() {
isMuted = !isMuted;
localStorage.setItem('soundMuted', isMuted.toString());
enabled = !enabled; // ✅ Modifier 'enabled' au lieu de 'isMuted'
}
function updateVolume(newVolume) {
@@ -105,9 +115,9 @@
<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}
class="p-2 rounded transition-colors {!enabled ? 'bg-red-600/20 text-red-400 hover:bg-red-600/30' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}"
title={!enabled ? 'Unmute sounds' : 'Mute sounds'}>
{#if !enabled}
<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>

View File

@@ -1,37 +1,41 @@
<script>
export let message;
export let type = 'info'; // 'success', 'error', 'warning', 'info'
export let type = 'info'; // 'success', 'error', 'warning', 'info', 'milestone', 'band'
const icons = {
success: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>`,
error: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>`,
warning: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>`,
info: ''
info: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>`,
milestone: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path>`,
band: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>`
};
const colors = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-orange-500',
info: 'bg-blue-500'
info: 'bg-blue-500',
milestone: 'bg-gradient-to-r from-purple-500 to-pink-500',
band: 'bg-gradient-to-r from-orange-500 to-amber-500'
};
</script>
<div class="fixed bottom-5 right-5 {colors[type]} text-white px-5 py-3 rounded-lg shadow-lg z-50 animate-in slide-in-from-bottom-5 duration-300">
<div class="flex items-center gap-2">
<div class="fixed bottom-5 right-5 {colors[type]} text-white px-5 py-3 rounded-lg shadow-lg z-50 animate-in slide-in-from-bottom-5 duration-300 min-w-[300px] backdrop-blur-sm">
<div class="flex items-center gap-3">
{#if icons[type]}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{@html icons[type]}
</svg>
{/if}
<span>{message}</span>
<span class="font-medium text-sm">{message}</span>
</div>
</div>
<style>
@keyframes slide-in-from-bottom {
from {
transform: translateY(400px);
transform: translateY(100px);
opacity: 0;
}
to {

View File

@@ -75,17 +75,24 @@
).length;
}
function getMatchingSpotsForCallsign(callsign) {
const spots = watchlistSpots.filter(s => s.dx === callsign || s.dx.startsWith(callsign));
function getMatchingSpotsForCallsign(callsign) {
const spots = watchlistSpots.filter(s => s.dx === callsign || s.dx.startsWith(callsign));
// ✅ Trier par bande d'abord, puis par heure
const bandOrder = { '160M': 0, '80M': 1, '60M': 2, '40M': 3, '30M': 4, '20M': 5, '17M': 6, '15M': 7, '12M': 8, '10M': 9, '6M': 10 };
return spots.sort((a, b) => {
// Trier par bande en premier
const bandA = bandOrder[a.band] ?? 99;
const bandB = bandOrder[b.band] ?? 99;
if (bandA !== bandB) return bandA - bandB;
// ✅ Trier les spots par heure décroissante (plus récent en premier)
return spots.sort((a, b) => {
// Comparer les heures UTC (format "HH:MM")
const timeA = a.utcTime || "00:00";
const timeB = b.utcTime || "00:00";
return timeB.localeCompare(timeA);
});
}
// Si même bande, trier par heure (plus récent en premier)
const timeA = a.utcTime || "00:00";
const timeB = b.utcTime || "00:00";
return timeB.localeCompare(timeA);
});
}
async function addToWatchlist() {
const callsign = newCallsign.trim().toUpperCase();