302 lines
9.2 KiB
C#
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));
|
|
}
|
|
}
|