Files
app/windows/StreamPlayer.Desktop/ViewModels/MainWindowViewModel.cs
2025-12-17 19:20:55 +00:00

302 lines
9.2 KiB
C#

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<ChannelSection> _sections;
private readonly AsyncRelayCommand _refreshEventsCommand;
private readonly RelayCommand<StreamChannel> _openChannelCommand;
private readonly RelayCommand<LiveEvent> _openEventCommand;
private bool _isInitialized;
private CancellationTokenSource? _eventsCts;
public ObservableCollection<StreamChannel> VisibleChannels { get; } = new();
public ObservableCollection<LiveEvent> 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<ChannelSection> Sections => _sections;
public string RefreshButtonLabel => IsRefreshingEvents
? "Actualizando..."
: "Actualizar eventos";
public bool IsInteractionLocked => IsDeviceCheckInProgress || !IsDeviceAllowed;
public IAsyncRelayCommand RefreshEventsCommand => _refreshEventsCommand;
public IRelayCommand<StreamChannel> OpenChannelCommand => _openChannelCommand;
public IRelayCommand<LiveEvent> OpenEventCommand => _openEventCommand;
public event EventHandler<StreamChannel>? ChannelRequested;
public event EventHandler<string>? ErrorRaised;
public event EventHandler<UpdateInfo>? UpdateAvailable;
public event EventHandler<DeviceStatus>? DeviceStatusEvaluated;
public MainWindowViewModel()
{
var sections = SectionBuilder.BuildSections().ToList();
_sections = new ReadOnlyCollection<ChannelSection>(sections);
_refreshEventsCommand = new AsyncRelayCommand(
() => LoadEventsAsync(forceRefresh: true, CancellationToken.None),
() => IsShowingEvents && !IsRefreshingEvents && CanInteract());
_openChannelCommand = new RelayCommand<StreamChannel>(
channel =>
{
if (channel != null)
{
ChannelRequested?.Invoke(this, channel);
}
},
_ => CanInteract());
_openEventCommand = new RelayCommand<LiveEvent>(
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));
}
}