feat: Add macOS VLC native bundling and improve UI styling

- Added AsyncImageLoader package for improved image loading.
- Implemented macOS native VLC library bundling with a script to fetch and copy necessary files.
- Enhanced PlayerView UI with updated colors and improved layout for better user experience.
- Refactored media playback logic in PlayerView to handle buffering and errors more gracefully.
- Updated Playlists and Programs views to use consistent styling and improved text colors.
- Introduced a new settings layout with better organization and visual appeal.
This commit is contained in:
Vladimir
2026-03-22 16:58:11 +02:00
parent f0cbf709b6
commit f995625460
11 changed files with 761 additions and 164 deletions
+36 -30
View File
@@ -5,51 +5,55 @@
<Grid>
<vlc:VideoView x:Name="VideoView" />
<Grid Background="#01000000">
<Grid Background="#00000000">
<Grid.RowDefinitions>
<RowDefinition Height="80" />
<RowDefinition Height="*" />
<RowDefinition Height="80" />
</Grid.RowDefinitions>
<Grid Background="#70000000">
<Border Background="#E5E7EBCC" BorderBrush="#D1D5DB" BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<Button Width="70" Height="50" Margin="10,0,0,0" Classes="icon-yellow" Command="{Binding BackCommand}">
<Viewbox Width="22" Height="22">
<Path Fill="Gray" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
<Button Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Command="{Binding BackCommand}">
<Viewbox Width="14" Height="14">
<Path Fill="#6B7280" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
</Viewbox>
</Button>
<TextBlock Grid.Column="1" FontSize="20" Foreground="White" Text="{Binding TopPanelTitle}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Button Grid.Column="2" Width="50" Height="70" Margin="10,0,0,0" Classes="icon-yellow" Command="{Binding FullscreenCommand}">
<Viewbox Width="22" Height="22">
<Path Fill="Gray" Data="M4 9V4H9V6H6V9H4M15 4H20V9H18V6H15V4M20 15V20H15V18H18V15H20M9 20H4V15H6V18H9V20Z" />
<TextBlock Grid.Column="1" FontSize="20" FontWeight="SemiBold" Foreground="#111827" Text="{Binding TopPanelTitle}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Button Grid.Column="2" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Command="{Binding FullscreenCommand}">
<Viewbox Width="14" Height="14">
<Path Fill="#6B7280" Data="M4 9V4H9V6H6V9H4M15 4H20V9H18V6H15V4M20 15V20H15V18H18V15H20M9 20H4V15H6V18H9V20Z" />
</Viewbox>
</Button>
<Button Grid.Column="3" Width="70" Height="70" Margin="10,0,0,0" Classes="icon-red" Command="{Binding CloseAppCommand}">
<Viewbox Width="22" Height="22">
<Button Grid.Column="3" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-red" Command="{Binding CloseAppCommand}">
<Viewbox Width="14" Height="14">
<Path Stroke="White" StrokeThickness="2.5" Data="M5,5 L19,19 M19,5 L5,19" />
</Viewbox>
</Button>
</Grid>
</Border>
<Border Grid.Row="1"
Width="400"
HorizontalAlignment="Left"
Background="#B0000000"
Background="#F8FAFCCC"
BorderBrush="#D1D5DB"
BorderThickness="0,0,1,0"
IsVisible="{Binding ProgramGuideVisible}">
<ItemsControl ItemsSource="{Binding Programs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical" Margin="6,2">
<TextBlock Text="{Binding Title}" TextWrapping="Wrap" FontSize="15" Foreground="White" TextAlignment="Left" Margin="1,0,1,2" />
<TextBlock Text="{Binding Title}" TextWrapping="Wrap" FontSize="15" Foreground="#111827" TextAlignment="Left" Margin="1,0,1,2" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding StartTime, StringFormat='{}{0:HH:mm}'}" FontSize="12" Foreground="White" Margin="1,0,1,2" />
<TextBlock Text="{Binding EndTime, StringFormat='{}{0:HH:mm}'}" FontSize="12" Foreground="White" Margin="10,0,1,2" />
<TextBlock Text="{Binding StartTime, StringFormat='{}{0:HH:mm}'}" FontSize="12" Foreground="#6B7280" Margin="1,0,1,2" />
<TextBlock Text="{Binding EndTime, StringFormat='{}{0:HH:mm}'}" FontSize="12" Foreground="#6B7280" Margin="10,0,1,2" />
</StackPanel>
</StackPanel>
</DataTemplate>
@@ -57,7 +61,7 @@
</ItemsControl>
</Border>
<Grid Grid.Row="2" Background="#B0000000">
<Grid Grid.Row="2" Background="#E5E7EBCC">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="*" />
@@ -65,43 +69,45 @@
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Button Width="50" Height="70" Margin="10,0,10,0" Classes="icon-yellow" Command="{Binding PreviousCommand}">
<Viewbox Width="18" Height="18">
<Path Fill="Gray" Data="M8 12L14 6V18Z" />
<Button Width="36" Height="36" Margin="10,0,10,0" Classes="icon-neutral" Command="{Binding PreviousCommand}">
<Viewbox Width="12" Height="12">
<Path Fill="#6B7280" Data="M8 12L14 6V18Z" />
</Viewbox>
</Button>
<TextBlock FontSize="15" Foreground="White" Text="Ch" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Button Width="50" Height="70" Margin="10,0,10,0" Classes="icon-yellow" Command="{Binding NextCommand}">
<Viewbox Width="18" Height="18">
<Path Fill="Gray" Data="M16 12L10 18V6Z" />
<TextBlock FontSize="15" Foreground="#111827" Text="Ch" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Button Width="36" Height="36" Margin="10,0,10,0" Classes="icon-neutral" Command="{Binding NextCommand}">
<Viewbox Width="12" Height="12">
<Path Fill="#6B7280" Data="M16 12L10 18V6Z" />
</Viewbox>
</Button>
</StackPanel>
<Button Grid.Column="1"
Background="#0F000000"
Background="#FFFFFF99"
BorderThickness="0"
IsVisible="{Binding IsProgramInfoVisible}"
Command="{Binding ShowProgramListCommand}">
<StackPanel>
<TextBlock FontSize="20" Foreground="White" Text="{Binding ProgramGuideText}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<TextBlock FontSize="20" Foreground="#111827" Text="{Binding ProgramGuideText}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock FontSize="15" Foreground="White" Text="{Binding StartProgram}" />
<ProgressBar Height="10" Foreground="Yellow" Value="{Binding DurationValue}" Maximum="100" Width="400" VerticalAlignment="Center" />
<TextBlock FontSize="15" Foreground="White" Text="{Binding EndProgram}" />
<TextBlock FontSize="15" Foreground="#374151" Text="{Binding StartProgram}" />
<ProgressBar Height="10" Foreground="#0A84FF" Value="{Binding DurationValue}" Maximum="100" Width="400" VerticalAlignment="Center" />
<TextBlock FontSize="15" Foreground="#374151" Text="{Binding EndProgram}" />
</StackPanel>
</StackPanel>
</Button>
</Grid>
</Grid>
<Border Background="#66000000"
<Border Background="#FFFFFFCC"
BorderBrush="#D1D5DB"
BorderThickness="1"
Padding="10"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="10"
IsVisible="{Binding HasPlaybackStatus}">
<TextBlock Text="{Binding PlaybackStatus}" Foreground="White" TextWrapping="Wrap" MaxWidth="420" />
<TextBlock Text="{Binding PlaybackStatus}" Foreground="#111827" TextWrapping="Wrap" MaxWidth="420" />
</Border>
</Grid>
</UserControl>
+414 -16
View File
@@ -1,7 +1,13 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using LibVLCSharp.Shared;
using System.Diagnostics;
using System.ComponentModel;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using TV_Player.AvaloniaApp.ViewModels;
namespace TV_Player.AvaloniaApp.Views;
@@ -10,12 +16,17 @@ public partial class PlayerView : UserControl
{
private LibVLC? _libVlc;
private MediaPlayer? _mediaPlayer;
private Media? _currentMedia;
private PlayerViewModel? _viewModel;
private bool _initialized;
private CancellationTokenSource? _bufferingWatchdogCts;
private string? _activeStreamUrl;
private bool _fallbackAttempted;
public PlayerView()
{
AvaloniaXamlLoader.Load(this);
Log("PlayerView ctor");
DataContextChanged += OnDataContextChanged;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
@@ -26,35 +37,178 @@ public partial class PlayerView : UserControl
if (_initialized)
return;
Core.Initialize();
_libVlc = new LibVLC(enableDebugLogs: true);
_mediaPlayer = new MediaPlayer(_libVlc)
Log("Attach to visual tree -> initializing VLC");
try
{
EnableHardwareDecoding = true
string? macPluginDirectory = null;
if (OperatingSystem.IsMacOS())
{
if (!TryGetMacLibVlcPaths(out var libVlcDirectory, out macPluginDirectory))
{
const string message = "VLC native dependencies not found. Build once with internet to auto-bundle natives/macos (lib + plugins).";
Log(message);
_viewModel?.SetPlaybackStatus(message);
return;
}
if (!string.IsNullOrWhiteSpace(macPluginDirectory) && Directory.Exists(macPluginDirectory))
{
Environment.SetEnvironmentVariable("VLC_PLUGIN_PATH", macPluginDirectory);
Log($"VLC_PLUGIN_PATH set to: {macPluginDirectory}");
}
var dyldLibraryPath = PrependPath(Environment.GetEnvironmentVariable("DYLD_LIBRARY_PATH"), libVlcDirectory);
var dyldFallbackPath = PrependPath(Environment.GetEnvironmentVariable("DYLD_FALLBACK_LIBRARY_PATH"), libVlcDirectory);
Environment.SetEnvironmentVariable("DYLD_LIBRARY_PATH", dyldLibraryPath);
Environment.SetEnvironmentVariable("DYLD_FALLBACK_LIBRARY_PATH", dyldFallbackPath);
Log($"DYLD_LIBRARY_PATH set to: {dyldLibraryPath}");
Log($"DYLD_FALLBACK_LIBRARY_PATH set to: {dyldFallbackPath}");
Log($"Core.Initialize(path) start: {libVlcDirectory}");
Core.Initialize(libVlcDirectory);
Log("Core.Initialize(path) completed");
}
else
{
Log("Core.Initialize() start");
Core.Initialize();
Log("Core.Initialize() completed");
}
_libVlc = new LibVLC(enableDebugLogs: true);
Log("LibVLC instance created");
_mediaPlayer = new MediaPlayer(_libVlc)
{
// Software decode is more reliable for mixed IPTV codecs on macOS.
EnableHardwareDecoding = !OperatingSystem.IsMacOS()
};
Log($"MediaPlayer created. HW decoding enabled: {_mediaPlayer.EnableHardwareDecoding}");
_mediaPlayer.Buffering += OnMediaPlayerBuffering;
_mediaPlayer.Playing += OnMediaPlayerPlaying;
_mediaPlayer.EncounteredError += OnMediaPlayerEncounteredError;
_mediaPlayer.EndReached += OnMediaPlayerEndReached;
_mediaPlayer.Stopped += OnMediaPlayerStopped;
VideoView.MediaPlayer = _mediaPlayer;
_initialized = true;
Log("VideoView.MediaPlayer assigned");
PlayCurrentStream();
}
catch (System.Exception ex)
{
Log($"VLC initialization failed: {ex}");
_viewModel?.SetPlaybackStatus($"VLC init failed: {ex.Message}");
}
}
private static bool TryGetMacLibVlcPaths(out string libDirectory, out string pluginDirectory)
{
var outputBase = AppContext.BaseDirectory;
var candidates = new[]
{
outputBase,
Path.Combine(outputBase, "natives", "macos", "lib"),
Path.Combine(outputBase, "lib"),
Path.Combine(outputBase, "libvlc", "osx-x64", "lib"),
"/Applications/VLC.app/Contents/MacOS/lib",
"/opt/homebrew/lib",
"/usr/local/lib"
};
VideoView.MediaPlayer = _mediaPlayer;
_initialized = true;
PlayCurrentStream();
foreach (var candidateDir in candidates)
{
var libvlc = Path.Combine(candidateDir, "libvlc.dylib");
var libvlccore = Path.Combine(candidateDir, "libvlccore.dylib");
if (File.Exists(libvlc) && File.Exists(libvlccore))
{
var pluginCandidates = new[]
{
Path.Combine(candidateDir, "plugins"),
Path.Combine(Path.GetDirectoryName(candidateDir) ?? string.Empty, "plugins")
};
pluginDirectory = string.Empty;
foreach (var candidatePluginDir in pluginCandidates)
{
if (Directory.Exists(candidatePluginDir))
{
pluginDirectory = candidatePluginDir;
break;
}
}
libDirectory = candidateDir;
Log($"Found macOS VLC libs in: {candidateDir}");
if (!string.IsNullOrWhiteSpace(pluginDirectory))
{
Log($"Found macOS VLC plugins in: {pluginDirectory}");
}
else
{
Log("macOS VLC plugins directory not found next to libraries");
}
return true;
}
}
libDirectory = string.Empty;
pluginDirectory = string.Empty;
Log("macOS VLC libs not found. Checked directories: " + string.Join("; ", candidates));
return false;
}
private static string PrependPath(string? existingPaths, string pathToPrepend)
{
if (string.IsNullOrWhiteSpace(pathToPrepend))
return existingPaths ?? string.Empty;
if (string.IsNullOrWhiteSpace(existingPaths))
return pathToPrepend;
var parts = existingPaths.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Contains(pathToPrepend))
return existingPaths;
return pathToPrepend + ":" + existingPaths;
}
private void OnDetachedFromVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
{
Log("Detach from visual tree -> disposing player resources");
if (_viewModel != null)
{
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
}
if (_mediaPlayer != null)
{
_mediaPlayer.Buffering -= OnMediaPlayerBuffering;
_mediaPlayer.Playing -= OnMediaPlayerPlaying;
_mediaPlayer.EncounteredError -= OnMediaPlayerEncounteredError;
_mediaPlayer.EndReached -= OnMediaPlayerEndReached;
_mediaPlayer.Stopped -= OnMediaPlayerStopped;
}
_mediaPlayer?.Stop();
_bufferingWatchdogCts?.Cancel();
_bufferingWatchdogCts?.Dispose();
_currentMedia?.Dispose();
_mediaPlayer?.Dispose();
_libVlc?.Dispose();
_bufferingWatchdogCts = null;
_currentMedia = null;
_mediaPlayer = null;
_libVlc = null;
_activeStreamUrl = null;
_fallbackAttempted = false;
_initialized = false;
}
private void OnDataContextChanged(object? sender, System.EventArgs e)
{
Log($"DataContext changed. New type: {DataContext?.GetType().Name ?? "null"}");
if (_viewModel != null)
{
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
@@ -73,6 +227,7 @@ public partial class PlayerView : UserControl
{
if (e.PropertyName == nameof(PlayerViewModel.StreamUrl))
{
Log("ViewModel StreamUrl changed -> replaying stream");
PlayCurrentStream();
}
}
@@ -80,30 +235,273 @@ public partial class PlayerView : UserControl
private void PlayCurrentStream()
{
if (!_initialized || _viewModel == null || _mediaPlayer == null || _libVlc == null)
{
Log($"PlayCurrentStream skipped: initialized={_initialized}, hasVm={_viewModel != null}, hasPlayer={_mediaPlayer != null}, hasLibVlc={_libVlc != null}");
return;
}
if (string.IsNullOrWhiteSpace(_viewModel.StreamUrl))
{
Log("PlayCurrentStream aborted: empty StreamUrl");
_viewModel.SetPlaybackStatus("No stream URL available for this channel.");
return;
}
try
{
if (_mediaPlayer.Media != null)
{
_mediaPlayer.Stop();
_mediaPlayer.Media.Dispose();
}
var streamUrl = _viewModel.StreamUrl.Trim();
Log($"PlayCurrentStream start. Url={SanitizeUrlForLog(streamUrl)}");
_activeStreamUrl = streamUrl;
_fallbackAttempted = false;
var media = new Media(_libVlc, new Uri(_viewModel.StreamUrl));
_mediaPlayer.Media = media;
_mediaPlayer.Play();
_viewModel.SetPlaybackStatus("Playing with embedded VLC.");
_mediaPlayer.Stop();
_currentMedia?.Dispose();
_currentMedia = null;
_viewModel.SetPlaybackStatus("Buffering stream...");
StartBufferingWatchdog();
var started = StartPlayback(streamUrl, fallback: false);
Log($"Primary playback start result: {started}");
if (!started)
{
_viewModel.SetPlaybackStatus("Stream failed to start. Try another channel or Open Externally.");
}
}
catch (System.Exception ex)
{
Log($"PlayCurrentStream exception: {ex}");
_viewModel.SetPlaybackStatus($"Embedded playback failed: {ex.Message}. Use Open Externally as a fallback.");
}
}
private static Media BuildMediaFromStreamUrl(LibVLC libVlc, string rawStreamUrl)
{
// Common IPTV format: http://.../stream.m3u8|user-agent=...&referer=...
var splitIndex = rawStreamUrl.IndexOf('|');
if (splitIndex <= 0)
{
return new Media(libVlc, rawStreamUrl, FromType.FromLocation);
}
var baseUrl = rawStreamUrl[..splitIndex].Trim();
var optionsSegment = rawStreamUrl[(splitIndex + 1)..].Trim();
var media = new Media(libVlc, baseUrl, FromType.FromLocation);
if (string.IsNullOrWhiteSpace(optionsSegment))
{
return media;
}
var pairs = optionsSegment.Split('&', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
foreach (var pair in pairs)
{
var equalsIndex = pair.IndexOf('=');
if (equalsIndex <= 0 || equalsIndex == pair.Length - 1)
{
continue;
}
var key = pair[..equalsIndex].Trim().ToLowerInvariant();
var value = HttpUtility.UrlDecode(pair[(equalsIndex + 1)..].Trim());
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
switch (key)
{
case "user-agent":
case "http-user-agent":
media.AddOption($":http-user-agent={value}");
break;
case "referer":
case "referrer":
case "http-referrer":
case "http-referer":
media.AddOption($":http-referrer={value}");
break;
case "origin":
media.AddOption($":http-header=Origin={value}");
break;
case "cookie":
case "cookies":
media.AddOption($":http-header=Cookie={value}");
break;
}
}
return media;
}
private bool StartPlayback(string streamUrl, bool fallback)
{
if (_mediaPlayer == null || _libVlc == null)
return false;
Log($"StartPlayback called. fallback={fallback}, url={SanitizeUrlForLog(streamUrl)}");
_currentMedia?.Dispose();
_currentMedia = BuildMediaFromStreamUrl(_libVlc, streamUrl);
_currentMedia.AddOption(":network-caching=1500");
_currentMedia.AddOption(":live-caching=1500");
_currentMedia.AddOption(":http-reconnect=true");
_currentMedia.AddOption(":codec=avcodec");
if (OperatingSystem.IsMacOS())
{
_currentMedia.AddOption(":avcodec-hw=none");
}
if (fallback)
{
_currentMedia.AddOption(":demux=any");
_currentMedia.AddOption(":hls-segment-threads=4");
_currentMedia.AddOption(":http-continuous=true");
Log("Fallback options applied: demux=any, hls-segment-threads=4, http-continuous=true");
}
var started = _mediaPlayer.Play(_currentMedia);
Log($"MediaPlayer.Play returned: {started}");
return started;
}
private void StartBufferingWatchdog()
{
_bufferingWatchdogCts?.Cancel();
_bufferingWatchdogCts?.Dispose();
_bufferingWatchdogCts = new CancellationTokenSource();
var token = _bufferingWatchdogCts.Token;
Log("Buffering watchdog started (12s)");
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(12), token);
}
catch (TaskCanceledException)
{
return;
}
if (token.IsCancellationRequested)
return;
if (_mediaPlayer == null || _viewModel == null || string.IsNullOrWhiteSpace(_activeStreamUrl))
return;
if (_mediaPlayer.IsPlaying || _fallbackAttempted)
return;
_fallbackAttempted = true;
Log("Buffering watchdog triggered fallback attempt");
Dispatcher.UIThread.Post(() =>
{
if (_viewModel == null)
return;
_viewModel.SetPlaybackStatus("Still buffering, retrying with fallback settings...");
var started = StartPlayback(_activeStreamUrl!, fallback: true);
Log($"Fallback playback start result: {started}");
if (!started)
{
_viewModel.SetPlaybackStatus("Fallback playback failed. Try Open Externally for this channel.");
}
});
}, token);
}
private void OnMediaPlayerBuffering(object? sender, MediaPlayerBufferingEventArgs e)
{
if (_viewModel == null)
return;
if (e.Cache < 1 || ((int)e.Cache % 10) == 0)
{
Log($"MediaPlayer buffering: {e.Cache:0}%");
}
Dispatcher.UIThread.Post(() =>
{
_viewModel.SetPlaybackStatus($"Buffering stream... {e.Cache:0}%");
});
}
private void OnMediaPlayerPlaying(object? sender, System.EventArgs e)
{
if (_viewModel == null)
return;
Dispatcher.UIThread.Post(() =>
{
_bufferingWatchdogCts?.Cancel();
Log("MediaPlayer event: Playing");
_viewModel.SetPlaybackStatus(string.Empty);
});
}
private void OnMediaPlayerEncounteredError(object? sender, System.EventArgs e)
{
if (_viewModel == null)
return;
Dispatcher.UIThread.Post(() =>
{
_bufferingWatchdogCts?.Cancel();
Log("MediaPlayer event: EncounteredError");
_viewModel.SetPlaybackStatus("Playback error. The stream may be unavailable or unsupported.");
});
}
private void OnMediaPlayerEndReached(object? sender, System.EventArgs e)
{
if (_viewModel == null)
return;
Dispatcher.UIThread.Post(() =>
{
_bufferingWatchdogCts?.Cancel();
Log("MediaPlayer event: EndReached");
_viewModel.SetPlaybackStatus("Stream ended.");
});
}
private void OnMediaPlayerStopped(object? sender, System.EventArgs e)
{
if (_viewModel == null)
return;
Dispatcher.UIThread.Post(() =>
{
_bufferingWatchdogCts?.Cancel();
Log("MediaPlayer event: Stopped");
if (string.IsNullOrWhiteSpace(_viewModel.PlaybackStatus))
{
_viewModel.SetPlaybackStatus("Playback stopped.");
}
});
}
private static string SanitizeUrlForLog(string url)
{
if (string.IsNullOrWhiteSpace(url))
return "<empty>";
var trimmed = url.Trim();
var tokenIndex = trimmed.IndexOf("token=", System.StringComparison.OrdinalIgnoreCase);
if (tokenIndex >= 0)
{
return "<url-with-token-redacted>";
}
var splitIndex = trimmed.IndexOf('|');
return splitIndex > 0 ? trimmed[..splitIndex] + "|<headers>" : trimmed;
}
private static void Log(string message)
{
var line = $"[PlayerView] {DateTime.Now:HH:mm:ss.fff} {message}";
Debug.WriteLine(line);
Console.WriteLine(line);
}
}
@@ -19,13 +19,10 @@
Height="70"
Margin="8,6"
Classes="card"
Background="#B0000000"
BorderBrush="Yellow"
BorderThickness="2"
Command="{Binding DataContext.SelectPlaylistCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<TextBlock Text="{Binding Name}"
Foreground="White"
Foreground="#111827"
FontSize="15"
TextWrapping="Wrap"
HorizontalAlignment="Center"
@@ -18,14 +18,11 @@
Height="70"
Margin="8,6"
Classes="card"
Background="#B0000000"
BorderBrush="Yellow"
BorderThickness="2"
Command="{Binding DataContext.SelectGroupCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<Grid RowDefinitions="*,Auto" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}"
Foreground="White"
Foreground="#111827"
FontSize="15"
TextWrapping="Wrap"
HorizontalAlignment="Center"
@@ -34,7 +31,7 @@
Margin="4,0" />
<TextBlock Text="{Binding Count}"
Grid.Row="1"
Foreground="White"
Foreground="#6B7280"
FontSize="10"
HorizontalAlignment="Center"
Margin="0,0,0,2" />
@@ -1,5 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
x:Class="TV_Player.AvaloniaApp.Views.ProgramsListView">
<Grid>
<ListBox ItemsSource="{Binding Programs}"
@@ -11,26 +12,23 @@
ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="12" />
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Button Width="106"
Height="145"
Margin="3"
Margin="5"
Classes="card"
Background="#B0000000"
BorderBrush="Yellow"
BorderThickness="2"
Command="{Binding DataContext.SelectProgramCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}">
<Grid Margin="1" Background="#B0000000" RowDefinitions="*,Auto">
<Image Source="{Binding Logo}" MaxWidth="100" Margin="0,0,0,10" Stretch="Uniform" />
<Grid Margin="1" Background="Transparent" RowDefinitions="*,Auto">
<Image asyncImageLoader:ImageLoader.Source="{Binding Logo}" MaxWidth="100" Margin="0,0,0,10" Stretch="Uniform" />
<TextBlock Grid.Row="1"
Text="{Binding Name}"
FontSize="15"
Foreground="White"
Foreground="#111827"
TextWrapping="Wrap"
TextAlignment="Center"
Margin="1,0,1,2"
+74 -54
View File
@@ -1,67 +1,87 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="TV_Player.AvaloniaApp.Views.SettingsView">
<Viewbox>
<StackPanel VerticalAlignment="Center" Width="900">
<TextBlock Margin="10"
Foreground="White"
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="700" HorizontalAlignment="Center" Margin="24" Spacing="16">
<TextBlock Foreground="#111827"
HorizontalAlignment="Center"
FontSize="25"
FontSize="20"
FontWeight="Bold"
Text="Settings" />
<StackPanel Margin="10" Spacing="8">
<TextBlock Foreground="White" FontSize="22" FontWeight="Bold" Text="Playlist URL" />
<TextBox Text="{Binding PlaylistURL}" />
<TextBlock Foreground="White" FontSize="22" FontWeight="Bold" Text="Playlist Name" />
<TextBox Text="{Binding PlaylistName}" />
<Button Width="70" Height="70" HorizontalAlignment="Center" Classes="icon-green" Command="{Binding AddPlaylistCommand}">
<Viewbox Width="24" Height="24">
<Path Fill="White" Data="M11 5H13V11H19V13H13V19H11V13H5V11H11V5Z" />
<!-- Add Playlist Section -->
<Border Background="#FFFFFF" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="12" Padding="16">
<StackPanel Spacing="10">
<TextBlock Foreground="#374151" FontSize="13" FontWeight="SemiBold" Text="Add Playlist" />
<TextBlock Foreground="#6B7280" FontSize="12" Text="Playlist URL" />
<TextBox Text="{Binding PlaylistURL}" />
<TextBlock Foreground="#6B7280" FontSize="12" Text="Playlist Name" />
<TextBox Text="{Binding PlaylistName}" />
<Button Width="36" Height="36" HorizontalAlignment="Left" Classes="icon-green" Command="{Binding AddPlaylistCommand}">
<Viewbox Width="14" Height="14">
<Path Fill="White" Data="M11 5H13V11H19V13H13V19H11V13H5V11H11V5Z" />
</Viewbox>
</Button>
</StackPanel>
</Border>
<!-- Playlists List -->
<Border Background="#FFFFFF" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="12" Padding="8">
<ListBox ItemsSource="{Binding Playlists}"
Background="Transparent"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemContainerTheme>
<ControlTheme TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Padding" Value="8,4" />
</ControlTheme>
</ListBox.ItemContainerTheme>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch" Height="48" ColumnDefinitions="180,*,48">
<TextBlock Foreground="#111827" VerticalAlignment="Center" FontSize="13" FontWeight="SemiBold" Text="{Binding Key}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" Foreground="#4B5563" VerticalAlignment="Center" FontSize="12" Text="{Binding Value}" TextTrimming="CharacterEllipsis" />
<Button Grid.Column="2"
Width="32" Height="32"
VerticalAlignment="Center"
Classes="icon-red"
CommandParameter="{Binding}"
Command="{Binding DataContext.PlaylistDeleteCommand, RelativeSource={RelativeSource AncestorType=UserControl}}">
<Viewbox Width="12" Height="12">
<Path Fill="White" Data="M5 11H19V13H5Z" />
</Viewbox>
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<!-- Options -->
<Border Background="#FFFFFF" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="12" Padding="16">
<StackPanel Spacing="10">
<TextBlock Foreground="#374151" FontSize="13" FontWeight="SemiBold" Text="Options" />
<CheckBox Foreground="#111827" FontSize="13" IsChecked="{Binding StartFullScreen}" Content="Start fullscreen" />
<CheckBox Foreground="#111827" FontSize="13" IsChecked="{Binding StartLastScreen}" Content="Remember last channel" />
</StackPanel>
</Border>
<!-- Actions -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<Button Width="36" Height="36" Classes="icon-neutral" Command="{Binding BackCommand}">
<Viewbox Width="14" Height="14">
<Path Fill="#374151" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
</Viewbox>
</Button>
<Button Width="36" Height="36" Classes="icon-lightgreen" Command="{Binding SaveCommand}">
<Viewbox Width="14" Height="14">
<Path Fill="White" Data="M9 16.2L4.8 12L3.4 13.4L9 19L21 7L19.6 5.6Z" />
</Viewbox>
</Button>
</StackPanel>
<ListBox Height="250"
ItemsSource="{Binding Playlists}"
Background="Transparent"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch" Height="80" ColumnDefinitions="200,*,100">
<TextBlock Foreground="White" VerticalAlignment="Center" FontSize="25" FontWeight="Bold" Text="{Binding Key}" />
<TextBlock Grid.Column="1" Foreground="White" VerticalAlignment="Center" FontSize="25" FontWeight="Bold" Text="{Binding Value}" />
<Button Grid.Column="2"
Width="70"
Height="70"
Classes="icon-red"
CommandParameter="{Binding}"
Command="{Binding DataContext.PlaylistDeleteCommand, RelativeSource={RelativeSource AncestorType=UserControl}}">
<Viewbox Width="24" Height="24">
<Path Fill="White" Data="M5 11H19V13H5Z" />
</Viewbox>
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<CheckBox Margin="10" Foreground="White" FontSize="25" IsChecked="{Binding StartFullScreen}" Content="Fullscreen" />
<CheckBox Margin="10" Foreground="White" FontSize="25" IsChecked="{Binding StartLastScreen}" Content="Remember last" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Width="70" Height="70" Margin="10,0,50,0" Classes="icon-yellow" Command="{Binding BackCommand}">
<Viewbox Width="24" Height="24">
<Path Fill="Gray" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
</Viewbox>
</Button>
<Button Width="70" Height="70" Classes="icon-lightgreen" Command="{Binding SaveCommand}">
<Viewbox Width="24" Height="24">
<Path Fill="Gray" Data="M9 16.2L4.8 12L3.4 13.4L9 19L21 7L19.6 5.6Z" />
</Viewbox>
</Button>
</StackPanel>
</StackPanel>
</Viewbox>
</ScrollViewer>
</UserControl>