Add Windows desktop version

This commit is contained in:
renato97
2025-12-17 19:20:55 +00:00
parent 93dbe0941e
commit 8921d7f2a6
36 changed files with 2760 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="StreamPlayer.Desktop.Views.BlockedDialog"
Width="420"
SizeToContent="Height"
WindowStartupLocation="CenterOwner"
CanResize="False"
Title="Dispositivo bloqueado">
<Border Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="Acceso bloqueado"
FontSize="20"
FontWeight="SemiBold"/>
<TextBlock x:Name="ReasonText"
TextWrapping="Wrap"/>
<StackPanel x:Name="TokenPanel"
Spacing="6"
IsVisible="False">
<TextBlock Text="Token para soporte:"/>
<TextBox x:Name="TokenText"
IsReadOnly="True"
Background="#111"
Foreground="White"
BorderBrush="#555"/>
<Button Content="Copiar token"
HorizontalAlignment="Left"
Click="OnCopyClicked"/>
</StackPanel>
<Button Content="Cerrar aplicación"
HorizontalAlignment="Right"
Width="180"
Click="OnCloseClicked"/>
</StackPanel>
</Border>
</Window>

View File

@@ -0,0 +1,67 @@
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
namespace StreamPlayer.Desktop.Views;
public partial class BlockedDialog : Window
{
private readonly string _token;
private TextBlock? _reasonText;
private StackPanel? _tokenPanel;
private TextBox? _tokenText;
public BlockedDialog() : this(string.Empty, string.Empty)
{
}
public BlockedDialog(string reason, string token)
{
_token = token ?? string.Empty;
InitializeComponent();
if (_reasonText != null)
{
_reasonText.Text = string.IsNullOrWhiteSpace(reason)
? "Tu dispositivo fue bloqueado por el administrador."
: reason;
}
if (!string.IsNullOrWhiteSpace(_token))
{
if (_tokenPanel != null)
{
_tokenPanel.IsVisible = true;
}
if (_tokenText != null)
{
_tokenText.Text = _token;
}
}
}
private async void OnCopyClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (string.IsNullOrEmpty(_token))
{
return;
}
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard != null)
{
await clipboard.SetTextAsync(_token);
}
}
private void OnCloseClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close(true);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_reasonText = this.FindControl<TextBlock>("ReasonText");
_tokenPanel = this.FindControl<StackPanel>("TokenPanel");
_tokenText = this.FindControl<TextBox>("TokenText");
}
}

View File

