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:
+110
-25
@@ -3,54 +3,139 @@
|
|||||||
xmlns:vm="clr-namespace:TV_Player.AvaloniaApp.ViewModels"
|
xmlns:vm="clr-namespace:TV_Player.AvaloniaApp.ViewModels"
|
||||||
xmlns:views="clr-namespace:TV_Player.AvaloniaApp.Views"
|
xmlns:views="clr-namespace:TV_Player.AvaloniaApp.Views"
|
||||||
x:Class="TV_Player.AvaloniaApp.App"
|
x:Class="TV_Player.AvaloniaApp.App"
|
||||||
RequestedThemeVariant="Dark">
|
RequestedThemeVariant="Default">
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme />
|
<FluentTheme />
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value=".AppleSystemUIFont, -apple-system, Helvetica Neue, Helvetica, Arial" />
|
||||||
|
<Setter Property="Foreground" Value="#1F2937" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button">
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="Padding" Value="12,8" />
|
||||||
|
<Setter Property="Background" Value="#F3F4F6" />
|
||||||
|
<Setter Property="BorderBrush" Value="#D1D5DB" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Foreground" Value="#111827" />
|
||||||
|
<Setter Property="FontFamily" Value=".AppleSystemUIFont, -apple-system, Helvetica Neue, Helvetica, Arial" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button:pointerover">
|
||||||
|
<Setter Property="Background" Value="#E5E7EB" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="Padding" Value="10,8" />
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="BorderBrush" Value="#D1D5DB" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="FontFamily" Value=".AppleSystemUIFont, -apple-system, Helvetica Neue, Helvetica, Arial" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="Foreground" Value="#111827" />
|
||||||
|
<Setter Property="CaretBrush" Value="#111827" />
|
||||||
|
<Setter Property="SelectionForegroundBrush" Value="#111827" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="BorderBrush" Value="#0A84FF" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="Background" Value="White" />
|
||||||
|
<Setter Property="BorderBrush" Value="#0A84FF" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBox">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
</Style>
|
||||||
<Style Selector="Button.card">
|
<Style Selector="Button.card">
|
||||||
<Setter Property="BorderThickness" Value="2" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="BorderBrush" Value="Yellow" />
|
<Setter Property="BorderBrush" Value="#E5E7EB" />
|
||||||
<Setter Property="Background" Value="#B0000000" />
|
<Setter Property="Background" Value="#FFFFFF" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.card /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#FFFFFF" />
|
||||||
|
<Setter Property="BorderBrush" Value="#E5E7EB" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.card:pointerover">
|
<Style Selector="Button.card:pointerover">
|
||||||
<Setter Property="Background" Value="#C0000000" />
|
<Setter Property="Background" Value="#F9FAFB" />
|
||||||
<Setter Property="BorderBrush" Value="#FFF176" />
|
<Setter Property="BorderBrush" Value="#CBD5E1" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.card:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#F9FAFB" />
|
||||||
|
<Setter Property="BorderBrush" Value="#CBD5E1" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.card:pressed">
|
<Style Selector="Button.card:pressed">
|
||||||
<Setter Property="Background" Value="#D0000000" />
|
<Setter Property="Background" Value="#EEF2F7" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.icon-yellow">
|
<Style Selector="Button.card:pressed /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Yellow" />
|
<Setter Property="Background" Value="#EEF2F7" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderBrush" Value="#CBD5E1" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-neutral">
|
||||||
|
<Setter Property="Background" Value="#F3F4F6" />
|
||||||
|
<Setter Property="BorderBrush" Value="#D1D5DB" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="300" />
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
<Setter Property="Width" Value="50" />
|
<Setter Property="Width" Value="36" />
|
||||||
<Setter Property="Height" Value="50" />
|
<Setter Property="Height" Value="36" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.icon-yellow:pointerover">
|
<Style Selector="Button.icon-neutral /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="#FFF176" />
|
<Setter Property="Background" Value="#F3F4F6" />
|
||||||
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-neutral:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#E5E7EB" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.icon-red">
|
<Style Selector="Button.icon-red">
|
||||||
<Setter Property="Background" Value="Red" />
|
<Setter Property="Background" Value="#FF5F57" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="300" />
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
<Setter Property="Width" Value="50" />
|
<Setter Property="Width" Value="36" />
|
||||||
<Setter Property="Height" Value="50" />
|
<Setter Property="Height" Value="36" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.icon-red:pointerover">
|
<Style Selector="Button.icon-red /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="#EF5350" />
|
<Setter Property="Background" Value="#FF5F57" />
|
||||||
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-red:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#E34C43" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.icon-green">
|
<Style Selector="Button.icon-green">
|
||||||
<Setter Property="Background" Value="Green" />
|
<Setter Property="Background" Value="#28C840" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="300" />
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
<Setter Property="Width" Value="50" />
|
<Setter Property="Width" Value="36" />
|
||||||
<Setter Property="Height" Value="50" />
|
<Setter Property="Height" Value="36" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-green /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#28C840" />
|
||||||
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-green:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#20A835" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.icon-lightgreen">
|
<Style Selector="Button.icon-lightgreen">
|
||||||
<Setter Property="Background" Value="LightGreen" />
|
<Setter Property="Background" Value="#0A84FF" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="300" />
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
<Setter Property="Width" Value="50" />
|
<Setter Property="Width" Value="36" />
|
||||||
<Setter Property="Height" Value="50" />
|
<Setter Property="Height" Value="36" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-lightgreen /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#0A84FF" />
|
||||||
|
<Setter Property="CornerRadius" Value="300" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon-lightgreen:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#0973D9" />
|
||||||
</Style>
|
</Style>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
|
|||||||
@@ -10,43 +10,42 @@
|
|||||||
MinHeight="450"
|
MinHeight="450"
|
||||||
Title="TV"
|
Title="TV"
|
||||||
WindowState="{Binding CurrentWindowState}"
|
WindowState="{Binding CurrentWindowState}"
|
||||||
SystemDecorations="None">
|
Background="#EEF1F5">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Grid.Background>
|
<Border IsVisible="{Binding IsTopPanelVisible}" Height="80" Background="#F8F9FB" BorderBrush="#E5E7EB" BorderThickness="0,0,0,1">
|
||||||
<ImageBrush Source="/Assets/bkground.jpg" Stretch="UniformToFill" />
|
|
||||||
</Grid.Background>
|
|
||||||
|
|
||||||
<Grid IsVisible="{Binding IsTopPanelVisible}" Height="80">
|
|
||||||
<Grid ColumnDefinitions="80,80,*,80,80">
|
<Grid ColumnDefinitions="80,80,*,80,80">
|
||||||
<Button Width="70" Height="70" Margin="10,0,0,0" Classes="icon-yellow" Foreground="Gray" Command="{Binding SettingsCommand}">
|
<Button Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Foreground="#6B7280" Command="{Binding SettingsCommand}">
|
||||||
<Viewbox Width="30" Height="30">
|
<Viewbox Width="18" Height="18">
|
||||||
<Path Fill="Gray" Data="M12 8.9A3.1 3.1 0 1 0 12 15.1A3.1 3.1 0 1 0 12 8.9M19.4 13A7.5 7.5 0 0 0 19.5 12A7.5 7.5 0 0 0 19.4 11L21.5 9.4L19.5 5.9L17 6.9A7.2 7.2 0 0 0 15.3 6L14.9 3.3H9.1L8.7 6A7.2 7.2 0 0 0 7 6.9L4.5 5.9L2.5 9.4L4.6 11A7.5 7.5 0 0 0 4.5 12A7.5 7.5 0 0 0 4.6 13L2.5 14.6L4.5 18.1L7 17.1A7.2 7.2 0 0 0 8.7 18L9.1 20.7H14.9L15.3 18A7.2 7.2 0 0 0 17 17.1L19.5 18.1L21.5 14.6L19.4 13Z" />
|
<Path Fill="#6B7280" Data="M12 8.9A3.1 3.1 0 1 0 12 15.1A3.1 3.1 0 1 0 12 8.9M19.4 13A7.5 7.5 0 0 0 19.5 12A7.5 7.5 0 0 0 19.4 11L21.5 9.4L19.5 5.9L17 6.9A7.2 7.2 0 0 0 15.3 6L14.9 3.3H9.1L8.7 6A7.2 7.2 0 0 0 7 6.9L4.5 5.9L2.5 9.4L4.6 11A7.5 7.5 0 0 0 4.5 12A7.5 7.5 0 0 0 4.6 13L2.5 14.6L4.5 18.1L7 17.1A7.2 7.2 0 0 0 8.7 18L9.1 20.7H14.9L15.3 18A7.2 7.2 0 0 0 17 17.1L19.5 18.1L21.5 14.6L19.4 13Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="1" Width="70" Height="70" Margin="10,0,0,0" Classes="icon-yellow" Foreground="Gray" Command="{Binding BackCommand}">
|
<Button Grid.Column="1" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Foreground="#6B7280" Command="{Binding BackCommand}">
|
||||||
<Viewbox Width="26" Height="26">
|
<Viewbox Width="16" Height="16">
|
||||||
<Path Fill="Gray" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
|
<Path Fill="#6B7280" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock Grid.Column="2"
|
<TextBlock Grid.Column="2"
|
||||||
Text="{Binding TopPanelTitle}"
|
Text="{Binding TopPanelTitle}"
|
||||||
FontSize="30"
|
FontSize="22"
|
||||||
|
FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Foreground="White" />
|
Foreground="#111827" />
|
||||||
<Button Grid.Column="3" Width="70" Height="70" Margin="10,0,0,0" Classes="icon-yellow" Foreground="Gray" Command="{Binding FullscreenCommand}">
|
<Button Grid.Column="3" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Foreground="#6B7280" Command="{Binding FullscreenCommand}">
|
||||||
<Viewbox Width="24" Height="24">
|
<Viewbox Width="16" Height="16">
|
||||||
<Path Fill="Gray" Data="M4 9V4H9V6H6V9H4M15 4H20V9H18V6H15V4M20 15V20H15V18H18V15H20M9 20H4V15H6V18H9V20Z" />
|
<Path Fill="#6B7280" Data="M4 9V4H9V6H6V9H4M15 4H20V9H18V6H15V4M20 15V20H15V18H18V15H20M9 20H4V15H6V18H9V20Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="4" Width="70" Height="70" Margin="10,0,0,0" Classes="icon-red" Foreground="White" Command="{Binding CloseAppCommand}">
|
<Button Grid.Column="4" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-red" Foreground="White" Command="{Binding CloseAppCommand}">
|
||||||
<Viewbox Width="24" Height="24">
|
<Viewbox Width="16" Height="16">
|
||||||
<Path Stroke="White" StrokeThickness="2.5" Data="M5,5 L19,19 M19,5 L5,19" />
|
<Path Stroke="White" StrokeThickness="2.5" Data="M5,5 L19,19 M19,5 L5,19" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Border>
|
||||||
|
|
||||||
<ContentControl Grid.Row="1" Content="{Binding CurrentViewModel}" />
|
<Border Grid.Row="1" Margin="12" CornerRadius="16" Background="#FFFFFF" BorderBrush="#E5E7EB" BorderThickness="1">
|
||||||
|
<ContentControl Content="{Binding CurrentViewModel}" Margin="12" />
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.7.0" />
|
||||||
<PackageReference Include="Avalonia" Version="11.2.5" />
|
<PackageReference Include="Avalonia" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
||||||
@@ -18,7 +19,6 @@
|
|||||||
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.6" />
|
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
<PackageReference Include="System.Reactive" Version="6.0.2" />
|
<PackageReference Include="System.Reactive" Version="6.0.2" />
|
||||||
<PackageReference Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1" Condition="$([MSBuild]::IsOSPlatform('macos'))" />
|
|
||||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="$([MSBuild]::IsOSPlatform('windows'))" />
|
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="$([MSBuild]::IsOSPlatform('windows'))" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -29,4 +29,33 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**\*" />
|
<AvaloniaResource Include="Assets\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('macos'))">
|
||||||
|
<None Include="natives/macos/**">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Link>natives/macos/%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="EnsureBundledMacVlc" BeforeTargets="Build" Condition="$([MSBuild]::IsOSPlatform('macos')) and ( !Exists('$(MSBuildProjectDirectory)/natives/macos/lib/libvlccore.dylib') or !Exists('$(MSBuildProjectDirectory)/natives/macos/plugins') )">
|
||||||
|
<Message Text="Bundling VLC native macOS runtime files (one-time download)..." Importance="high" />
|
||||||
|
<Exec Command="sh "$(MSBuildProjectDirectory)/scripts/fetch-vlc-macos.sh" "$(MSBuildProjectDirectory)"" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="CopyBundledMacVlcToOutput" AfterTargets="Build" Condition="$([MSBuild]::IsOSPlatform('macos')) and Exists('$(MSBuildProjectDirectory)/natives/macos/lib/libvlccore.dylib')">
|
||||||
|
<ItemGroup>
|
||||||
|
<BundledMacVlcFiles Include="$(MSBuildProjectDirectory)/natives/macos/**/*" />
|
||||||
|
<BundledMacVlcRootLibs Include="$(MSBuildProjectDirectory)/natives/macos/lib/libvlc.dylib;$(MSBuildProjectDirectory)/natives/macos/lib/libvlccore.dylib" />
|
||||||
|
<BundledMacVlcRootPlugins Include="$(MSBuildProjectDirectory)/natives/macos/plugins/**/*" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Copy SourceFiles="@(BundledMacVlcFiles)"
|
||||||
|
DestinationFiles="@(BundledMacVlcFiles->'$(TargetDir)natives/macos/%(RecursiveDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
<Copy SourceFiles="@(BundledMacVlcRootLibs)"
|
||||||
|
DestinationFiles="@(BundledMacVlcRootLibs->'$(TargetDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
<Copy SourceFiles="@(BundledMacVlcRootPlugins)"
|
||||||
|
DestinationFiles="@(BundledMacVlcRootPlugins->'$(TargetDir)plugins/%(RecursiveDir)%(Filename)%(Extension)')"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,51 +5,55 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<vlc:VideoView x:Name="VideoView" />
|
<vlc:VideoView x:Name="VideoView" />
|
||||||
|
|
||||||
<Grid Background="#01000000">
|
<Grid Background="#00000000">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="80" />
|
<RowDefinition Height="80" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="80" />
|
<RowDefinition Height="80" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid Background="#70000000">
|
<Border Background="#E5E7EBCC" BorderBrush="#D1D5DB" BorderThickness="0,0,0,1">
|
||||||
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="80" />
|
<ColumnDefinition Width="80" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="80" />
|
<ColumnDefinition Width="80" />
|
||||||
<ColumnDefinition Width="80" />
|
<ColumnDefinition Width="80" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Button Width="70" Height="50" Margin="10,0,0,0" Classes="icon-yellow" Command="{Binding BackCommand}">
|
<Button Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Command="{Binding BackCommand}">
|
||||||
<Viewbox Width="22" Height="22">
|
<Viewbox Width="14" Height="14">
|
||||||
<Path Fill="Gray" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
|
<Path Fill="#6B7280" Data="M14.7 5.3L8 12L14.7 18.7L16.1 17.3L10.8 12L16.1 6.7Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock Grid.Column="1" FontSize="20" Foreground="White" Text="{Binding TopPanelTitle}" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="1" FontSize="20" FontWeight="SemiBold" Foreground="#111827" 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}">
|
<Button Grid.Column="2" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-neutral" Command="{Binding FullscreenCommand}">
|
||||||
<Viewbox Width="22" Height="22">
|
<Viewbox Width="14" Height="14">
|
||||||
<Path Fill="Gray" Data="M4 9V4H9V6H6V9H4M15 4H20V9H18V6H15V4M20 15V20H15V18H18V15H20M9 20H4V15H6V18H9V20Z" />
|
<Path Fill="#6B7280" Data="M4 9V4H9V6H6V9H4M15 4H20V9H18V6H15V4M20 15V20H15V18H18V15H20M9 20H4V15H6V18H9V20Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="3" Width="70" Height="70" Margin="10,0,0,0" Classes="icon-red" Command="{Binding CloseAppCommand}">
|
<Button Grid.Column="3" Width="36" Height="36" Margin="16,0,0,0" Classes="icon-red" Command="{Binding CloseAppCommand}">
|
||||||
<Viewbox Width="22" Height="22">
|
<Viewbox Width="14" Height="14">
|
||||||
<Path Stroke="White" StrokeThickness="2.5" Data="M5,5 L19,19 M19,5 L5,19" />
|
<Path Stroke="White" StrokeThickness="2.5" Data="M5,5 L19,19 M19,5 L5,19" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="1"
|
<Border Grid.Row="1"
|
||||||
Width="400"
|
Width="400"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Background="#B0000000"
|
Background="#F8FAFCCC"
|
||||||
|
BorderBrush="#D1D5DB"
|
||||||
|
BorderThickness="0,0,1,0"
|
||||||
IsVisible="{Binding ProgramGuideVisible}">
|
IsVisible="{Binding ProgramGuideVisible}">
|
||||||
<ItemsControl ItemsSource="{Binding Programs}">
|
<ItemsControl ItemsSource="{Binding Programs}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Orientation="Vertical" Margin="6,2">
|
<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">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="{Binding StartTime, StringFormat='{}{0:HH:mm}'}" FontSize="12" Foreground="White" Margin="1,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="White" Margin="10,0,1,2" />
|
<TextBlock Text="{Binding EndTime, StringFormat='{}{0:HH:mm}'}" FontSize="12" Foreground="#6B7280" Margin="10,0,1,2" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@@ -57,7 +61,7 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid Grid.Row="2" Background="#B0000000">
|
<Grid Grid.Row="2" Background="#E5E7EBCC">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="180" />
|
<ColumnDefinition Width="180" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
@@ -65,43 +69,45 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Button Width="50" Height="70" Margin="10,0,10,0" Classes="icon-yellow" Command="{Binding PreviousCommand}">
|
<Button Width="36" Height="36" Margin="10,0,10,0" Classes="icon-neutral" Command="{Binding PreviousCommand}">
|
||||||
<Viewbox Width="18" Height="18">
|
<Viewbox Width="12" Height="12">
|
||||||
<Path Fill="Gray" Data="M8 12L14 6V18Z" />
|
<Path Fill="#6B7280" Data="M8 12L14 6V18Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock FontSize="15" Foreground="White" Text="Ch" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
<TextBlock FontSize="15" Foreground="#111827" Text="Ch" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
<Button Width="50" Height="70" Margin="10,0,10,0" Classes="icon-yellow" Command="{Binding NextCommand}">
|
<Button Width="36" Height="36" Margin="10,0,10,0" Classes="icon-neutral" Command="{Binding NextCommand}">
|
||||||
<Viewbox Width="18" Height="18">
|
<Viewbox Width="12" Height="12">
|
||||||
<Path Fill="Gray" Data="M16 12L10 18V6Z" />
|
<Path Fill="#6B7280" Data="M16 12L10 18V6Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
Background="#0F000000"
|
Background="#FFFFFF99"
|
||||||
BorderThickness="0"
|
BorderThickness="0"
|
||||||
IsVisible="{Binding IsProgramInfoVisible}"
|
IsVisible="{Binding IsProgramInfoVisible}"
|
||||||
Command="{Binding ShowProgramListCommand}">
|
Command="{Binding ShowProgramListCommand}">
|
||||||
<StackPanel>
|
<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">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||||
<TextBlock FontSize="15" Foreground="White" Text="{Binding StartProgram}" />
|
<TextBlock FontSize="15" Foreground="#374151" Text="{Binding StartProgram}" />
|
||||||
<ProgressBar Height="10" Foreground="Yellow" Value="{Binding DurationValue}" Maximum="100" Width="400" VerticalAlignment="Center" />
|
<ProgressBar Height="10" Foreground="#0A84FF" Value="{Binding DurationValue}" Maximum="100" Width="400" VerticalAlignment="Center" />
|
||||||
<TextBlock FontSize="15" Foreground="White" Text="{Binding EndProgram}" />
|
<TextBlock FontSize="15" Foreground="#374151" Text="{Binding EndProgram}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Border Background="#66000000"
|
<Border Background="#FFFFFFCC"
|
||||||
|
BorderBrush="#D1D5DB"
|
||||||
|
BorderThickness="1"
|
||||||
Padding="10"
|
Padding="10"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
IsVisible="{Binding HasPlaybackStatus}">
|
IsVisible="{Binding HasPlaybackStatus}">
|
||||||
<TextBlock Text="{Binding PlaybackStatus}" Foreground="White" TextWrapping="Wrap" MaxWidth="420" />
|
<TextBlock Text="{Binding PlaybackStatus}" Foreground="#111827" TextWrapping="Wrap" MaxWidth="420" />
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Threading;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
using TV_Player.AvaloniaApp.ViewModels;
|
using TV_Player.AvaloniaApp.ViewModels;
|
||||||
|
|
||||||
namespace TV_Player.AvaloniaApp.Views;
|
namespace TV_Player.AvaloniaApp.Views;
|
||||||
@@ -10,12 +16,17 @@ public partial class PlayerView : UserControl
|
|||||||
{
|
{
|
||||||
private LibVLC? _libVlc;
|
private LibVLC? _libVlc;
|
||||||
private MediaPlayer? _mediaPlayer;
|
private MediaPlayer? _mediaPlayer;
|
||||||
|
private Media? _currentMedia;
|
||||||
private PlayerViewModel? _viewModel;
|
private PlayerViewModel? _viewModel;
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
|
private CancellationTokenSource? _bufferingWatchdogCts;
|
||||||
|
private string? _activeStreamUrl;
|
||||||
|
private bool _fallbackAttempted;
|
||||||
|
|
||||||
public PlayerView()
|
public PlayerView()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
Log("PlayerView ctor");
|
||||||
DataContextChanged += OnDataContextChanged;
|
DataContextChanged += OnDataContextChanged;
|
||||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
@@ -26,35 +37,178 @@ public partial class PlayerView : UserControl
|
|||||||
if (_initialized)
|
if (_initialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Core.Initialize();
|
Log("Attach to visual tree -> initializing VLC");
|
||||||
_libVlc = new LibVLC(enableDebugLogs: true);
|
try
|
||||||
_mediaPlayer = new MediaPlayer(_libVlc)
|
|
||||||
{
|
{
|
||||||
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;
|
foreach (var candidateDir in candidates)
|
||||||
_initialized = true;
|
{
|
||||||
PlayCurrentStream();
|
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)
|
private void OnDetachedFromVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
|
Log("Detach from visual tree -> disposing player resources");
|
||||||
if (_viewModel != null)
|
if (_viewModel != null)
|
||||||
{
|
{
|
||||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_mediaPlayer != null)
|
||||||
|
{
|
||||||
|
_mediaPlayer.Buffering -= OnMediaPlayerBuffering;
|
||||||
|
_mediaPlayer.Playing -= OnMediaPlayerPlaying;
|
||||||
|
_mediaPlayer.EncounteredError -= OnMediaPlayerEncounteredError;
|
||||||
|
_mediaPlayer.EndReached -= OnMediaPlayerEndReached;
|
||||||
|
_mediaPlayer.Stopped -= OnMediaPlayerStopped;
|
||||||
|
}
|
||||||
|
|
||||||
_mediaPlayer?.Stop();
|
_mediaPlayer?.Stop();
|
||||||
|
_bufferingWatchdogCts?.Cancel();
|
||||||
|
_bufferingWatchdogCts?.Dispose();
|
||||||
|
_currentMedia?.Dispose();
|
||||||
_mediaPlayer?.Dispose();
|
_mediaPlayer?.Dispose();
|
||||||
_libVlc?.Dispose();
|
_libVlc?.Dispose();
|
||||||
|
_bufferingWatchdogCts = null;
|
||||||
|
_currentMedia = null;
|
||||||
_mediaPlayer = null;
|
_mediaPlayer = null;
|
||||||
_libVlc = null;
|
_libVlc = null;
|
||||||
|
_activeStreamUrl = null;
|
||||||
|
_fallbackAttempted = false;
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDataContextChanged(object? sender, System.EventArgs e)
|
private void OnDataContextChanged(object? sender, System.EventArgs e)
|
||||||
{
|
{
|
||||||
|
Log($"DataContext changed. New type: {DataContext?.GetType().Name ?? "null"}");
|
||||||
if (_viewModel != null)
|
if (_viewModel != null)
|
||||||
{
|
{
|
||||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
@@ -73,6 +227,7 @@ public partial class PlayerView : UserControl
|
|||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(PlayerViewModel.StreamUrl))
|
if (e.PropertyName == nameof(PlayerViewModel.StreamUrl))
|
||||||
{
|
{
|
||||||
|
Log("ViewModel StreamUrl changed -> replaying stream");
|
||||||
PlayCurrentStream();
|
PlayCurrentStream();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,30 +235,273 @@ public partial class PlayerView : UserControl
|
|||||||
private void PlayCurrentStream()
|
private void PlayCurrentStream()
|
||||||
{
|
{
|
||||||
if (!_initialized || _viewModel == null || _mediaPlayer == null || _libVlc == null)
|
if (!_initialized || _viewModel == null || _mediaPlayer == null || _libVlc == null)
|
||||||
|
{
|
||||||
|
Log($"PlayCurrentStream skipped: initialized={_initialized}, hasVm={_viewModel != null}, hasPlayer={_mediaPlayer != null}, hasLibVlc={_libVlc != null}");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_viewModel.StreamUrl))
|
if (string.IsNullOrWhiteSpace(_viewModel.StreamUrl))
|
||||||
{
|
{
|
||||||
|
Log("PlayCurrentStream aborted: empty StreamUrl");
|
||||||
_viewModel.SetPlaybackStatus("No stream URL available for this channel.");
|
_viewModel.SetPlaybackStatus("No stream URL available for this channel.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_mediaPlayer.Media != null)
|
var streamUrl = _viewModel.StreamUrl.Trim();
|
||||||
{
|
Log($"PlayCurrentStream start. Url={SanitizeUrlForLog(streamUrl)}");
|
||||||
_mediaPlayer.Stop();
|
_activeStreamUrl = streamUrl;
|
||||||
_mediaPlayer.Media.Dispose();
|
_fallbackAttempted = false;
|
||||||
}
|
|
||||||
|
|
||||||
var media = new Media(_libVlc, new Uri(_viewModel.StreamUrl));
|
_mediaPlayer.Stop();
|
||||||
_mediaPlayer.Media = media;
|
_currentMedia?.Dispose();
|
||||||
_mediaPlayer.Play();
|
_currentMedia = null;
|
||||||
_viewModel.SetPlaybackStatus("Playing with embedded VLC.");
|
|
||||||
|
_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)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
|
Log($"PlayCurrentStream exception: {ex}");
|
||||||
_viewModel.SetPlaybackStatus($"Embedded playback failed: {ex.Message}. Use Open Externally as a fallback.");
|
_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"
|
Height="70"
|
||||||
Margin="8,6"
|
Margin="8,6"
|
||||||
Classes="card"
|
Classes="card"
|
||||||
Background="#B0000000"
|
|
||||||
BorderBrush="Yellow"
|
|
||||||
BorderThickness="2"
|
|
||||||
Command="{Binding DataContext.SelectPlaylistCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Command="{Binding DataContext.SelectPlaylistCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<TextBlock Text="{Binding Name}"
|
<TextBlock Text="{Binding Name}"
|
||||||
Foreground="White"
|
Foreground="#111827"
|
||||||
FontSize="15"
|
FontSize="15"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
|
|||||||
@@ -18,14 +18,11 @@
|
|||||||
Height="70"
|
Height="70"
|
||||||
Margin="8,6"
|
Margin="8,6"
|
||||||
Classes="card"
|
Classes="card"
|
||||||
Background="#B0000000"
|
|
||||||
BorderBrush="Yellow"
|
|
||||||
BorderThickness="2"
|
|
||||||
Command="{Binding DataContext.SelectGroupCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Command="{Binding DataContext.SelectGroupCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<Grid RowDefinitions="*,Auto" VerticalAlignment="Center" HorizontalAlignment="Stretch">
|
<Grid RowDefinitions="*,Auto" VerticalAlignment="Center" HorizontalAlignment="Stretch">
|
||||||
<TextBlock Text="{Binding Name}"
|
<TextBlock Text="{Binding Name}"
|
||||||
Foreground="White"
|
Foreground="#111827"
|
||||||
FontSize="15"
|
FontSize="15"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
@@ -34,7 +31,7 @@
|
|||||||
Margin="4,0" />
|
Margin="4,0" />
|
||||||
<TextBlock Text="{Binding Count}"
|
<TextBlock Text="{Binding Count}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Foreground="White"
|
Foreground="#6B7280"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Margin="0,0,0,2" />
|
Margin="0,0,0,2" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||||
x:Class="TV_Player.AvaloniaApp.Views.ProgramsListView">
|
x:Class="TV_Player.AvaloniaApp.Views.ProgramsListView">
|
||||||
<Grid>
|
<Grid>
|
||||||
<ListBox ItemsSource="{Binding Programs}"
|
<ListBox ItemsSource="{Binding Programs}"
|
||||||
@@ -11,26 +12,23 @@
|
|||||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<UniformGrid Columns="12" />
|
<WrapPanel Orientation="Horizontal" />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Button Width="106"
|
<Button Width="106"
|
||||||
Height="145"
|
Height="145"
|
||||||
Margin="3"
|
Margin="5"
|
||||||
Classes="card"
|
Classes="card"
|
||||||
Background="#B0000000"
|
|
||||||
BorderBrush="Yellow"
|
|
||||||
BorderThickness="2"
|
|
||||||
Command="{Binding DataContext.SelectProgramCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Command="{Binding DataContext.SelectProgramCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<Grid Margin="1" Background="#B0000000" RowDefinitions="*,Auto">
|
<Grid Margin="1" Background="Transparent" RowDefinitions="*,Auto">
|
||||||
<Image Source="{Binding Logo}" MaxWidth="100" Margin="0,0,0,10" Stretch="Uniform" />
|
<Image asyncImageLoader:ImageLoader.Source="{Binding Logo}" MaxWidth="100" Margin="0,0,0,10" Stretch="Uniform" />
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock Grid.Row="1"
|
||||||
Text="{Binding Name}"
|
Text="{Binding Name}"
|
||||||
FontSize="15"
|
FontSize="15"
|
||||||
Foreground="White"
|
Foreground="#111827"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
Margin="1,0,1,2"
|
Margin="1,0,1,2"
|
||||||
|
|||||||
@@ -1,67 +1,87 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
x:Class="TV_Player.AvaloniaApp.Views.SettingsView">
|
x:Class="TV_Player.AvaloniaApp.Views.SettingsView">
|
||||||
<Viewbox>
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel VerticalAlignment="Center" Width="900">
|
<StackPanel MaxWidth="700" HorizontalAlignment="Center" Margin="24" Spacing="16">
|
||||||
<TextBlock Margin="10"
|
|
||||||
Foreground="White"
|
<TextBlock Foreground="#111827"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
FontSize="25"
|
FontSize="20"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Text="Settings" />
|
Text="Settings" />
|
||||||
|
|
||||||
<StackPanel Margin="10" Spacing="8">
|
<!-- Add Playlist Section -->
|
||||||
<TextBlock Foreground="White" FontSize="22" FontWeight="Bold" Text="Playlist URL" />
|
<Border Background="#FFFFFF" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="12" Padding="16">
|
||||||
<TextBox Text="{Binding PlaylistURL}" />
|
<StackPanel Spacing="10">
|
||||||
<TextBlock Foreground="White" FontSize="22" FontWeight="Bold" Text="Playlist Name" />
|
<TextBlock Foreground="#374151" FontSize="13" FontWeight="SemiBold" Text="Add Playlist" />
|
||||||
<TextBox Text="{Binding PlaylistName}" />
|
<TextBlock Foreground="#6B7280" FontSize="12" Text="Playlist URL" />
|
||||||
<Button Width="70" Height="70" HorizontalAlignment="Center" Classes="icon-green" Command="{Binding AddPlaylistCommand}">
|
<TextBox Text="{Binding PlaylistURL}" />
|
||||||
<Viewbox Width="24" Height="24">
|
<TextBlock Foreground="#6B7280" FontSize="12" Text="Playlist Name" />
|
||||||
<Path Fill="White" Data="M11 5H13V11H19V13H13V19H11V13H5V11H11V5Z" />
|
<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>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</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>
|
</StackPanel>
|
||||||
</Viewbox>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
project_dir="$1"
|
||||||
|
lib_dir="$project_dir/natives/macos/lib"
|
||||||
|
plugins_dir="$project_dir/natives/macos/plugins"
|
||||||
|
|
||||||
|
if [ -f "$lib_dir/libvlccore.dylib" ] && [ -f "$lib_dir/libvlc.dylib" ] && [ -d "$plugins_dir" ]; then
|
||||||
|
echo "[VLC] macOS native libs and plugins already bundled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_dir="$project_dir/obj/vlc-download"
|
||||||
|
mount_point="$download_dir/mount"
|
||||||
|
mkdir -p "$download_dir" "$project_dir/natives/macos"
|
||||||
|
|
||||||
|
arch="$(uname -m)"
|
||||||
|
case "$arch" in
|
||||||
|
arm64|aarch64)
|
||||||
|
dmg_name="vlc-3.0.21-arm64.dmg"
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
dmg_name="vlc-3.0.21-intel64.dmg"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[VLC] Unsupported macOS architecture: $arch"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
url="https://get.videolan.org/vlc/3.0.21/macosx/$dmg_name"
|
||||||
|
dmg_path="$download_dir/$dmg_name"
|
||||||
|
|
||||||
|
if [ ! -f "$dmg_path" ]; then
|
||||||
|
echo "[VLC] Downloading $url"
|
||||||
|
curl -L --fail --retry 3 --retry-delay 2 -o "$dmg_path" "$url"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$mount_point" ]; then
|
||||||
|
hdiutil detach "$mount_point" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
mkdir -p "$mount_point"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
hdiutil detach "$mount_point" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "[VLC] Mounting DMG"
|
||||||
|
hdiutil attach "$dmg_path" -nobrowse -readonly -mountpoint "$mount_point" >/dev/null
|
||||||
|
|
||||||
|
if [ ! -d "$mount_point/VLC.app/Contents/MacOS/lib" ]; then
|
||||||
|
echo "[VLC] Could not find VLC libs inside mounted image"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[VLC] Copying native libraries and plugins"
|
||||||
|
rm -rf "$project_dir/natives/macos/lib"
|
||||||
|
rm -rf "$project_dir/natives/macos/plugins"
|
||||||
|
cp -R "$mount_point/VLC.app/Contents/MacOS/lib" "$project_dir/natives/macos/"
|
||||||
|
if [ -d "$mount_point/VLC.app/Contents/MacOS/plugins" ]; then
|
||||||
|
cp -R "$mount_point/VLC.app/Contents/MacOS/plugins" "$project_dir/natives/macos/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
hdiutil detach "$mount_point" >/dev/null 2>&1 || true
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
echo "[VLC] Bundled macOS native runtime under $project_dir/natives/macos"
|
||||||
Reference in New Issue
Block a user