using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using StreamPlayer.Desktop.Models; using StreamPlayer.Desktop.Services; namespace StreamPlayer.Desktop.ViewModels; public partial class MainWindowViewModel : ViewModelBase { private readonly EventService _eventService = new(); private readonly UpdateService _updateService = new(); private readonly DeviceRegistryService _deviceRegistryService = new(); private readonly WindowsDnsService _dnsService = new(); private readonly ReadOnlyCollection _sections; private readonly AsyncRelayCommand _refreshEventsCommand; private readonly RelayCommand _openChannelCommand; private readonly RelayCommand _openEventCommand; private bool _isInitialized; private CancellationTokenSource? _eventsCts; public ObservableCollection VisibleChannels { get; } = new(); public ObservableCollection VisibleEvents { get; } = new(); [ObservableProperty] private ChannelSection? selectedSection; [ObservableProperty] private bool isLoading; [ObservableProperty] private bool isShowingEvents; [ObservableProperty] private bool isRefreshingEvents; [ObservableProperty] private bool isDeviceCheckInProgress = true; [ObservableProperty] private bool isDeviceAllowed; [ObservableProperty] private string statusMessage = string.Empty; [ObservableProperty] private string deviceStatusMessage = "Verificando dispositivo…"; public ReadOnlyCollection Sections => _sections; public string RefreshButtonLabel => IsRefreshingEvents ? "Actualizando..." : "Actualizar eventos"; public bool IsInteractionLocked => IsDeviceCheckInProgress || !IsDeviceAllowed; public IAsyncRelayCommand RefreshEventsCommand => _refreshEventsCommand; public IRelayCommand OpenChannelCommand => _openChannelCommand; public IRelayCommand OpenEventCommand => _openEventCommand; public event EventHandler? ChannelRequested; public event EventHandler? ErrorRaised; public event EventHandler? UpdateAvailable; public event EventHandler? DeviceStatusEvaluated; public MainWindowViewModel() { var sections = SectionBuilder.BuildSections().ToList(); _sections = new ReadOnlyCollection(sections); _refreshEventsCommand = new AsyncRelayCommand( () => LoadEventsAsync(forceRefresh: true, CancellationToken.None), () => IsShowingEvents && !IsRefreshingEvents && CanInteract()); _openChannelCommand = new RelayCommand( channel => { if (channel != null) { ChannelRequested?.Invoke(this, channel); } }, _ => CanInteract()); _openEventCommand = new RelayCommand( evt => { if (evt == null || string.IsNullOrWhiteSpace(evt.PageUrl)) { return; } ChannelRequested?.Invoke(this, new StreamChannel(evt.Title, evt.PageUrl)); }, _ => CanInteract()); SelectedSection = _sections.FirstOrDefault(); } public async Task InitializeAsync(CancellationToken cancellationToken) { if (_isInitialized) { return; } var dnsResult = await _dnsService.EnsureGoogleDnsAsync(cancellationToken); if (!dnsResult.Success) { ErrorRaised?.Invoke(this, dnsResult.Message); } else if (!string.IsNullOrWhiteSpace(dnsResult.Message)) { StatusMessage = dnsResult.Message; } DnsHelper.WarmUp(); _isInitialized = true; if (SelectedSection != null) { await LoadSectionAsync(SelectedSection, cancellationToken); } } public async Task CheckForUpdatesAsync(CancellationToken cancellationToken) { try { var info = await _updateService.CheckForUpdatesAsync(cancellationToken); if (info != null && info.IsUpdateAvailable(AppVersion.VersionCode)) { UpdateAvailable?.Invoke(this, info); } } catch (Exception ex) { ErrorRaised?.Invoke(this, $"No se pudo verificar actualizaciones: {ex.Message}"); } } public async Task VerifyDeviceAsync(CancellationToken cancellationToken) { IsDeviceCheckInProgress = true; DeviceStatusMessage = "Verificando dispositivo…"; try { var status = await _deviceRegistryService.SyncAsync(cancellationToken); IsDeviceAllowed = !status.IsBlocked; DeviceStatusMessage = status.IsBlocked ? "Dispositivo bloqueado" : string.Empty; DeviceStatusEvaluated?.Invoke(this, status); } catch (Exception ex) { ErrorRaised?.Invoke(this, $"Error sincronizando dispositivo: {ex.Message}"); IsDeviceAllowed = true; DeviceStatusMessage = string.Empty; } finally { IsDeviceCheckInProgress = false; } } partial void OnSelectedSectionChanged(ChannelSection? value) { IsShowingEvents = value?.IsEvents == true; _refreshEventsCommand.NotifyCanExecuteChanged(); if (!_isInitialized || value == null) { return; } _ = LoadSectionSafeAsync(value); } private async Task LoadSectionAsync(ChannelSection section, CancellationToken cancellationToken) { if (section.IsEvents) { await LoadEventsAsync(false, cancellationToken); return; } CancelPendingEvents(); SetLoading(false); VisibleEvents.Clear(); VisibleChannels.Clear(); foreach (var channel in section.Channels) { VisibleChannels.Add(channel); } StatusMessage = VisibleChannels.Count == 0 ? "No hay canales en esta sección." : string.Empty; } private async Task LoadEventsAsync(bool forceRefresh, CancellationToken cancellationToken) { if (SelectedSection?.IsEvents != true) { return; } CancelPendingEvents(); _eventsCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var token = _eventsCts.Token; try { SetLoading(true); IsRefreshingEvents = true; StatusMessage = string.Empty; var events = await _eventService.GetEventsAsync(forceRefresh, token); if (token.IsCancellationRequested) { return; } VisibleChannels.Clear(); VisibleEvents.Clear(); foreach (var evt in events.OrderBy(e => e.StartTimestamp <= 0 ? long.MaxValue : e.StartTimestamp)) { VisibleEvents.Add(evt); } if (VisibleEvents.Count == 0) { StatusMessage = "No hay eventos próximos."; } } catch (OperationCanceledException) { // Ignore cancellation. } catch (Exception ex) { StatusMessage = "No se pudieron cargar los eventos."; ErrorRaised?.Invoke(this, $"No se pudieron cargar los eventos: {ex.Message}"); } finally { if (!cancellationToken.IsCancellationRequested) { SetLoading(false); } IsRefreshingEvents = false; _refreshEventsCommand.NotifyCanExecuteChanged(); } } private async Task LoadSectionSafeAsync(ChannelSection section) { try { await LoadSectionAsync(section, CancellationToken.None); } catch (Exception ex) { ErrorRaised?.Invoke(this, $"Error al actualizar la sección: {ex.Message}"); } } private void SetLoading(bool value) { IsLoading = value; } private void CancelPendingEvents() { if (_eventsCts != null) { _eventsCts.Cancel(); _eventsCts.Dispose(); _eventsCts = null; } } private bool CanInteract() => !IsInteractionLocked; private void NotifyInteractionChanged() { _openChannelCommand.NotifyCanExecuteChanged(); _openEventCommand.NotifyCanExecuteChanged(); _refreshEventsCommand.NotifyCanExecuteChanged(); OnPropertyChanged(nameof(IsInteractionLocked)); } partial void OnIsDeviceCheckInProgressChanged(bool value) { NotifyInteractionChanged(); } partial void OnIsDeviceAllowedChanged(bool value) { NotifyInteractionChanged(); } partial void OnIsRefreshingEventsChanged(bool value) { _refreshEventsCommand.NotifyCanExecuteChanged(); OnPropertyChanged(nameof(RefreshButtonLabel)); } }