Web-Sender: WebView2 Browser-Integration (YouTube, Netflix, Mediatheken mit persistentem Login)
This commit is contained in:
parent
ab1954e28c
commit
72bafbcba6
7 changed files with 214 additions and 364 deletions
|
|
@ -1,191 +1,59 @@
|
|||
{
|
||||
"_comment": "Online-Streams für HomeStream. URLs Stand Mai 2026, Quelle: harryshomepage.de + rundfunkforum. Privatsender (RTL, ProSieben, Sat.1) NICHT enthalten wegen Lizenz/Paywall (Joyn, RTL+).",
|
||||
"_comment": "Online-Streams fuer HomeStream. URLs Stand Mai 2026, Quelle: harryshomepage.de + rundfunkforum.",
|
||||
"tv": [
|
||||
{
|
||||
"name": "Das Erste HD",
|
||||
"url": "https://daserste-live.ard-mcdn.de/daserste/live/hls/de/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "ZDF HD",
|
||||
"url": "https://zdf-hls-15.akamaized.net/hls/live/2016498/de/veryhigh/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "3sat HD",
|
||||
"url": "https://zdf-hls-18.akamaized.net/hls/live/2016501/dach/veryhigh/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "ARTE HD",
|
||||
"url": "https://artesimulcast.akamaized.net/hls/live/2030993/artelive_de/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "ARD-alpha HD",
|
||||
"url": "https://mcdn.br.de/br/fs/ard_alpha/hls/de/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "ONE HD",
|
||||
"url": "https://mcdn-one.ard.de/ardone/hls/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "ZDFneo HD",
|
||||
"url": "https://zdf-hls-16.akamaized.net/hls/live/2016499/de/veryhigh/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "ZDFinfo HD",
|
||||
"url": "https://zdf-hls-17.akamaized.net/hls/live/2016500/de/veryhigh/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "Tagesschau24 HD",
|
||||
"url": "https://tagesschau.akamaized.net/hls/live/2020117/tagesschau/tagesschau_3/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "Phoenix HD",
|
||||
"url": "https://zdf-hls-19.akamaized.net/hls/live/2016502/de/veryhigh/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "BR Süd HD",
|
||||
"url": "https://livestreams.br.de/i/bfssued_germany@119890/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "WDR HD",
|
||||
"url": "https://wdr-wdrfernsehen-koeln.icecast.wdr.de/wdr/wdrfernsehen/koeln/hls/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "NDR HD",
|
||||
"url": "https://ndrfs-lh.akamaihd.net/i/ndrfs_nds@430233/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "HR HD",
|
||||
"url": "https://hr-hrfernsehen-live.akamaized.net/hls/live/2016105/hrfernsehen/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "MDR Sachsen HD",
|
||||
"url": "https://mdrsnhls-lh.akamaihd.net/i/livetvmdr_sachsen@439432/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "RBB Berlin HD",
|
||||
"url": "https://rbb_berlin-lh.akamaihd.net/i/rbbBerlin_Live@380294/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "SWR BW HD",
|
||||
"url": "https://swrbw-lh.akamaihd.net/i/swrbw_live@196738/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "SR Fernsehen HD",
|
||||
"url": "https://live2_sr-lh.akamaihd.net/i/sr_universal02@107595/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "KiKA HD",
|
||||
"url": "https://kikageohls.akamaized.net/hls/live/2022693/livetvkika_de/master.m3u8",
|
||||
"kind": "TvHd"
|
||||
},
|
||||
{
|
||||
"name": "Deutsche Welle",
|
||||
"url": "https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8",
|
||||
"kind": "TvHd"
|
||||
}
|
||||
{ "name": "Das Erste HD", "url": "https://daserste-live.ard-mcdn.de/daserste/live/hls/de/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "ZDF HD", "url": "https://zdf-hls-15.akamaized.net/hls/live/2016498/de/veryhigh/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "3sat HD", "url": "https://zdf-hls-18.akamaized.net/hls/live/2016501/dach/veryhigh/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "ARTE HD", "url": "https://artesimulcast.akamaized.net/hls/live/2030993/artelive_de/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "ARD-alpha HD", "url": "https://mcdn.br.de/br/fs/ard_alpha/hls/de/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "ONE HD", "url": "https://mcdn-one.ard.de/ardone/hls/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "ZDFneo HD", "url": "https://zdf-hls-16.akamaized.net/hls/live/2016499/de/veryhigh/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "ZDFinfo HD", "url": "https://zdf-hls-17.akamaized.net/hls/live/2016500/de/veryhigh/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "Tagesschau24 HD", "url": "https://tagesschau.akamaized.net/hls/live/2020117/tagesschau/tagesschau_3/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "Phoenix HD", "url": "https://zdf-hls-19.akamaized.net/hls/live/2016502/de/veryhigh/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "BR Sued HD", "url": "https://livestreams.br.de/i/bfssued_germany@119890/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "WDR HD", "url": "https://wdr-wdrfernsehen-koeln.icecast.wdr.de/wdr/wdrfernsehen/koeln/hls/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "NDR HD", "url": "https://ndrfs-lh.akamaihd.net/i/ndrfs_nds@430233/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "HR HD", "url": "https://hr-hrfernsehen-live.akamaized.net/hls/live/2016105/hrfernsehen/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "MDR Sachsen HD", "url": "https://mdrsnhls-lh.akamaihd.net/i/livetvmdr_sachsen@439432/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "RBB Berlin HD", "url": "https://rbb_berlin-lh.akamaihd.net/i/rbbBerlin_Live@380294/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "SWR BW HD", "url": "https://swrbw-lh.akamaihd.net/i/swrbw_live@196738/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "SR Fernsehen HD", "url": "https://live2_sr-lh.akamaihd.net/i/sr_universal02@107595/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "KiKA HD", "url": "https://kikageohls.akamaized.net/hls/live/2022693/livetvkika_de/master.m3u8", "kind": "TvHd" },
|
||||
{ "name": "Deutsche Welle", "url": "https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8", "kind": "TvHd" }
|
||||
],
|
||||
"radio": [
|
||||
{
|
||||
"name": "Bayern 1",
|
||||
"url": "https://dispatcher.rndfnk.com/br/br1/obb/mp3/high"
|
||||
},
|
||||
{
|
||||
"name": "Bayern 2",
|
||||
"url": "https://dispatcher.rndfnk.com/br/br2/sued/mp3/high"
|
||||
},
|
||||
{
|
||||
"name": "Bayern 3",
|
||||
"url": "https://dispatcher.rndfnk.com/br/br3/live/mp3/high"
|
||||
},
|
||||
{
|
||||
"name": "BR Klassik",
|
||||
"url": "https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high"
|
||||
},
|
||||
{
|
||||
"name": "B5 aktuell",
|
||||
"url": "https://dispatcher.rndfnk.com/br/br5/live/mp3/high"
|
||||
},
|
||||
{
|
||||
"name": "Deutschlandfunk",
|
||||
"url": "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Deutschlandfunk Kultur",
|
||||
"url": "https://st02.sslstream.dlf.de/dlf/02/128/mp3/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Deutschlandfunk Nova",
|
||||
"url": "https://st03.sslstream.dlf.de/dlf/03/128/mp3/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "WDR 2",
|
||||
"url": "https://wdr-wdr2-rheinland.icecastssl.wdr.de/wdr/wdr2/rheinland/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "WDR 4",
|
||||
"url": "https://wdr-wdr4-live.icecastssl.wdr.de/wdr/wdr4/live/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "1LIVE",
|
||||
"url": "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "NDR 2",
|
||||
"url": "https://icecast.ndr.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "N-JOY",
|
||||
"url": "https://icecast.ndr.de/ndr/njoy/live/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "SWR1 BW",
|
||||
"url": "https://liveradio.swr.de/sw282p3/swr1bw/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "SWR3",
|
||||
"url": "https://liveradio.swr.de/sw282p3/swr3/play.mp3"
|
||||
},
|
||||
{
|
||||
"name": "HR1",
|
||||
"url": "https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "HR3",
|
||||
"url": "https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "MDR Sachsen",
|
||||
"url": "https://mdr-284350-0.cast.mdr.de/mdr/284350/0/mp3/high/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "RBB Radio Eins",
|
||||
"url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3"
|
||||
},
|
||||
{
|
||||
"name": "Antenne Bayern",
|
||||
"url": "https://stream.antenne.de/antenne/stream/mp3"
|
||||
},
|
||||
{
|
||||
"name": "Radio Eins",
|
||||
"url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3"
|
||||
}
|
||||
{ "name": "Bayern 1", "url": "https://dispatcher.rndfnk.com/br/br1/obb/mp3/high" },
|
||||
{ "name": "Bayern 2", "url": "https://dispatcher.rndfnk.com/br/br2/sued/mp3/high" },
|
||||
{ "name": "Bayern 3", "url": "https://dispatcher.rndfnk.com/br/br3/live/mp3/high" },
|
||||
{ "name": "BR Klassik", "url": "https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high" },
|
||||
{ "name": "B5 aktuell", "url": "https://dispatcher.rndfnk.com/br/br5/live/mp3/high" },
|
||||
{ "name": "Deutschlandfunk", "url": "https://st01.sslstream.dlf.de/dlf/01/128/mp3/stream.mp3" },
|
||||
{ "name": "Deutschlandfunk Kultur", "url": "https://st02.sslstream.dlf.de/dlf/02/128/mp3/stream.mp3" },
|
||||
{ "name": "Deutschlandfunk Nova", "url": "https://st03.sslstream.dlf.de/dlf/03/128/mp3/stream.mp3" },
|
||||
{ "name": "WDR 2", "url": "https://wdr-wdr2-rheinland.icecastssl.wdr.de/wdr/wdr2/rheinland/mp3/128/stream.mp3" },
|
||||
{ "name": "WDR 4", "url": "https://wdr-wdr4-live.icecastssl.wdr.de/wdr/wdr4/live/mp3/128/stream.mp3" },
|
||||
{ "name": "1LIVE", "url": "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3" },
|
||||
{ "name": "NDR 2", "url": "https://icecast.ndr.de/ndr/ndr2/niedersachsen/mp3/128/stream.mp3" },
|
||||
{ "name": "N-JOY", "url": "https://icecast.ndr.de/ndr/njoy/live/mp3/128/stream.mp3" },
|
||||
{ "name": "SWR1 BW", "url": "https://liveradio.swr.de/sw282p3/swr1bw/play.mp3" },
|
||||
{ "name": "SWR3", "url": "https://liveradio.swr.de/sw282p3/swr3/play.mp3" },
|
||||
{ "name": "HR1", "url": "https://hr-hr1-live.cast.addradio.de/hr/hr1/live/mp3/128/stream.mp3" },
|
||||
{ "name": "HR3", "url": "https://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/128/stream.mp3" },
|
||||
{ "name": "MDR Sachsen", "url": "https://mdr-284350-0.cast.mdr.de/mdr/284350/0/mp3/high/stream.mp3" },
|
||||
{ "name": "RBB Radio Eins", "url": "https://rbb-radioeins-live.cast.addradio.de/rbb/radioeins/live/mp3/128/stream.mp3" },
|
||||
{ "name": "Antenne Bayern", "url": "https://stream.antenne.de/antenne/stream/mp3" }
|
||||
],
|
||||
"web": [
|
||||
{ "name": "YouTube", "url": "https://www.youtube.com" },
|
||||
{ "name": "ARD Mediathek", "url": "https://www.ardmediathek.de" },
|
||||
{ "name": "ZDF Mediathek", "url": "https://www.zdf.de/serien-und-filme" },
|
||||
{ "name": "Netflix", "url": "https://www.netflix.com" },
|
||||
{ "name": "Disney+", "url": "https://www.disneyplus.com" },
|
||||
{ "name": "Joyn", "url": "https://www.joyn.de" },
|
||||
{ "name": "RTL+", "url": "https://plus.rtl.de" },
|
||||
{ "name": "ARTE", "url": "https://www.arte.tv/de" },
|
||||
{ "name": "Apple TV+", "url": "https://tv.apple.com" },
|
||||
{ "name": "Amazon Prime Video", "url": "https://www.amazon.de/gp/video/storefront" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.7.1" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3967.48" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<Window x:Class="FritzTV.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
xmlns:vlc="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF"
|
||||
Title="HomeStream"
|
||||
Height="720" Width="1280"
|
||||
|
|
@ -147,6 +148,8 @@
|
|||
ToolTip="DVB-C-Radio über die FritzBox"/>
|
||||
<Button x:Name="BtnRadioOnline" Content="📡 Radio (Online)" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="radio-online"
|
||||
ToolTip="Webradio (ÖR-Streams)"/>
|
||||
<Button x:Name="BtnWeb" Content="🌐 Web-Sender" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="web"
|
||||
ToolTip="YouTube, Netflix, Mediatheken etc. (Browser)"/>
|
||||
<Separator Margin="8" Background="#333"/>
|
||||
<Button x:Name="BtnFav" Content="⭐ Favoriten" Style="{StaticResource SidebarButton}" Click="BtnCategory_Click" Tag="fav"/>
|
||||
<Separator Margin="8" Background="#333"/>
|
||||
|
|
@ -224,8 +227,9 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- VideoView -->
|
||||
<!-- VideoView + WebView2: beide im gleichen Grid-Cell, Visibility steuert wer sichtbar ist -->
|
||||
<Grid Grid.Row="0">
|
||||
<!-- VLC-Player fuer TV/Radio -->
|
||||
<vlc:VideoView x:Name="VideoView" Background="Black">
|
||||
<Border x:Name="VideoOverlay"
|
||||
Background="#01000000"
|
||||
|
|
@ -350,6 +354,11 @@
|
|||
</Grid>
|
||||
</Border>
|
||||
</vlc:VideoView>
|
||||
|
||||
<!-- WebView2 fuer Web-Sender (YouTube, Netflix, Mediatheken etc.) -->
|
||||
<!-- Persistentes User-Data-Profil: Login bleibt erhalten -->
|
||||
<wv2:WebView2 x:Name="WebView"
|
||||
Visibility="Collapsed"/>
|
||||
<TextBlock x:Name="TxtNoChannel"
|
||||
Text="Wähle einen Sender aus der Liste"
|
||||
Foreground="#666" FontSize="18"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Windows.Threading;
|
|||
using FritzTV.Models;
|
||||
using FritzTV.Services;
|
||||
using LibVLCSharp.Shared;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace FritzTV;
|
||||
|
||||
|
|
@ -37,6 +38,9 @@ public partial class MainWindow : Window
|
|||
// Periodischer Refresh: aktualisiert Jetzt/Danach wenn Sendung endet
|
||||
private readonly DispatcherTimer _epgRefreshTimer;
|
||||
|
||||
// WebView2 für Web-Sender (YouTube, Netflix etc.)
|
||||
private bool _webViewReady = false;
|
||||
|
||||
// Pre-Fullscreen UI-State
|
||||
private WindowStyle _prevStyle = WindowStyle.SingleBorderWindow;
|
||||
private WindowState _prevState = WindowState.Normal;
|
||||
|
|
@ -66,10 +70,8 @@ public partial class MainWindow : Window
|
|||
_epgRefreshTimer.Tick += (_, _) =>
|
||||
{
|
||||
if (_currentChannel == null) return;
|
||||
// EIT alle 10s neu lesen (FritzBox-TV)
|
||||
if (_currentChannel.Source == ChannelSource.FritzBox && _currentMedia != null)
|
||||
UpdateEpgFromMedia(_currentMedia);
|
||||
// XMLTV nur alle 60s (Online-TV + Danach für alle)
|
||||
_epgRefreshTick++;
|
||||
if (_epgRefreshTick >= 6)
|
||||
{
|
||||
|
|
@ -86,7 +88,6 @@ public partial class MainWindow : Window
|
|||
|
||||
private void OnVideoDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// Doppelklick auf VideoView → Vollbild
|
||||
ToggleFullscreen();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
|
@ -104,16 +105,35 @@ public partial class MainWindow : Window
|
|||
|
||||
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Core.Initialize lädt libVLC-Natives (~5-10s) und blockiert sonst den UI-Thread.
|
||||
// Volume vorab auf UI-Thread lesen, dann Natives auf Background-Thread initialisieren.
|
||||
var initialVolume = (int)SldVolume.Value;
|
||||
await Task.Run(() => InitializePlayerNatives(initialVolume));
|
||||
VideoView.MediaPlayer = _player; // UI-Zuweisung auf UI-Thread
|
||||
VideoView.MediaPlayer = _player;
|
||||
|
||||
// WebView2 async initialisieren (persistentes Profil in %APPDATA%\HomeStream\webview2)
|
||||
_ = InitializeWebViewAsync();
|
||||
|
||||
await LoadChannelsAsync();
|
||||
RestoreLastChannel();
|
||||
_ = LoadEpgInBackgroundAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeWebViewAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var profileDir = AppPaths.WebView2Profile;
|
||||
Directory.CreateDirectory(profileDir);
|
||||
var env = await CoreWebView2Environment.CreateAsync(null, profileDir);
|
||||
await WebView.EnsureCoreWebView2Async(env);
|
||||
_webViewReady = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// WebView2 nicht verfügbar — Web-Sender werden deaktiviert
|
||||
System.Diagnostics.Debug.WriteLine($"WebView2 init failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Background-Thread: libVLC-Natives laden. Kein UI-Zugriff.</summary>
|
||||
private void InitializePlayerNatives(int initialVolume)
|
||||
{
|
||||
|
|
@ -172,7 +192,6 @@ public partial class MainWindow : Window
|
|||
TxtStatus.Text = "Lade Senderliste…";
|
||||
try
|
||||
{
|
||||
// FritzBox + Online-Sender parallel laden
|
||||
var fritzClient = new FritzBoxClient(_settings.FritzBoxIp);
|
||||
var onlineClient = new OnlineSourceClient();
|
||||
|
||||
|
|
@ -188,7 +207,6 @@ public partial class MainWindow : Window
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// FritzBox nicht erreichbar (z.B. unterwegs)? Trotzdem mit Online-Sendern weitermachen
|
||||
fritzChannels = new List<Channel>();
|
||||
TxtStatus.Text = $"FritzBox-Fehler: {ex.Message}";
|
||||
}
|
||||
|
|
@ -223,7 +241,6 @@ public partial class MainWindow : Window
|
|||
|
||||
private async Task LoadLogosInBackgroundAsync()
|
||||
{
|
||||
// Parallel laden, aber begrenzt (max 6 gleichzeitig) damit AVM nicht zum DDoS wird
|
||||
using var sem = new SemaphoreSlim(6);
|
||||
var tasks = _allChannels
|
||||
.Where(c => c.LogoPath == null)
|
||||
|
|
@ -233,7 +250,6 @@ public partial class MainWindow : Window
|
|||
try
|
||||
{
|
||||
var path = await _logoService.GetLogoPathAsync(ch.Name);
|
||||
// Property-Change läuft über INotifyPropertyChanged → UI updated
|
||||
await Dispatcher.InvokeAsync(() => ch.LogoPath = path);
|
||||
}
|
||||
finally { sem.Release(); }
|
||||
|
|
@ -278,36 +294,31 @@ public partial class MainWindow : Window
|
|||
{
|
||||
IEnumerable<Channel> q = _allChannels;
|
||||
|
||||
// Kategorie-Filter
|
||||
q = _currentCategory switch
|
||||
{
|
||||
"tv-fritz" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Source == ChannelSource.FritzBox)),
|
||||
"tv-online" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Source == ChannelSource.Online)),
|
||||
"tv-fritz" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web && c.Source == ChannelSource.FritzBox)),
|
||||
"tv-online" => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web && c.Source == ChannelSource.Online)),
|
||||
"radio-fritz" => q.Where(c => c.Kind == ChannelKind.Radio && c.Source == ChannelSource.FritzBox),
|
||||
"radio-online" => q.Where(c => c.Kind == ChannelKind.Radio && c.Source == ChannelSource.Online),
|
||||
"web" => q.Where(c => c.Kind == ChannelKind.Web),
|
||||
"fav" => MergeHdSd(q.Where(c => c.IsFavorite)),
|
||||
_ => MergeHdSd(q) // "all"
|
||||
_ => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Web)) // "all" ohne Web
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(_searchTerm))
|
||||
q = q.Where(c => c.Name.Contains(_searchTerm, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Alle Listen alphabetisch
|
||||
q = q.OrderBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_filteredChannels.Clear();
|
||||
foreach (var c in q) _filteredChannels.Add(c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HD + SD zusammenfassen: gleicher Basisname → HD-Variante bevorzugen.
|
||||
/// Radio-Sender werden nicht angefasst.
|
||||
/// </summary>
|
||||
private static IEnumerable<Channel> MergeHdSd(IEnumerable<Channel> source)
|
||||
{
|
||||
var list = source.ToList();
|
||||
var radio = list.Where(c => c.Kind == ChannelKind.Radio);
|
||||
var tv = list.Where(c => c.Kind != ChannelKind.Radio);
|
||||
var tv = list.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web);
|
||||
|
||||
var merged = tv
|
||||
.GroupBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase)
|
||||
|
|
@ -316,9 +327,6 @@ public partial class MainWindow : Window
|
|||
return merged.Concat(radio);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sendername normalisieren für Sort & Merge: " HD"-Suffix weg, klein, getrimmt.
|
||||
/// </summary>
|
||||
private static string NormalizeName(string name)
|
||||
{
|
||||
var s = name.Trim();
|
||||
|
|
@ -335,7 +343,6 @@ public partial class MainWindow : Window
|
|||
{
|
||||
if (LstChannels.SelectedItem is Channel ch)
|
||||
{
|
||||
// Debounce: schnelle ↑/↓-Navigation soll nicht jeden Sender starten
|
||||
_pendingChannel = ch;
|
||||
_zapTimer.Stop();
|
||||
_zapTimer.Start();
|
||||
|
|
@ -344,17 +351,31 @@ public partial class MainWindow : Window
|
|||
|
||||
private void PlayChannel(Channel ch)
|
||||
{
|
||||
_currentChannel = ch;
|
||||
TxtCurrentChannel.Text = ch.Name;
|
||||
TxtChannelSource.Text = ch.Source == ChannelSource.Online ? "● Online" : "● FritzBox";
|
||||
TxtChannelSource.Foreground = ch.Source == ChannelSource.Online
|
||||
? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x00, 0x78, 0xD4))
|
||||
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x66, 0xBB, 0x6A));
|
||||
TxtNoChannel.Visibility = Visibility.Collapsed;
|
||||
UpdateFavButton();
|
||||
|
||||
if (ch.Kind == ChannelKind.Web)
|
||||
{
|
||||
PlayWebChannel(ch);
|
||||
return;
|
||||
}
|
||||
|
||||
// VLC-Player aktiv, WebView verstecken
|
||||
WebView.Visibility = Visibility.Collapsed;
|
||||
VideoView.Visibility = Visibility.Visible;
|
||||
|
||||
if (_libVLC == null || _player == null) return;
|
||||
try
|
||||
{
|
||||
_currentChannel = ch;
|
||||
|
||||
// Radio-Cover (zeigt Logo + Sendername statt weißes Bild)
|
||||
UpdateRadioCover(ch);
|
||||
|
||||
// KEIN Stop() — libVLC ersetzt Media bei laufendem Player ohne Frame-Lücke.
|
||||
var oldMedia = _currentMedia;
|
||||
|
||||
var media = new Media(_libVLC, new Uri(ch.Url));
|
||||
media.MetaChanged += OnMediaMetaChanged;
|
||||
|
||||
|
|
@ -362,24 +383,14 @@ public partial class MainWindow : Window
|
|||
_player.Media = media;
|
||||
_player.Play();
|
||||
|
||||
TxtCurrentChannel.Text = ch.Name;
|
||||
// Quelle-Badge anzeigen (FritzBox/Online)
|
||||
TxtChannelSource.Text = ch.Source == ChannelSource.Online ? "● Online" : "● FritzBox";
|
||||
TxtChannelSource.Foreground = ch.Source == ChannelSource.Online
|
||||
? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x00, 0x78, 0xD4)) // Blau für Online
|
||||
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x66, 0xBB, 0x6A)); // Grün für FritzBox
|
||||
TxtEpgNow.Text = "EPG wird geladen…";
|
||||
TxtEpgNext.Text = "";
|
||||
TxtNoChannel.Visibility = Visibility.Collapsed;
|
||||
UpdateFavButton();
|
||||
|
||||
_epgTimer.Stop();
|
||||
_epgTimer.Tag = (DateTime.Now, ch);
|
||||
_epgTimer.Start();
|
||||
|
||||
try { oldMedia?.Dispose(); } catch { }
|
||||
|
||||
// "Danach"-Liste aus XMLTV-EPG vorbefüllen (überlebt bis EIT-Daten kommen)
|
||||
UpdateNextFromEpgService(ch);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -388,17 +399,42 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Füllt Jetzt+Danach aus dem XMLTV-EPG.
|
||||
///
|
||||
/// Online-Streams (HLS) haben kein EIT, daher müssen wir hier auch das AKTUELLE
|
||||
/// Programm aus XMLTV setzen. FritzBox-TV nutzt primär EIT, XMLTV nur als "Danach"-Vorschau.
|
||||
/// </summary>
|
||||
private void PlayWebChannel(Channel ch)
|
||||
{
|
||||
// VLC stoppen, WebView zeigen
|
||||
_player?.Stop();
|
||||
VideoView.Visibility = Visibility.Collapsed;
|
||||
RadioCover.Visibility = Visibility.Collapsed;
|
||||
WebView.Visibility = Visibility.Visible;
|
||||
|
||||
TxtEpgNow.Text = "🌐 Browser";
|
||||
TxtEpgNext.Text = "";
|
||||
TxtChannelSource.Text = "● Web";
|
||||
TxtChannelSource.Foreground = new System.Windows.Media.SolidColorBrush(
|
||||
System.Windows.Media.Color.FromRgb(0xFF, 0x99, 0x00)); // Orange für Web
|
||||
|
||||
if (_webViewReady)
|
||||
{
|
||||
WebView.Source = new Uri(ch.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
// WebView noch nicht bereit — kurz warten und retry
|
||||
var retryTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
retryTimer.Tick += (_, _) =>
|
||||
{
|
||||
retryTimer.Stop();
|
||||
if (_webViewReady && _currentChannel?.Kind == ChannelKind.Web)
|
||||
WebView.Source = new Uri(ch.Url);
|
||||
};
|
||||
retryTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNextFromEpgService(Channel ch)
|
||||
{
|
||||
if (ch.Kind == ChannelKind.Radio) return; // Radio: kein TV-EPG
|
||||
if (ch.Kind == ChannelKind.Radio || ch.Kind == ChannelKind.Web) return;
|
||||
|
||||
// EPG noch nicht geladen? Hintergrund-Load anstossen, kommt später wieder
|
||||
if (!_epgService.IsCurrent)
|
||||
{
|
||||
_ = LoadEpgInBackgroundAsync();
|
||||
|
|
@ -407,41 +443,34 @@ public partial class MainWindow : Window
|
|||
|
||||
var allEvents = _epgService.GetEvents(ch.Name, hoursAhead: 12);
|
||||
|
||||
// Aktuelle Sendung aus XMLTV: nur für Online-TV setzen (FritzBox: EIT überschreibt)
|
||||
if (ch.Source == ChannelSource.Online)
|
||||
{
|
||||
var current = allEvents.FirstOrDefault(e => e.IsCurrent);
|
||||
if (current != null)
|
||||
{
|
||||
var jetzt = current.Title;
|
||||
if (!string.IsNullOrWhiteSpace(current.Description))
|
||||
jetzt += $" · {current.Description.Trim()}";
|
||||
TxtEpgNow.Text = $"▶ Jetzt: {jetzt}";
|
||||
_epgTimer.Stop(); // keine EIT-Updates erwarten
|
||||
TxtEpgNow.Text = $"▶ Jetzt: {current.Title}";
|
||||
_epgTimer.Stop();
|
||||
}
|
||||
else if (TxtEpgNow.Text == "EPG wird geladen\u2026")
|
||||
{
|
||||
TxtEpgNow.Text = "(kein EPG verf\u00fcgbar)";
|
||||
TxtEpgNow.Text = "(kein EPG verfügbar)";
|
||||
_epgTimer.Stop();
|
||||
}
|
||||
}
|
||||
else if (ch.Source == ChannelSource.FritzBox)
|
||||
{
|
||||
var current = allEvents.FirstOrDefault(e => e.IsCurrent);
|
||||
if (current != null && (TxtEpgNow.Text == "EPG wird geladen\u2026" || TxtEpgNow.Text == "(kein EPG verfügbar)"))
|
||||
TxtEpgNow.Text = $"▶ Jetzt: {current.Title}";
|
||||
}
|
||||
|
||||
// Danach-Liste (gleich für beide Quellen)
|
||||
var next = allEvents.Where(e => e.StartTime > DateTime.Now).Take(3).ToList();
|
||||
if (next.Count > 0)
|
||||
{
|
||||
TxtEpgNext.Text = string.Join(" · ",
|
||||
next.Select(e => $"{e.StartTime:HH:mm} {e.Title}"));
|
||||
}
|
||||
TxtEpgNext.Text = string.Join(" · ", next.Select(e => $"{e.StartTime:HH:mm} {e.Title}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bei Radio-Sendern Cover-Bild anzeigen, bei TV verbergen.
|
||||
/// libVLC zeigt sonst bei reinen Audio-Streams einen weißen Default-Hintergrund.
|
||||
/// </summary>
|
||||
private void UpdateRadioCover(Channel ch)
|
||||
{
|
||||
// Bisher gebundener Channel abhängen damit wir nicht doppelt hören
|
||||
if (_radioCoverBoundChannel != null)
|
||||
_radioCoverBoundChannel.PropertyChanged -= OnCurrentChannelPropertyChanged;
|
||||
_radioCoverBoundChannel = null;
|
||||
|
|
@ -452,8 +481,6 @@ public partial class MainWindow : Window
|
|||
TxtRadioName.Text = ch.Name;
|
||||
TxtRadioText.Text = "";
|
||||
ApplyRadioLogo(ch);
|
||||
|
||||
// Anhören falls Logo erst später (async) geladen wird
|
||||
ch.PropertyChanged += OnCurrentChannelPropertyChanged;
|
||||
_radioCoverBoundChannel = ch;
|
||||
}
|
||||
|
|
@ -491,7 +518,7 @@ public partial class MainWindow : Window
|
|||
RadioFallbackIcon.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
catch { /* fallthrough zu Fallback-Icon */ }
|
||||
catch { }
|
||||
}
|
||||
RadioLogo.Source = null;
|
||||
RadioLogo.Visibility = Visibility.Collapsed;
|
||||
|
|
@ -506,20 +533,17 @@ public partial class MainWindow : Window
|
|||
private void EpgTimer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
if (_currentMedia == null) { _epgTimer.Stop(); return; }
|
||||
|
||||
// Online-Streams haben kein EIT — EpgService liefert Daten, EIT-Polling unnötig
|
||||
if (_currentChannel?.Source == ChannelSource.Online)
|
||||
if (_currentChannel?.Source == ChannelSource.Online || _currentChannel?.Kind == ChannelKind.Web)
|
||||
{
|
||||
_epgTimer.Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_epgTimer.Tag is (DateTime started, Channel ch))
|
||||
{
|
||||
if ((DateTime.Now - started).TotalSeconds > 30)
|
||||
{
|
||||
_epgTimer.Stop();
|
||||
if (TxtEpgNow.Text == "EPG wird geladen…")
|
||||
if (TxtEpgNow.Text == "EPG wird geladen\u2026")
|
||||
TxtEpgNow.Text = "(kein EPG verfügbar)";
|
||||
return;
|
||||
}
|
||||
|
|
@ -531,19 +555,12 @@ public partial class MainWindow : Window
|
|||
{
|
||||
try
|
||||
{
|
||||
// EIT-Daten (TV) oder RDS-Daten (Radio) aus dem Stream:
|
||||
// NowPlaying → TV: aktuelle Sendung; Radio: "Künstler - Titel"
|
||||
// ShowName → TV: manchmal Titel; Radio: meist Sendername
|
||||
// Description → TV: Untertitel
|
||||
// Title → Radio: Track-Titel (RDS RadioText+)
|
||||
// Artist → Radio: Künstler
|
||||
var nowPlaying = m.Meta(MetadataType.NowPlaying) ?? "";
|
||||
var showName = m.Meta(MetadataType.ShowName) ?? "";
|
||||
var description = m.Meta(MetadataType.Description) ?? "";
|
||||
var title = m.Meta(MetadataType.Title) ?? "";
|
||||
var artist = m.Meta(MetadataType.Artist) ?? "";
|
||||
|
||||
// URLs ausfiltern (libVLC packt manchmal die Stream-URL rein)
|
||||
if (description.StartsWith("rtsp:", StringComparison.OrdinalIgnoreCase) ||
|
||||
description.StartsWith("http:", StringComparison.OrdinalIgnoreCase))
|
||||
description = "";
|
||||
|
|
@ -555,14 +572,6 @@ public partial class MainWindow : Window
|
|||
|
||||
if (isRadio)
|
||||
{
|
||||
// Radio-RDS: verschiedene Sender befüllen unterschiedliche Felder.
|
||||
// Reihenfolge der Versuche:
|
||||
// 1. Artist + Title kombiniert (sauberster Fall)
|
||||
// 2. NowPlaying (oft "Künstler - Titel")
|
||||
// 3. Title alleine
|
||||
// 4. Artist alleine
|
||||
// 5. Description (manche Sender packen RDS-Text dort rein)
|
||||
// 6. ShowName (Fallback)
|
||||
string nowText = "";
|
||||
if (!string.IsNullOrWhiteSpace(artist) && !string.IsNullOrWhiteSpace(title))
|
||||
nowText = $"{artist.Trim()} – {title.Trim()}";
|
||||
|
|
@ -587,7 +596,6 @@ public partial class MainWindow : Window
|
|||
}
|
||||
else
|
||||
{
|
||||
// TV: EIT NowPlaying / ShowName
|
||||
string nowText = !string.IsNullOrWhiteSpace(nowPlaying) ? nowPlaying : showName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(nowText))
|
||||
|
|
@ -597,15 +605,12 @@ public partial class MainWindow : Window
|
|||
jetzt += $" · {description.Trim()}";
|
||||
TxtEpgNow.Text = $"▶ Jetzt: {jetzt}";
|
||||
|
||||
// "Danach" lassen wir aus dem XMLTV-EPG (3 Sendungen) — das ist informativer
|
||||
// als die einzelne EIT-ShowName-Angabe. Nur fallback wenn EPG-Service leer ist.
|
||||
if (string.IsNullOrEmpty(TxtEpgNext.Text))
|
||||
{
|
||||
var next = (!string.IsNullOrWhiteSpace(showName) && showName != nowPlaying)
|
||||
? showName : "";
|
||||
TxtEpgNext.Text = string.IsNullOrWhiteSpace(next) ? "" : $"⏭ Danach: {next.Trim()}";
|
||||
}
|
||||
|
||||
_epgTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
|
@ -696,7 +701,6 @@ public partial class MainWindow : Window
|
|||
}
|
||||
else
|
||||
{
|
||||
// ItemTemplate-Refresh ohne Stream-Reset
|
||||
if (LstChannels.ItemContainerGenerator.ContainerFromItem(_currentChannel)
|
||||
is ListBoxItem lbi)
|
||||
{
|
||||
|
|
@ -735,7 +739,7 @@ public partial class MainWindow : Window
|
|||
{
|
||||
if (_currentChannel == null)
|
||||
{
|
||||
MessageBox.Show("Erst einen Sender auswählen.", "FRITZ!TV");
|
||||
MessageBox.Show("Erst einen Sender auswählen.", "HomeStream");
|
||||
return;
|
||||
}
|
||||
var w = new EpgChannelWindow(_epgService, _currentChannel.Name) { Owner = this };
|
||||
|
|
@ -754,12 +758,11 @@ public partial class MainWindow : Window
|
|||
|
||||
// ────────── EPG-Overlay (Joyn-Style) ──────────
|
||||
|
||||
// Layout-Konstanten fürs Grid
|
||||
private const double EpgPxPerMin = 4.0; // 4 px/min → 240 px/h
|
||||
private const double EpgChannelColWidth = 200; // Senderspalte links (Logo + Name)
|
||||
private const double EpgRowHeight = 64; // jede Senderzeile
|
||||
private const double EpgHeaderHeight = 40; // Zeit-Header oben
|
||||
private const int EpgTotalHours = 8; // 8 Stunden sichtbar/scrollbar
|
||||
private const double EpgPxPerMin = 4.0;
|
||||
private const double EpgChannelColWidth = 200;
|
||||
private const double EpgRowHeight = 64;
|
||||
private const double EpgHeaderHeight = 40;
|
||||
private const int EpgTotalHours = 8;
|
||||
|
||||
private DateTime _epgStartTime;
|
||||
|
||||
|
|
@ -797,7 +800,7 @@ public partial class MainWindow : Window
|
|||
EpgCanvas.Children.Clear();
|
||||
|
||||
var channels = _allChannels
|
||||
.Where(c => c.Kind != ChannelKind.Radio)
|
||||
.Where(c => c.Kind != ChannelKind.Radio && c.Kind != ChannelKind.Web)
|
||||
.GroupBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.FirstOrDefault(c => c.Kind == ChannelKind.TvHd) ?? g.First())
|
||||
.OrderBy(c => NormalizeName(c.Name), StringComparer.OrdinalIgnoreCase)
|
||||
|
|
@ -814,7 +817,6 @@ public partial class MainWindow : Window
|
|||
BuildEpgHeader(totalMinutes, contentWidth, contentHeight);
|
||||
BuildEpgNowLine(totalMinutes, contentHeight);
|
||||
|
||||
// Zeilen in Batches einf\u00fcgen damit UI-Thread nicht h\u00e4ngt (82 Sender w\u00e4ren >5s)
|
||||
const int batchSize = 10;
|
||||
for (int i = 0; i < channels.Count; i++)
|
||||
{
|
||||
|
|
@ -830,28 +832,22 @@ public partial class MainWindow : Window
|
|||
|
||||
private void BuildEpgHeader(double totalMinutes, double contentWidth, double contentHeight)
|
||||
{
|
||||
// Header-Hintergrund
|
||||
var bg = new System.Windows.Shapes.Rectangle
|
||||
{
|
||||
Width = contentWidth,
|
||||
Height = EpgHeaderHeight,
|
||||
Width = contentWidth, Height = EpgHeaderHeight,
|
||||
Fill = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x10, 0x10, 0x10))
|
||||
};
|
||||
Canvas.SetLeft(bg, 0); Canvas.SetTop(bg, 0);
|
||||
EpgCanvas.Children.Add(bg);
|
||||
|
||||
// Stunden-Marker + vertikale Trennlinien
|
||||
for (int h = 0; h <= EpgTotalHours; h++)
|
||||
{
|
||||
var t = _epgStartTime.AddHours(h);
|
||||
var x = EpgChannelColWidth + h * 60 * EpgPxPerMin;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = t.ToString("HH:mm"),
|
||||
Foreground = System.Windows.Media.Brushes.White,
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold
|
||||
Text = t.ToString("HH:mm"), Foreground = System.Windows.Media.Brushes.White,
|
||||
FontSize = 13, FontWeight = FontWeights.SemiBold
|
||||
};
|
||||
Canvas.SetLeft(label, x + 6); Canvas.SetTop(label, 11);
|
||||
EpgCanvas.Children.Add(label);
|
||||
|
|
@ -886,11 +882,9 @@ public partial class MainWindow : Window
|
|||
{
|
||||
var y = EpgHeaderHeight + rowIndex * EpgRowHeight;
|
||||
|
||||
// Sender-Spalte links: Logo + Name
|
||||
var nameCell = new Border
|
||||
{
|
||||
Width = EpgChannelColWidth,
|
||||
Height = EpgRowHeight,
|
||||
Width = EpgChannelColWidth, Height = EpgRowHeight,
|
||||
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x16, 0x16, 0x16)),
|
||||
BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x22, 0x22, 0x22)),
|
||||
BorderThickness = new Thickness(0, 0, 1, 1)
|
||||
|
|
@ -913,10 +907,8 @@ public partial class MainWindow : Window
|
|||
}
|
||||
var nameTb = new TextBlock
|
||||
{
|
||||
Text = ch.Name,
|
||||
Foreground = System.Windows.Media.Brushes.White,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Text = ch.Name, Foreground = System.Windows.Media.Brushes.White,
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Padding = new Thickness(8, 0, 8, 0),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
|
|
@ -927,17 +919,14 @@ public partial class MainWindow : Window
|
|||
Canvas.SetLeft(nameCell, 0); Canvas.SetTop(nameCell, y);
|
||||
EpgCanvas.Children.Add(nameCell);
|
||||
|
||||
// Zeilen-Hintergrund (rechts der Sendernamen)
|
||||
var rowBg = new System.Windows.Shapes.Rectangle
|
||||
{
|
||||
Width = contentWidth - EpgChannelColWidth,
|
||||
Height = EpgRowHeight,
|
||||
Width = contentWidth - EpgChannelColWidth, Height = EpgRowHeight,
|
||||
Fill = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x1A, 0x1A, 0x1A))
|
||||
};
|
||||
Canvas.SetLeft(rowBg, EpgChannelColWidth); Canvas.SetTop(rowBg, y);
|
||||
EpgCanvas.Children.Add(rowBg);
|
||||
|
||||
// Events für diesen Sender
|
||||
var events = _epgService.GetEvents(ch.Name, hoursAhead: EpgTotalHours);
|
||||
var totalMinutes = EpgTotalHours * 60;
|
||||
foreach (var ev in events)
|
||||
|
|
@ -962,91 +951,63 @@ public partial class MainWindow : Window
|
|||
{
|
||||
var isCurrent = ev.IsCurrent;
|
||||
var bgColor = isCurrent
|
||||
? System.Windows.Media.Color.FromRgb(0x9E, 0x1B, 0x32) // Joyn-Rot für aktuell
|
||||
? System.Windows.Media.Color.FromRgb(0x9E, 0x1B, 0x32)
|
||||
: System.Windows.Media.Color.FromRgb(0x2A, 0x2A, 0x2A);
|
||||
var borderColor = isCurrent
|
||||
? System.Windows.Media.Color.FromRgb(0xC9, 0x29, 0x42)
|
||||
: System.Windows.Media.Color.FromRgb(0x44, 0x44, 0x44);
|
||||
|
||||
var content = new StackPanel { Margin = new Thickness(8, 6, 8, 6) };
|
||||
var timeLabel = new TextBlock
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{ev.StartTime:HH:mm} – {ev.EndTime:HH:mm}",
|
||||
Foreground = isCurrent ? System.Windows.Media.Brushes.White
|
||||
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xAA, 0xAA, 0xAA)),
|
||||
FontSize = 10,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
};
|
||||
var titleLabel = new TextBlock
|
||||
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xAA, 0xAA, 0xAA)),
|
||||
FontSize = 10, TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = ev.Title,
|
||||
Foreground = System.Windows.Media.Brushes.White,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Text = ev.Title, Foreground = System.Windows.Media.Brushes.White,
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxHeight = 36
|
||||
};
|
||||
content.Children.Add(timeLabel);
|
||||
content.Children.Add(titleLabel);
|
||||
TextWrapping = TextWrapping.Wrap, MaxHeight = 36
|
||||
});
|
||||
|
||||
var box = new Border
|
||||
{
|
||||
Width = width,
|
||||
Height = EpgRowHeight - 4,
|
||||
Width = width, Height = EpgRowHeight - 4,
|
||||
Background = new System.Windows.Media.SolidColorBrush(bgColor),
|
||||
BorderBrush = new System.Windows.Media.SolidColorBrush(borderColor),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(3),
|
||||
Cursor = Cursors.Hand,
|
||||
ToolTip = $"{ev.StartTime:HH:mm}–{ev.EndTime:HH:mm}\n{ev.Title}" +
|
||||
(string.IsNullOrWhiteSpace(ev.Description) ? "" : $"\n\n{ev.Description}"),
|
||||
Tag = ch,
|
||||
Child = content
|
||||
Tag = ch, Child = content
|
||||
};
|
||||
|
||||
// Hover-Effekt
|
||||
box.MouseEnter += (_, _) =>
|
||||
{
|
||||
var hoverColor = isCurrent
|
||||
? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A)
|
||||
: System.Windows.Media.Color.FromRgb(0x36, 0x36, 0x36);
|
||||
box.Background = new System.Windows.Media.SolidColorBrush(hoverColor);
|
||||
};
|
||||
box.MouseLeave += (_, _) =>
|
||||
box.Background = new System.Windows.Media.SolidColorBrush(bgColor);
|
||||
box.MouseEnter += (_, _) => box.Background = new System.Windows.Media.SolidColorBrush(
|
||||
isCurrent ? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A)
|
||||
: System.Windows.Media.Color.FromRgb(0x36, 0x36, 0x36));
|
||||
box.MouseLeave += (_, _) => box.Background = new System.Windows.Media.SolidColorBrush(bgColor);
|
||||
|
||||
// Click → Sender wechseln + Overlay schließen
|
||||
box.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
// Channel-Match: gleicher normalisierter Name in der Senderliste
|
||||
var target = _allChannels.FirstOrDefault(c =>
|
||||
NormalizeName(c.Name).Equals(NormalizeName(ch.Name), StringComparison.OrdinalIgnoreCase))
|
||||
?? ch;
|
||||
|
||||
NormalizeName(c.Name).Equals(NormalizeName(ch.Name), StringComparison.OrdinalIgnoreCase)) ?? ch;
|
||||
HideEpgOverlay();
|
||||
|
||||
// Falls Sender schon selektiert ist, feuert SelectionChanged nicht
|
||||
// → Direkt PlayChannel aufrufen. Sonst über Listbox damit Highlight passt.
|
||||
var listEntry = _filteredChannels.FirstOrDefault(c =>
|
||||
NormalizeName(c.Name).Equals(NormalizeName(ch.Name), StringComparison.OrdinalIgnoreCase));
|
||||
if (listEntry != null && LstChannels.SelectedItem != listEntry)
|
||||
{
|
||||
LstChannels.SelectedItem = listEntry;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Direkt abspielen (Sender war schon selektiert ODER nicht in der gefilterten Liste)
|
||||
PlayChannel(target);
|
||||
}
|
||||
};
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
// ────────── Sidebar (zwei separate Toggles für Kategorien und Liste) ──────────
|
||||
// ────────── Sidebar ──────────
|
||||
|
||||
// Beide Bereiche unabhängig: Kategorien zu/auf, Liste zu/auf
|
||||
private bool _categoriesCollapsed;
|
||||
private bool _listCollapsed;
|
||||
|
||||
|
|
@ -1062,9 +1023,6 @@ public partial class MainWindow : Window
|
|||
ApplySidebarLayout();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strg+B / globaler Burger: öffnet beide wenn beide zu sind, sonst schließt beide.
|
||||
/// </summary>
|
||||
private void BtnSidebarToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_categoriesCollapsed && _listCollapsed)
|
||||
|
|
@ -1084,12 +1042,8 @@ public partial class MainWindow : Window
|
|||
{
|
||||
SidebarColumn.Width = _categoriesCollapsed ? new GridLength(0) : new GridLength(180);
|
||||
ChannelsColumn.Width = _listCollapsed ? new GridLength(0) : new GridLength(280);
|
||||
|
||||
// Burger-Toggle (im Player-Bereich) nur sichtbar wenn beide Spalten zu sind
|
||||
BtnSidebarToggleOverlay.Visibility = (_categoriesCollapsed && _listCollapsed)
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// Internes Flag für Fullscreen-Restore
|
||||
_sidebarHidden = _categoriesCollapsed && _listCollapsed;
|
||||
}
|
||||
|
||||
|
|
@ -1099,7 +1053,6 @@ public partial class MainWindow : Window
|
|||
{
|
||||
if (Keyboard.FocusedElement is TextBox) return;
|
||||
|
||||
// Strg+B: Sidebar/Liste komplett toggle
|
||||
if (e.Key == Key.B && Keyboard.Modifiers == ModifierKeys.Control)
|
||||
{
|
||||
BtnSidebarToggle_Click(this, new RoutedEventArgs());
|
||||
|
|
@ -1127,4 +1080,3 @@ public partial class MainWindow : Window
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using System.Runtime.CompilerServices;
|
|||
|
||||
namespace FritzTV.Models;
|
||||
|
||||
public enum ChannelKind { TvSd, TvHd, Radio }
|
||||
public enum ChannelKind { TvSd, TvHd, Radio, Web }
|
||||
|
||||
/// <summary>Woher kommt der Sender? FritzBox (DVB-C/lokal) oder Online (HLS-Stream)</summary>
|
||||
public enum ChannelSource { FritzBox, Online }
|
||||
|
|
|
|||
|
|
@ -35,4 +35,5 @@ public static class AppPaths
|
|||
public static string Logos => Path.Combine(Root, "logos");
|
||||
public static string Epg => Path.Combine(Root, "epg");
|
||||
public static string CrashLog => Path.Combine(Root, "crash.log");
|
||||
public static string WebView2Profile => Path.Combine(Root, "webview2");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,25 @@ public class OnlineSourceClient
|
|||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("web", out var webArr))
|
||||
{
|
||||
foreach (var item in webArr.EnumerateArray())
|
||||
{
|
||||
var name = item.GetProperty("name").GetString();
|
||||
var url = item.GetProperty("url").GetString();
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url)) continue;
|
||||
|
||||
channels.Add(new Channel
|
||||
{
|
||||
Name = name,
|
||||
Url = url,
|
||||
Kind = ChannelKind.Web,
|
||||
Number = number++,
|
||||
Source = ChannelSource.Online
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(channels);
|
||||
}
|
||||
catch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue