Add Windows desktop version
This commit is contained in:
35
windows/StreamPlayer.Desktop/Views/BlockedDialog.axaml
Normal file
35
windows/StreamPlayer.Desktop/Views/BlockedDialog.axaml
Normal 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>
|
||||
67
windows/StreamPlayer.Desktop/Views/BlockedDialog.axaml.cs
Normal file
67
windows/StreamPlayer.Desktop/Views/BlockedDialog.axaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
192
windows/StreamPlayer.Desktop/Views/MainWindow.axaml
Normal file
192
windows/StreamPlayer.Desktop/Views/MainWindow.axaml
Normal 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>
|
||||
208
windows/StreamPlayer.Desktop/Views/MainWindow.axaml.cs
Normal file
208
windows/StreamPlayer.Desktop/Views/MainWindow.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
74
windows/StreamPlayer.Desktop/Views/PlayerWindow.axaml
Normal file
74
windows/StreamPlayer.Desktop/Views/PlayerWindow.axaml
Normal 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>
|
||||
229
windows/StreamPlayer.Desktop/Views/PlayerWindow.axaml.cs
Normal file
229
windows/StreamPlayer.Desktop/Views/PlayerWindow.axaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
38
windows/StreamPlayer.Desktop/Views/UpdateDialog.axaml
Normal file
38
windows/StreamPlayer.Desktop/Views/UpdateDialog.axaml
Normal 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>
|
||||
96
windows/StreamPlayer.Desktop/Views/UpdateDialog.axaml.cs
Normal file
96
windows/StreamPlayer.Desktop/Views/UpdateDialog.axaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user