Add Windows desktop version
This commit is contained in:
301
windows/StreamPlayer.Desktop/ViewModels/MainWindowViewModel.cs
Normal file
301
windows/StreamPlayer.Desktop/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
7
windows/StreamPlayer.Desktop/ViewModels/ViewModelBase.cs
Normal file
7
windows/StreamPlayer.Desktop/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace StreamPlayer.Desktop.ViewModels;
|
||||
|
||||
public abstract class ViewModelBase : ObservableObject
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user