@@ -0,0 +1,192 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:StreamPlayer.Desktop.ViewModels"
xmlns:models="using:StreamPlayer.Desktop.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="StreamPlayer.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
Width="1200"
Height="720"
Title="StreamPlayer Desktop">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<Grid ColumnDefinitions="220,*" RowDefinitions="Auto,Auto,*"
IsEnabled="{Binding IsInteractionLocked, Converter={StaticResource InverseBooleanConverter}}">
<Border Grid.RowSpan="3"
Background="#111217"
Padding="0">
<ListBox ItemsSource="{Binding Sections}"
SelectedItem="{Binding SelectedSection}"
BorderThickness="0"
Background="Transparent"
Foreground="White">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="4"/>
<Setter Property="Padding" Value="14"/>
<Setter Property="CornerRadius" Value="6"/>
</Style>
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#2A2F3A"/>
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="#3A7AFE"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:ChannelSection">
<TextBlock Text="{Binding Title}"
FontSize="15"
TextWrapping="Wrap"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="20,16,20,8"
Spacing="12">
<TextBlock Text="{Binding SelectedSection.Title, FallbackValue=StreamPlayer}"
FontSize="26"
FontWeight="SemiBold"/>
<Button Content="{Binding RefreshButtonLabel}"
Command="{Binding RefreshEventsCommand}"
IsVisible="{Binding IsShowingEvents}"
IsEnabled="{Binding IsRefreshingEvents, Converter={StaticResource InverseBooleanConverter}}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Padding="12,4"/>
</StackPanel>
<TextBlock Grid.Column="1"
Grid.Row="1"
Text="{Binding StatusMessage}"
Margin="20,0,20,8"
Foreground="#8A8F9C"
FontStyle="Italic"/>
<Grid Grid.Column="1" Grid.Row="2">
<ScrollViewer IsVisible="{Binding IsShowingEvents, Converter={StaticResource InverseBooleanConverter}}"
Margin="10"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding VisibleChannels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:StreamChannel">
<Button Command="{Binding $parent[Window].DataContext.OpenChannelCommand}"
CommandParameter="{Binding .}"
Margin="6"
Padding="16"
Width="220"
HorizontalContentAlignment="Stretch">
<StackPanel>
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" TextWrapping="Wrap"/>
<TextBlock Text="{Binding PageUrl}" FontSize="11" Foreground="#8A8F9C"
TextWrapping="Wrap"/>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ScrollViewer IsVisible="{Binding IsShowingEvents}"
Margin="10"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding VisibleEvents}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:LiveEvent">
<Border Margin="0,0,0,8"
Padding="14"
Background="#181C24"
CornerRadius="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding Title}"
FontSize="16"
FontWeight="SemiBold"
TextWrapping="Wrap"/>
<TextBlock Text="{Binding Subtitle}"
Foreground="#8A8F9C"
Margin="0,4,0,0"/>
<TextBlock Text="{Binding ChannelName}"
Foreground="#6FA8FF"
FontSize="13"/>
</StackPanel>
<StackPanel Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Spacing="6">
<Button Content="Ver canal"
Command="{Binding $parent[Window].DataContext.OpenEventCommand}"
CommandParameter="{Binding .}"
Padding="12,4"/>
<Border Background="{Binding IsLive, Converter={StaticResource LiveStatusBrushConverter}}"
CornerRadius="4"
Padding="6"
HorizontalAlignment="Right">
<TextBlock Text="{Binding Status}"
Foreground="White"
FontSize="12"/>
</Border>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Border Background="#CC0E1016"
IsVisible="{Binding IsLoading}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="12">
<ProgressBar Width="220" IsIndeterminate="True"/>
<TextBlock Text="Cargando contenido..."
Foreground="White"
FontSize="16"/>
</StackPanel>
</Border>
</Grid>
<Border Grid.ColumnSpan="2"
Grid.RowSpan="3"
Background="#CC000000"
IsVisible="{Binding IsInteractionLocked}">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<ProgressBar Width="260"
IsIndeterminate="True"
IsVisible="{Binding IsDeviceCheckInProgress}"/>
<TextBlock Text="{Binding DeviceStatusMessage}"
Foreground="White"
FontSize="18"
TextAlignment="Center"
TextWrapping="Wrap"
Margin="0,4"/>
<TextBlock Text="Espera a que verifiquemos tu dispositivo…"
FontSize="14"
Foreground="#DDDDDD"
TextAlignment="Center"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,208 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using StreamPlayer.Desktop.Models;
using StreamPlayer.Desktop.ViewModels;
namespace StreamPlayer.Desktop.Views;
public partial class MainWindow : Window
{
private readonly CancellationTokenSource _lifetimeCts = new();
private MainWindowViewModel? _viewModel;
public MainWindow()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
Opened += OnOpened;
Closed += OnClosed;
AttachViewModel(DataContext as MainWindowViewModel);
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
AttachViewModel(DataContext as MainWindowViewModel);
}
private void AttachViewModel(MainWindowViewModel? newViewModel)
{
if (_viewModel != null)
{
_viewModel.ChannelRequested -= OnChannelRequested;
_viewModel.UpdateAvailable -= OnUpdateAvailable;
_viewModel.DeviceStatusEvaluated -= OnDeviceStatusEvaluated;
_viewModel.ErrorRaised -= OnErrorRaised;
}
_viewModel = newViewModel;
if (_viewModel != null)
{
_viewModel.ChannelRequested += OnChannelRequested;
_viewModel.UpdateAvailable += OnUpdateAvailable;
_viewModel.DeviceStatusEvaluated += OnDeviceStatusEvaluated;
_viewModel.ErrorRaised += OnErrorRaised;
}
}
private async void OnOpened(object? sender, EventArgs e)
{
if (_viewModel == null)
{
return;
}
try
{
await _viewModel.InitializeAsync(_lifetimeCts.Token);
await _viewModel.CheckForUpdatesAsync(_lifetimeCts.Token);
await _viewModel.VerifyDeviceAsync(_lifetimeCts.Token);
}
catch (OperationCanceledException)
{
// ignored
}
}
private void OnClosed(object? sender, EventArgs e)
{
_lifetimeCts.Cancel();
_lifetimeCts.Dispose();
AttachViewModel(null);
}
private void OnChannelRequested(object? sender, StreamChannel channel)
{
if (channel == null)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
var player = new PlayerWindow(channel);
player.Show(this);
});
}
private async void OnUpdateAvailable(object? sender, UpdateInfo info)
{
var mandatory = info.IsMandatory(AppVersion.VersionCode);
var dialog = new UpdateDialog(info, mandatory);
var result = await dialog.ShowDialog<UpdateDialogAction>(this);
switch (result)
{
case UpdateDialogAction.Download:
LaunchUrl(info.DownloadUrl);
if (mandatory)
{
Close();
}
break;
case UpdateDialogAction.ViewRelease:
LaunchUrl(string.IsNullOrWhiteSpace(info.ReleasePageUrl)
? info.DownloadUrl
: info.ReleasePageUrl);
if (mandatory)
{
Close();
}
break;
case UpdateDialogAction.Exit:
Close();
break;
case UpdateDialogAction.Skip:
default:
break;
}
}
private async void OnDeviceStatusEvaluated(object? sender, DeviceStatus status)
{
if (!status.IsBlocked)
{
return;
}
var dialog = new BlockedDialog(status.Reason, status.TokenPart);
await dialog.ShowDialog(this);
Close();
}
private async void OnErrorRaised(object? sender, string message)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
await ShowInfoAsync("Aviso", message);
}
private static void LaunchUrl(string? url)
{
if (string.IsNullOrWhiteSpace(url))
{
return;
}
try
{
Process.Start(new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
}
catch
{
// ignore
}
}
private Task ShowInfoAsync(string title, string message)
{
var okButton = new Button
{
Content = "Aceptar",
HorizontalAlignment = HorizontalAlignment.Right,
Width = 120
};
var stack = new StackPanel
{
Spacing = 12
};
stack.Children.Add(new TextBlock
{
Text = message,
TextWrapping = TextWrapping.Wrap
});
stack.Children.Add(okButton);
var dialog = new Window
{
Title = title,
Width = 420,
SizeToContent = SizeToContent.Height,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = false,
Content = new Border
{
Padding = new Thickness(20),
Child = stack
}
};
okButton.Click += (_, _) => dialog.Close();
return dialog.ShowDialog(this);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@@ -0,0 +1,74 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
x:Class="StreamPlayer.Desktop.Views.PlayerWindow"
Width="960"
Height="540"
WindowStartupLocation="CenterOwner"
Title="StreamPlayer - Reproductor">
<Grid Background="Black">
<vlc:VideoView x:Name="VideoView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PointerPressed="OnVideoPointerPressed"/>
<Border x:Name="Overlay"
Background="#80000000"
Padding="20"
IsVisible="True">
<DockPanel>
<TextBlock x:Name="ChannelNameText"
Text="Canal"
FontSize="20"
Foreground="White"
FontWeight="SemiBold"
DockPanel.Dock="Left"/>
<Button Content="Cerrar"
HorizontalAlignment="Right"
Click="OnCloseClicked"
Margin="12,0,0,0"
DockPanel.Dock="Right"/>
</DockPanel>
</Border>
<Border x:Name="LoadingOverlay"
Background="#CC000000"
IsVisible="True">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<ProgressBar IsIndeterminate="True" Width="280"/>
<TextBlock x:Name="StatusText"
Text="Preparando..."
Foreground="White"
FontSize="16"
TextAlignment="Center"/>
</StackPanel>
</Border>
<Border x:Name="ErrorOverlay"
Background="#CC1E1E1E"
IsVisible="False">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<TextBlock Text="Ocurrió un error"
FontSize="20"
Foreground="White"
FontWeight="SemiBold"
TextAlignment="Center"/>
<TextBlock x:Name="ErrorText"
Text=""
FontSize="14"
Foreground="#FFDDDD"
TextWrapping="Wrap"
MaxWidth="420"
TextAlignment="Center"/>
<Button Content="Cerrar"
Width="140"
HorizontalAlignment="Center"
Click="OnCloseClicked"/>
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,229 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LibVLCSharp.Avalonia;
using LibVLCSharp.Shared;
using StreamPlayer.Desktop.Models;
using StreamPlayer.Desktop.Services;
namespace StreamPlayer.Desktop.Views;
public partial class PlayerWindow : Window
{
private readonly StreamChannel _channel;
private readonly StreamUrlResolver _resolver = new();
private readonly CancellationTokenSource _lifetimeCts = new();
private LibVLC? _libVlc;
private MediaPlayer? _mediaPlayer;
private bool _overlayVisible = true;
private TextBlock? _channelNameText;
private Border? _loadingOverlay;
private Border? _errorOverlay;
private TextBlock? _statusText;
private VideoView? _videoView;
private TextBlock? _errorText;
private Border? _overlay;
public PlayerWindow() : this(new StreamChannel("Canal", string.Empty))
{
}
public PlayerWindow(StreamChannel channel)
{
_channel = channel;
InitializeComponent();
if (_channelNameText != null)
{
_channelNameText.Text = channel.Name;
}
Title = $"Reproduciendo {channel.Name}";
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
_ = LoadAndPlayAsync();
}
private async Task LoadAndPlayAsync()
{
try
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
if (_loadingOverlay != null)
{
_loadingOverlay.IsVisible = true;
}
if (_errorOverlay != null)
{
_errorOverlay.IsVisible = false;
}
if (_statusText != null)
{
_statusText.Text = "Resolviendo stream…";
}
});
string resolved = await _resolver.ResolveAsync(_channel.PageUrl, _lifetimeCts.Token);
await Dispatcher.UIThread.InvokeAsync(() => StartPlayback(resolved));
}
catch (OperationCanceledException)
{
// ignored
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(() => ShowError($"No se pudo iniciar el stream: {ex.Message}"));
}
}
private void StartPlayback(string streamUrl)
{
ReleasePlayer();
_libVlc = new LibVLC();
_mediaPlayer = new MediaPlayer(_libVlc);
_mediaPlayer.Playing += MediaPlayerOnPlaying;
_mediaPlayer.Buffering += MediaPlayerOnBuffering;
_mediaPlayer.EncounteredError += MediaPlayerOnEncounteredError;
_mediaPlayer.EndReached += MediaPlayerOnEndReached;
if (_videoView != null)
{
_videoView.MediaPlayer = _mediaPlayer;
}
using var media = new Media(_libVlc, new Uri(streamUrl));
media.AddOption($":http-referrer={_channel.PageUrl}");
media.AddOption(":http-header=Origin: https://streamtpmedia.com");
media.AddOption(":http-header=Accept: */*");
media.AddOption(":http-header=Connection: keep-alive");
media.AddOption(":network-caching=1500");
media.AddOption(":clock-jitter=0");
media.AddOption(":clock-synchro=0");
media.AddOption(":adaptive-use-access=true");
media.AddOption(":http-user-agent=Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TQ3A.230805.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0 Mobile Safari/537.36 ExoPlayer/2.18.7");
media.AddOption(":input-repeat=-1");
_mediaPlayer.Play(media);
}
private void MediaPlayerOnEndReached(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
if (_loadingOverlay != null)
{
_loadingOverlay.IsVisible = false;
}
});
}
private void MediaPlayerOnEncounteredError(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() => ShowError("Error en la reproducción. Intenta nuevamente más tarde."));
}
private void MediaPlayerOnBuffering(object? sender, MediaPlayerBufferingEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
if (_loadingOverlay != null)
{
_loadingOverlay.IsVisible = true;
}
if (_statusText != null)
{
_statusText.Text = "Conectando…";
}
});
}
private void MediaPlayerOnPlaying(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
if (_loadingOverlay != null)
{
_loadingOverlay.IsVisible = false;
}
SetOverlayVisible(false);
});
}
private void ShowError(string message)
{
if (_loadingOverlay != null)
{
_loadingOverlay.IsVisible = false;
}
if (_errorOverlay != null)
{
_errorOverlay.IsVisible = true;
}
if (_errorText != null)
{
_errorText.Text = message;
}
}
private void ReleasePlayer()
{
if (_mediaPlayer != null)
{
_mediaPlayer.Playing -= MediaPlayerOnPlaying;
_mediaPlayer.Buffering -= MediaPlayerOnBuffering;
_mediaPlayer.EncounteredError -= MediaPlayerOnEncounteredError;
_mediaPlayer.EndReached -= MediaPlayerOnEndReached;
_mediaPlayer.Dispose();
_mediaPlayer = null;
}
if (_libVlc != null)
{
_libVlc.Dispose();
_libVlc = null;
}
}
private void OnVideoPointerPressed(object? sender, PointerPressedEventArgs e)
{
SetOverlayVisible(!_overlayVisible);
}
private void SetOverlayVisible(bool visible)
{
_overlayVisible = visible;
if (_overlay != null)
{
_overlay.IsVisible = visible;
}
}
private void OnCloseClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close();
}
protected override void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
_lifetimeCts.Cancel();
ReleasePlayer();
_lifetimeCts.Dispose();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_channelNameText = this.FindControl<TextBlock>("ChannelNameText");
_loadingOverlay = this.FindControl<Border>("LoadingOverlay");
_errorOverlay = this.FindControl<Border>("ErrorOverlay");
_statusText = this.FindControl<TextBlock>("StatusText");
_videoView = this.FindControl<VideoView>("VideoView");
_errorText = this.FindControl<TextBlock>("ErrorText");
_overlay = this.FindControl<Border>("Overlay");
}
}

View File

@@ -0,0 +1,38 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="StreamPlayer.Desktop.Views.UpdateDialog"
Width="520"
SizeToContent="Height"
WindowStartupLocation="CenterOwner"
CanResize="False"
Title="Actualización disponible">
<Border Padding="20">
<StackPanel Spacing="12">
<TextBlock x:Name="TitleText"
FontSize="20"
FontWeight="SemiBold"
Text="Actualización disponible"/>
<TextBlock x:Name="InfoText"
TextWrapping="Wrap"/>
<TextBlock Text="Novedades"
FontWeight="SemiBold"
Margin="0,8,0,0"/>
<ScrollViewer Height="180">
<TextBlock x:Name="NotesText"
TextWrapping="Wrap"/>
</ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
<Button Content="Ver release"
x:Name="ViewReleaseButton"
Click="OnViewReleaseClicked"/>
<Button Content="Descargar"
IsDefault="True"
Click="OnDownloadClicked"/>
<Button x:Name="DismissButton"
Content="Más tarde"
IsCancel="True"
Click="OnDismissClicked"/>
</StackPanel>
</StackPanel>
</Border>
</Window>

View File

@@ -0,0 +1,96 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using StreamPlayer.Desktop.Models;
namespace StreamPlayer.Desktop.Views;
public enum UpdateDialogAction
{
Skip,
Download,
ViewRelease,
Exit
}
public partial class UpdateDialog : Window
{
private readonly UpdateInfo _info;
private readonly bool _mandatory;
private TextBlock? _titleText;
private TextBlock? _infoText;
private TextBlock? _notesText;
private Button? _viewReleaseButton;
private Button? _dismissButton;
public UpdateDialog() : this(
new UpdateInfo(AppVersion.VersionCode, AppVersion.VersionName, string.Empty, string.Empty, string.Empty, 0, 0, string.Empty, AppVersion.VersionCode, false),
false)
{
}
public UpdateDialog(UpdateInfo info, bool mandatory)
{
_info = info;
_mandatory = mandatory;
InitializeComponent();
ConfigureView();
}
private void ConfigureView()
{
if (_titleText != null)
{
_titleText.Text = _mandatory ? "Actualización obligatoria" : "Actualización disponible";
}
if (_infoText != null)
{
_infoText.Text =
$"Versión instalada: {AppVersion.VersionName} ({AppVersion.VersionCode}){Environment.NewLine}" +
$"Última versión: {_info.VersionName} ({_info.VersionCode}){Environment.NewLine}" +
$"Tamaño estimado: {_info.FormatSize()} Descargas: {_info.DownloadCount}";
}
if (_notesText != null)
{
_notesText.Text = string.IsNullOrWhiteSpace(_info.ReleaseNotes)
? "La release no incluye notas."
: _info.ReleaseNotes;
}
if (_viewReleaseButton != null && string.IsNullOrWhiteSpace(_info.ReleasePageUrl))
{
_viewReleaseButton.IsVisible = false;
}
if (_mandatory && _dismissButton != null)
{
_dismissButton.Content = "Salir";
}
}
private void OnDownloadClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close(UpdateDialogAction.Download);
}
private void OnViewReleaseClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close(UpdateDialogAction.ViewRelease);
}
private void OnDismissClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Close(_mandatory ? UpdateDialogAction.Exit : UpdateDialogAction.Skip);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_titleText = this.FindControl<TextBlock>("TitleText");
_infoText = this.FindControl<TextBlock>("InfoText");
_notesText = this.FindControl<TextBlock>("NotesText");
_viewReleaseButton = this.FindControl<Button>("ViewReleaseButton");
_dismissButton = this.FindControl<Button>("DismissButton");
}
}