Compare commits

..

No commits in common. "main" and "v0.1.2" have entirely different histories.
main ... v0.1.2

11 changed files with 62 additions and 763 deletions

View file

@ -47,14 +47,13 @@
"web": [ "web": [
{ "name": "YouTube", "url": "https://www.youtube.com" }, { "name": "YouTube", "url": "https://www.youtube.com" },
{ "name": "ARD Mediathek", "url": "https://www.ardmediathek.de" }, { "name": "ARD Mediathek", "url": "https://www.ardmediathek.de" },
{ "name": "ZDF Mediathek", "url": "https://www.zdf.de/" }, { "name": "ZDF Mediathek", "url": "https://www.zdf.de/serien-und-filme" },
{ "name": "Netflix", "url": "https://www.netflix.com" }, { "name": "Netflix", "url": "https://www.netflix.com" },
{ "name": "Disney+", "url": "https://www.disneyplus.com" }, { "name": "Disney+", "url": "https://www.disneyplus.com" },
{ "name": "Joyn", "url": "https://www.joyn.de" }, { "name": "Joyn", "url": "https://www.joyn.de" },
{ "name": "RTL+", "url": "https://plus.rtl.de" }, { "name": "RTL+", "url": "https://plus.rtl.de" },
{ "name": "ARTE", "url": "https://www.arte.tv/de" }, { "name": "ARTE", "url": "https://www.arte.tv/de" },
{ "name": "Apple TV+", "url": "https://tv.apple.com" }, { "name": "Apple TV+", "url": "https://tv.apple.com" },
{ "name": "Amazon Prime Video", "url": "https://www.amazon.de/gp/video/storefront" }, { "name": "Amazon Prime Video", "url": "https://www.amazon.de/gp/video/storefront" }
{ "name": "n-tv", "url": "https://www.n-tv.de" }
] ]
} }

View file

@ -1,16 +0,0 @@
<Window x:Class="FritzTV.ClockWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
ShowInTaskbar="False"
IsHitTestVisible="False"
Topmost="False"
SizeToContent="WidthAndHeight">
<Border Background="#AA000000" CornerRadius="8" Padding="20,10">
<TextBlock x:Name="TxtClock"
Foreground="White" FontSize="48" FontWeight="Light"
FontFamily="Consolas"/>
</Border>
</Window>

View file

@ -1,13 +0,0 @@
using System.Windows;
namespace FritzTV;
public partial class ClockWindow : Window
{
public ClockWindow()
{
InitializeComponent();
}
public void UpdateTime(string time) => TxtClock.Text = time;
}

View file

@ -227,50 +227,41 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- VideoView + WebView2 + EPG-Overlay: alle im gleichen Grid-Cell --> <!-- VideoView + WebView2: beide im gleichen Grid-Cell, Visibility steuert wer sichtbar ist -->
<Grid Grid.Row="0"> <Grid Grid.Row="0">
<!-- VLC-Player fuer TV/Radio --> <!-- VLC-Player fuer TV/Radio -->
<vlc:VideoView x:Name="VideoView" Background="Black"> <vlc:VideoView x:Name="VideoView" Background="Black">
<Border x:Name="VideoOverlay" <Border x:Name="VideoOverlay"
Background="#01000000" Background="#01000000"
PreviewMouseLeftButtonDown="VideoClickCatcher_DoubleClick"> PreviewMouseLeftButtonDown="VideoClickCatcher_DoubleClick">
<Grid> <Grid>
<!-- Digitale Uhr (optional, Strg+U) -->
<Border x:Name="ClockOverlay" Visibility="Collapsed"
HorizontalAlignment="Right" VerticalAlignment="Top"
Background="#AA000000" CornerRadius="8"
Padding="20,10" Margin="16"
IsHitTestVisible="False">
<TextBlock x:Name="TxtClock"
Foreground="White" FontSize="48" FontWeight="Light"
FontFamily="Consolas"/>
</Border>
<!-- Radio-Cover bei Audio-Streams --> <!-- Radio-Cover bei Audio-Streams — INNERHALB VideoView damit es die native HWND überdeckt -->
<Border x:Name="RadioCover" Visibility="Collapsed" Background="#1A1A1A"> <Border x:Name="RadioCover" Visibility="Collapsed"
Background="#1A1A1A">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<Image x:Name="RadioLogo" Width="200" Height="200" Stretch="Uniform" <Image x:Name="RadioLogo" Width="200" Height="200" Stretch="Uniform"
RenderOptions.BitmapScalingMode="HighQuality"/> RenderOptions.BitmapScalingMode="HighQuality"/>
<TextBlock x:Name="RadioFallbackIcon" Text="📻" <TextBlock x:Name="RadioFallbackIcon" Text="📻"
FontSize="120" Foreground="#444" FontSize="120" Foreground="#444"
HorizontalAlignment="Center" Visibility="Collapsed"/> HorizontalAlignment="Center"
Visibility="Collapsed"/>
<TextBlock x:Name="TxtRadioName" <TextBlock x:Name="TxtRadioName"
FontSize="24" FontWeight="Bold" Foreground="White" FontSize="24" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" Margin="0,24,0,0"/> HorizontalAlignment="Center" Margin="0,24,0,0"/>
<TextBlock x:Name="TxtRadioText" <TextBlock x:Name="TxtRadioText"
FontSize="16" Foreground="#0078D4" FontWeight="SemiBold" FontSize="16" Foreground="#0078D4" FontWeight="SemiBold"
HorizontalAlignment="Center" Margin="24,12,24,0" HorizontalAlignment="Center" Margin="24,12,24,0"
TextWrapping="Wrap" TextAlignment="Center" MaxWidth="600"/> TextWrapping="Wrap" TextAlignment="Center"
MaxWidth="600"/>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- EPG-Overlay: INNERHALB VideoView (HWND-Grenze) <!-- EPG-Overlay (Joyn-Style): liegt INNERHALB der VideoView damit Klicks ber HWND funktionieren -->
VideoView wird beim EPG-Oeffnen immer sichtbar gemacht,
auch wenn WebView aktiv war -->
<Border x:Name="EpgOverlay" Visibility="Collapsed" <Border x:Name="EpgOverlay" Visibility="Collapsed"
Background="#D8000000"> Background="#D8000000">
<DockPanel> <DockPanel>
<!-- Header: Programm + Datum/Zeit + Schließen -->
<Grid DockPanel.Dock="Top" Background="#0A0A0A" Height="60"> <Grid DockPanel.Dock="Top" Background="#0A0A0A" Height="60">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
@ -286,11 +277,20 @@
<Button x:Name="BtnCloseEpg" Content="✕" FontSize="18" <Button x:Name="BtnCloseEpg" Content="✕" FontSize="18"
Width="40" Height="40" Width="40" Height="40"
Background="Transparent" Foreground="White" BorderThickness="0" Background="Transparent" Foreground="White" BorderThickness="0"
Cursor="Hand" Click="BtnCloseEpg_Click" ToolTip="Schließen (Esc)"/> Cursor="Hand"
Click="BtnCloseEpg_Click"
ToolTip="Schließen (Esc)"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- Status-Zeile (Lade EPG… etc.) -->
<TextBlock x:Name="TxtEpgOverlayStatus" DockPanel.Dock="Bottom" <TextBlock x:Name="TxtEpgOverlayStatus" DockPanel.Dock="Bottom"
Foreground="#888" FontSize="11" Padding="24,8" Background="#0A0A0A"/> Foreground="#888" FontSize="11" Padding="24,8"
Background="#0A0A0A"/>
<!-- Scrollbares EPG-Grid (Canvas) -->
<!-- ScrollBar-Style explizit hier weil vlc:VideoView eine eigene HWND hat
und Window.Resources-Styles dort nicht greifen -->
<ScrollViewer x:Name="EpgScrollViewer" <ScrollViewer x:Name="EpgScrollViewer"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
@ -346,9 +346,14 @@
</Border> </Border>
</vlc:VideoView> </vlc:VideoView>
<!-- WebView2 fuer Web-Sender --> <!-- WebView2 fuer Web-Sender (YouTube, Netflix, Mediatheken etc.) -->
<wv2:WebView2 x:Name="WebView" Visibility="Collapsed" Panel.ZIndex="5"/> <!-- Persistentes User-Data-Profil: Login bleibt erhalten -->
<!-- Liegt VOR dem VideoView im Z-Order, damit Klicks nicht vom VideoOverlay gefangen werden -->
<wv2:WebView2 x:Name="WebView"
Visibility="Collapsed"
Panel.ZIndex="5"/>
<!-- Kein Burger-Button mehr - Strg+B zum Einblenden der Seitenleiste -->
<TextBlock x:Name="TxtNoChannel" <TextBlock x:Name="TxtNoChannel"
Text="Wähle einen Sender aus der Liste" Text="Wähle einen Sender aus der Liste"
Foreground="#666" FontSize="18" Foreground="#666" FontSize="18"
@ -392,12 +397,6 @@
<Slider x:Name="SldVolume" Width="100" Minimum="0" Maximum="100" Value="80" <Slider x:Name="SldVolume" Width="100" Minimum="0" Maximum="100" Value="80"
VerticalAlignment="Center" Margin="8,0" VerticalAlignment="Center" Margin="8,0"
ValueChanged="SldVolume_ValueChanged"/> ValueChanged="SldVolume_ValueChanged"/>
<Button x:Name="BtnClock" Content="🕒" FontSize="14" Width="40" Height="40"
Background="Transparent" Foreground="#888" BorderThickness="0"
Click="BtnClock_Click" Cursor="Hand" ToolTip="Uhr einblenden (Strg+U)"/>
<Button x:Name="BtnAlwaysOnTop" Content="📌" FontSize="14" Width="40" Height="40"
Background="Transparent" Foreground="#888" BorderThickness="0"
Click="BtnAlwaysOnTop_Click" Cursor="Hand" ToolTip="Immer im Vordergrund (Strg+T)"/>
<Button x:Name="BtnFullscreen" Content="⛶" FontSize="16" Width="40" Height="40" <Button x:Name="BtnFullscreen" Content="⛶" FontSize="16" Width="40" Height="40"
Background="Transparent" Foreground="White" BorderThickness="0" Background="Transparent" Foreground="White" BorderThickness="0"
Click="BtnFullscreen_Click" Cursor="Hand" ToolTip="Vollbild (F11)"/> Click="BtnFullscreen_Click" Cursor="Hand" ToolTip="Vollbild (F11)"/>

View file

@ -19,12 +19,11 @@ public partial class MainWindow : Window
private Media? _currentMedia; private Media? _currentMedia;
private AppSettings _settings = AppSettings.Load(); private AppSettings _settings = AppSettings.Load();
private readonly EpgService _epgService = new(); private readonly EpgService _epgService = new();
private readonly ReminderService _reminderService = new();
private readonly LogoService _logoService = new(); private readonly LogoService _logoService = new();
private readonly ObservableCollection<Channel> _allChannels = new(); private readonly ObservableCollection<Channel> _allChannels = new();
private readonly ObservableCollection<Channel> _filteredChannels = new(); private readonly ObservableCollection<Channel> _filteredChannels = new();
private string _currentCategory = "fav"; // wird in MainWindow_Loaded aus Settings gesetzt private string _currentCategory = "all";
private string _searchTerm = ""; private string _searchTerm = "";
private Channel? _currentChannel; private Channel? _currentChannel;
private double _volumeBeforeMute = 80; private double _volumeBeforeMute = 80;
@ -42,11 +41,6 @@ public partial class MainWindow : Window
// WebView2 für Web-Sender (YouTube, Netflix etc.) // WebView2 für Web-Sender (YouTube, Netflix etc.)
private bool _webViewReady = false; private bool _webViewReady = false;
// Uhr
private readonly DispatcherTimer _clockTimer;
private bool _clockVisible = false;
private ClockWindow? _clockWindow;
// WndProc-Hook für globale Tastatureingaben (auch wenn WebView2 Fokus hat) // WndProc-Hook für globale Tastatureingaben (auch wenn WebView2 Fokus hat)
private System.Windows.Interop.HwndSource? _hwndSource; private System.Windows.Interop.HwndSource? _hwndSource;
@ -62,7 +56,6 @@ public partial class MainWindow : Window
InitializeComponent(); InitializeComponent();
LstChannels.ItemsSource = _filteredChannels; LstChannels.ItemsSource = _filteredChannels;
TxtFritzBox.Text = $"FritzBox: {_settings.FritzBoxIp}"; TxtFritzBox.Text = $"FritzBox: {_settings.FritzBoxIp}";
_currentCategory = _settings.StartCategory;
SldVolume.Value = _settings.Volume; SldVolume.Value = _settings.Volume;
DarkTitleBar.EnableFor(this); DarkTitleBar.EnableFor(this);
@ -73,46 +66,28 @@ public partial class MainWindow : Window
_epgTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; _epgTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_epgTimer.Tick += EpgTimer_Tick; _epgTimer.Tick += EpgTimer_Tick;
// Jede 60s: XMLTV Jetzt/Danach refreshen + EIT neu lesen // Jede 10s: EIT-Daten aus Media neu lesen (libVLC feuert MetaChanged nicht bei laufendem EIT-Update)
_epgRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(60) }; // Jede 60s: XMLTV Jetzt/Danach refreshen
_epgRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(10) };
var _epgRefreshTick = 0;
_epgRefreshTimer.Tick += (_, _) => _epgRefreshTimer.Tick += (_, _) =>
{ {
if (_currentChannel == null) return; if (_currentChannel == null) return;
// EIT auf Background-Thread lesen damit UI nicht blockiert
if (_currentChannel.Source == ChannelSource.FritzBox && _currentMedia != null) if (_currentChannel.Source == ChannelSource.FritzBox && _currentMedia != null)
UpdateEpgFromMedia(_currentMedia);
_epgRefreshTick++;
if (_epgRefreshTick >= 6)
{ {
var media = _currentMedia; _epgRefreshTick = 0;
Task.Run(() =>
{
try
{
var nowPlaying = media.Meta(MetadataType.NowPlaying) ?? "";
var showName = media.Meta(MetadataType.ShowName) ?? "";
if (!string.IsNullOrWhiteSpace(nowPlaying) || !string.IsNullOrWhiteSpace(showName))
Dispatcher.BeginInvoke(() => UpdateEpgFromMedia(media));
}
catch { }
});
}
UpdateNextFromEpgService(_currentChannel); UpdateNextFromEpgService(_currentChannel);
}
}; };
_epgRefreshTimer.Start(); _epgRefreshTimer.Start();
// Uhr-Timer (jede Sekunde)
_clockTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_clockTimer.Tick += (_, _) =>
{
var time = DateTime.Now.ToString("HH:mm:ss");
TxtClock.Text = time;
_clockWindow?.UpdateTime(time);
// Nur neu positionieren wenn Hauptfenster bewegt wurde - nicht jede Sekunde
};
Loaded += MainWindow_Loaded; Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing; Closing += MainWindow_Closing;
KeyDown += MainWindow_KeyDown; KeyDown += MainWindow_KeyDown;
LocationChanged += (_, _) => PositionClockWindow(); PreviewKeyDown += MainWindow_KeyDown;
SizeChanged += (_, _) => PositionClockWindow();
} }
private void OnVideoDoubleClick(object sender, MouseButtonEventArgs e) private void OnVideoDoubleClick(object sender, MouseButtonEventArgs e)
@ -147,22 +122,6 @@ public partial class MainWindow : Window
_ = InitializeWebViewAsync(); _ = InitializeWebViewAsync();
await LoadChannelsAsync(); await LoadChannelsAsync();
// Erinnerungs-Callback: Sender automatisch einschalten
_reminderService.SwitchToChannel = channelName =>
{
var ch = _allChannels.FirstOrDefault(c =>
c.Name.Equals(channelName, StringComparison.OrdinalIgnoreCase));
if (ch != null)
{
HideEpgOverlay();
var listEntry = _filteredChannels.FirstOrDefault(c =>
c.Name.Equals(channelName, StringComparison.OrdinalIgnoreCase));
if (listEntry != null)
LstChannels.SelectedItem = listEntry;
else
PlayChannel(ch);
}
};
RestoreLastChannel(); RestoreLastChannel();
_ = LoadEpgInBackgroundAsync(); _ = LoadEpgInBackgroundAsync();
} }
@ -225,8 +184,6 @@ public partial class MainWindow : Window
_epgTimer.Stop(); _epgTimer.Stop();
_zapTimer.Stop(); _zapTimer.Stop();
_epgRefreshTimer.Stop(); _epgRefreshTimer.Stop();
_clockTimer.Stop();
_clockWindow?.Close();
_player?.Stop(); _player?.Stop();
_settings.LastChannel = _currentChannel?.Name ?? ""; _settings.LastChannel = _currentChannel?.Name ?? "";
_settings.Volume = SldVolume.Value; _settings.Volume = SldVolume.Value;
@ -354,7 +311,7 @@ public partial class MainWindow : Window
"radio-fritz" => q.Where(c => c.Kind == ChannelKind.Radio && c.Source == ChannelSource.FritzBox), "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), "radio-online" => q.Where(c => c.Kind == ChannelKind.Radio && c.Source == ChannelSource.Online),
"web" => q.Where(c => c.Kind == ChannelKind.Web), "web" => q.Where(c => c.Kind == ChannelKind.Web),
"fav" => q.Where(c => c.IsFavorite), // kein MergeHdSd - Web-Sender wuerden sonst rausgefiltert "fav" => MergeHdSd(q.Where(c => c.IsFavorite)),
_ => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Web)) // "all" ohne Web _ => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Web)) // "all" ohne Web
}; };
@ -410,8 +367,6 @@ public partial class MainWindow : Window
TxtChannelSource.Foreground = ch.Source == ChannelSource.Online 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(0x00, 0x78, 0xD4))
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x66, 0xBB, 0x6A)); : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x66, 0xBB, 0x6A));
// Aufnahme stoppen wenn Sender wechselt
// if (_isRecording) StopRecording();
TxtNoChannel.Visibility = Visibility.Collapsed; TxtNoChannel.Visibility = Visibility.Collapsed;
UpdateFavButton(); UpdateFavButton();
@ -427,14 +382,6 @@ public partial class MainWindow : Window
WebView.Visibility = Visibility.Collapsed; WebView.Visibility = Visibility.Collapsed;
VideoView.Visibility = Visibility.Visible; VideoView.Visibility = Visibility.Visible;
VideoOverlay.Visibility = Visibility.Visible; VideoOverlay.Visibility = Visibility.Visible;
// Uhr: separates Fenster -> WPF-Overlay wenn WebView deaktiviert wird
if (_clockVisible && _clockWindow != null)
{
_clockWindow.Close();
_clockWindow = null;
TxtClock.Text = DateTime.Now.ToString("HH:mm:ss");
ClockOverlay.Visibility = Visibility.Visible;
}
if (_libVLC == null || _player == null) return; if (_libVLC == null || _player == null) return;
try try
@ -449,7 +396,7 @@ public partial class MainWindow : Window
_player.Media = media; _player.Media = media;
_player.Play(); _player.Play();
TxtEpgNow.Text = "EPG wird geladen\u2026"; TxtEpgNow.Text = "EPG wird geladen";
TxtEpgNext.Text = ""; TxtEpgNext.Text = "";
_epgTimer.Stop(); _epgTimer.Stop();
@ -473,16 +420,6 @@ public partial class MainWindow : Window
RadioCover.Visibility = Visibility.Collapsed; RadioCover.Visibility = Visibility.Collapsed;
VideoOverlay.Visibility = Visibility.Collapsed; VideoOverlay.Visibility = Visibility.Collapsed;
WebView.Visibility = Visibility.Visible; WebView.Visibility = Visibility.Visible;
// Uhr: WPF-Overlay -> separates Fenster wenn WebView aktiv wird
if (_clockVisible && _clockWindow == null)
{
ClockOverlay.Visibility = Visibility.Collapsed;
_clockWindow = new ClockWindow { Owner = this };
_clockWindow.UpdateTime(DateTime.Now.ToString("HH:mm:ss"));
_clockWindow.Show();
_clockWindow.Dispatcher.BeginInvoke(() => PositionClockWindow(),
System.Windows.Threading.DispatcherPriority.Render);
}
// Titelleiste via ApplySidebarLayout gesetzt // Titelleiste via ApplySidebarLayout gesetzt
TxtEpgNow.Text = "🌐 Browser"; TxtEpgNow.Text = "🌐 Browser";
@ -493,7 +430,7 @@ public partial class MainWindow : Window
if (_webViewReady) if (_webViewReady)
{ {
if (_webViewReady) WebView.CoreWebView2.Navigate(ch.Url); else WebView.Source = new Uri(ch.Url); WebView.Source = new Uri(ch.Url);
} }
else else
{ {
@ -503,7 +440,7 @@ public partial class MainWindow : Window
{ {
retryTimer.Stop(); retryTimer.Stop();
if (_webViewReady && _currentChannel?.Kind == ChannelKind.Web) if (_webViewReady && _currentChannel?.Kind == ChannelKind.Web)
if (_webViewReady) WebView.CoreWebView2.Navigate(ch.Url); else WebView.Source = new Uri(ch.Url); WebView.Source = new Uri(ch.Url);
}; };
retryTimer.Start(); retryTimer.Start();
} }
@ -603,14 +540,8 @@ public partial class MainWindow : Window
RadioFallbackIcon.Visibility = Visibility.Visible; RadioFallbackIcon.Visibility = Visibility.Visible;
} }
private DateTime _lastMetaUpdate = DateTime.MinValue;
private void OnMediaMetaChanged(object? sender, MediaMetaChangedEventArgs args) private void OnMediaMetaChanged(object? sender, MediaMetaChangedEventArgs args)
{ {
// Throttle: maximal alle 2s verarbeiten damit der UI-Thread nicht geflutet wird
var now = DateTime.Now;
if ((now - _lastMetaUpdate).TotalSeconds < 2) return;
_lastMetaUpdate = now;
if (sender is Media m) Dispatcher.BeginInvoke(() => UpdateEpgFromMedia(m)); if (sender is Media m) Dispatcher.BeginInvoke(() => UpdateEpgFromMedia(m));
} }
@ -716,84 +647,15 @@ public partial class MainWindow : Window
{ {
_volumeBeforeMute = SldVolume.Value; _volumeBeforeMute = SldVolume.Value;
SldVolume.Value = 0; SldVolume.Value = 0;
if (_webViewReady && WebView.Visibility == Visibility.Visible)
WebView.CoreWebView2.ExecuteScriptAsync("document.querySelectorAll('video,audio').forEach(m => m.muted=true)");
} }
else else
{ {
SldVolume.Value = _volumeBeforeMute > 0 ? _volumeBeforeMute : 80; SldVolume.Value = _volumeBeforeMute > 0 ? _volumeBeforeMute : 80;
if (_webViewReady && WebView.Visibility == Visibility.Visible)
WebView.CoreWebView2.ExecuteScriptAsync("document.querySelectorAll('video,audio').forEach(m => m.muted=false)");
} }
} }
private void BtnFullscreen_Click(object sender, RoutedEventArgs e) => ToggleFullscreen(); private void BtnFullscreen_Click(object sender, RoutedEventArgs e) => ToggleFullscreen();
private void BtnAlwaysOnTop_Click(object sender, RoutedEventArgs e)
{
Topmost = !Topmost;
BtnAlwaysOnTop.Foreground = Topmost
? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x00, 0x78, 0xD4))
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0x88, 0x88, 0x88));
BtnAlwaysOnTop.ToolTip = Topmost
? "Immer im Vordergrund: AN (Strg+T)"
: "Immer im Vordergrund (Strg+T)";
}
private void BtnClock_Click(object sender, RoutedEventArgs e) => ToggleClock();
private void ToggleClock()
{
_clockVisible = !_clockVisible;
var time = DateTime.Now.ToString("HH:mm:ss");
if (_clockVisible)
{
TxtClock.Text = time;
// WPF-Overlay fuer TV/Radio, separates Fenster nur fuer WebView
if (WebView.Visibility == Visibility.Visible)
{
_clockWindow = new ClockWindow { Owner = this };
_clockWindow.UpdateTime(time);
_clockWindow.Show();
_clockWindow.Dispatcher.BeginInvoke(() => PositionClockWindow(),
System.Windows.Threading.DispatcherPriority.Render);
}
else
{
ClockOverlay.Visibility = Visibility.Visible;
}
_clockTimer.Start();
BtnClock.Foreground = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(0x00, 0x78, 0xD4));
}
else
{
ClockOverlay.Visibility = Visibility.Collapsed;
_clockWindow?.Close();
_clockWindow = null;
_clockTimer.Stop();
BtnClock.Foreground = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(0x88, 0x88, 0x88));
}
}
private void PositionClockWindow()
{
if (_clockWindow == null) return;
try
{
// Oben rechts im Player-Bereich positionieren
var area = VideoView.Visibility == Visibility.Visible
? (System.Windows.FrameworkElement)VideoView
: WebView;
var pt = area.PointToScreen(new System.Windows.Point(area.ActualWidth, 0));
// 16px Margin, Fensterbreite abziehen (wird erst nach Show() bekannt)
_clockWindow.Left = pt.X - _clockWindow.ActualWidth - 16;
_clockWindow.Top = pt.Y + 16;
}
catch { }
}
private void ToggleFullscreen() private void ToggleFullscreen()
{ {
if (!_isFullscreen) if (!_isFullscreen)
@ -899,26 +761,12 @@ public partial class MainWindow : Window
private async void BtnEpgGrid_Click(object sender, RoutedEventArgs e) private async void BtnEpgGrid_Click(object sender, RoutedEventArgs e)
{ {
// EPG-Overlay liegt innerhalb VideoView (HWND) - VideoView muss sichtbar sein
if (_currentChannel?.Kind == ChannelKind.Web && _webViewReady)
await WebView.CoreWebView2.ExecuteScriptAsync(
"document.querySelectorAll('video,audio').forEach(m => m.pause())");
WebView.Visibility = Visibility.Collapsed;
VideoView.Visibility = Visibility.Visible;
VideoOverlay.Visibility = Visibility.Visible;
await ShowEpgOverlayAsync(); await ShowEpgOverlayAsync();
} }
private void BtnCloseEpg_Click(object sender, RoutedEventArgs e) private void BtnCloseEpg_Click(object sender, RoutedEventArgs e)
{ {
HideEpgOverlay(); HideEpgOverlay();
// Wenn Web-Sender: WebView wieder zeigen
if (_currentChannel?.Kind == ChannelKind.Web)
{
VideoView.Visibility = Visibility.Collapsed;
VideoOverlay.Visibility = Visibility.Collapsed;
WebView.Visibility = Visibility.Visible;
}
} }
// ────────── EPG-Overlay (Joyn-Style) ────────── // ────────── EPG-Overlay (Joyn-Style) ──────────
@ -930,8 +778,6 @@ public partial class MainWindow : Window
private const int EpgTotalHours = 8; private const int EpgTotalHours = 8;
private DateTime _epgStartTime; private DateTime _epgStartTime;
private double _epgScrollH = 0;
private double _epgScrollV = 0;
private async Task ShowEpgOverlayAsync() private async Task ShowEpgOverlayAsync()
{ {
@ -953,7 +799,7 @@ public partial class MainWindow : Window
} }
TxtEpgOverlayStatus.Text = "Baue Programm\u2026"; TxtEpgOverlayStatus.Text = "Baue Programm\u2026";
await BuildEpgGridAsync(resetScroll: true); await BuildEpgGridAsync();
} }
private void HideEpgOverlay() private void HideEpgOverlay()
@ -962,13 +808,8 @@ public partial class MainWindow : Window
EpgCanvas.Children.Clear(); EpgCanvas.Children.Clear();
} }
private async Task BuildEpgGridAsync()
private async Task BuildEpgGridAsync(bool resetScroll = false)
{ {
_epgScrollH = EpgScrollViewer.HorizontalOffset;
_epgScrollV = EpgScrollViewer.VerticalOffset;
await Dispatcher.InvokeAsync(() => EpgCanvas.Children.Clear(),
System.Windows.Threading.DispatcherPriority.Background);
EpgCanvas.Children.Clear(); EpgCanvas.Children.Clear();
var channels = _allChannels var channels = _allChannels
@ -998,17 +839,9 @@ public partial class MainWindow : Window
} }
TxtEpgOverlayStatus.Text = $"{channels.Count} Sender \u00b7 {EpgTotalHours} Stunden"; TxtEpgOverlayStatus.Text = $"{channels.Count} Sender \u00b7 {EpgTotalHours} Stunden";
if (resetScroll)
{
EpgScrollViewer.ScrollToHorizontalOffset(0); EpgScrollViewer.ScrollToHorizontalOffset(0);
EpgScrollViewer.ScrollToVerticalOffset(0); EpgScrollViewer.ScrollToVerticalOffset(0);
} }
else
{
EpgScrollViewer.ScrollToHorizontalOffset(_epgScrollH);
EpgScrollViewer.ScrollToVerticalOffset(_epgScrollV);
}
}
private void BuildEpgHeader(double totalMinutes, double contentWidth, double contentHeight) private void BuildEpgHeader(double totalMinutes, double contentWidth, double contentHeight)
{ {
@ -1160,17 +993,10 @@ public partial class MainWindow : Window
BorderBrush = new System.Windows.Media.SolidColorBrush(borderColor), 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, Cursor = Cursors.Hand,
Tag = ch ToolTip = $"{ev.StartTime:HH:mm}{ev.EndTime:HH:mm}\n{ev.Title}" +
(string.IsNullOrWhiteSpace(ev.Description) ? "" : $"\n\n{ev.Description}"),
Tag = ch, Child = content
}; };
box.Child = content;
// Tooltip mit Zeilenumbruch und Beschreibung
var ttText = $"{ev.StartTime:HH:mm}\u2013{ev.EndTime:HH:mm} {ev.Title}";
if (!string.IsNullOrWhiteSpace(ev.Description))
ttText += $"\n\n{ev.Description}";
var tt = new ToolTip { MaxWidth = 420, Content = new TextBlock { Text = ttText, TextWrapping = TextWrapping.Wrap, FontSize = 12 } };
ToolTipService.SetShowDuration(box, 30000);
box.ToolTip = tt;
box.MouseEnter += (_, _) => box.Background = new System.Windows.Media.SolidColorBrush( box.MouseEnter += (_, _) => box.Background = new System.Windows.Media.SolidColorBrush(
isCurrent ? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A) isCurrent ? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A)
@ -1190,67 +1016,6 @@ public partial class MainWindow : Window
PlayChannel(target); PlayChannel(target);
}; };
// Rechtsklick: Kontextmenu mit Erinnern-Option
box.MouseRightButtonDown += (_, args) =>
{
args.Handled = true;
var hasReminder = _reminderService.HasReminder(ch.Name, ev.StartTime);
var menu = new ContextMenu();
var remind = new MenuItem
{
Header = hasReminder ? "\u23f0 Erinnerung entfernen" : "\u23f0 5 min vorher erinnern",
FontSize = 13
};
remind.Click += (_, _) =>
{
if (hasReminder)
{
var existing = _reminderService.Reminders
.FirstOrDefault(r => r.ChannelName == ch.Name && r.StartTime == ev.StartTime);
if (existing != null) _reminderService.Remove(existing.Id);
}
else
{
_reminderService.Add(new Reminder
{
ChannelName = ch.Name,
Title = ev.Title,
Description = ev.Description ?? "",
StartTime = ev.StartTime,
EndTime = ev.EndTime,
MinutesBefore = 5
});
}
// Verzoegert neu bauen damit Event-Handler sauber abgeschlossen ist
Dispatcher.BeginInvoke(async () => await BuildEpgGridAsync(resetScroll: false),
System.Windows.Threading.DispatcherPriority.Background);
};
menu.Items.Add(remind);
box.ContextMenu = menu;
menu.IsOpen = true;
};
// Erinnerungs-Indikator: bell-Icon oben rechts
if (_reminderService.HasReminder(ch.Name, ev.StartTime))
{
// box.Child auf null damit content aus dem logical tree raus ist
box.Child = null;
var bell = new TextBlock
{
Text = "\u23f0",
FontSize = 11,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 3, 4, 0),
IsHitTestVisible = false
};
var overlay = new Grid();
overlay.Children.Add(content);
overlay.Children.Add(bell);
box.Child = overlay;
}
return box; return box;
} }
@ -1303,20 +1068,15 @@ public partial class MainWindow : Window
// WM_KEYDOWN abfangen damit Strg+B auch funktioniert wenn WebView2 den Fokus hat // WM_KEYDOWN abfangen damit Strg+B auch funktioniert wenn WebView2 den Fokus hat
private const int WM_KEYDOWN = 0x0100; private const int WM_KEYDOWN = 0x0100;
private const int VK_B = 0x42; private const int VK_B = 0x42;
private const int VK_U = 0x55;
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{ {
if (msg == WM_KEYDOWN) if (msg == WM_KEYDOWN && (int)wParam == VK_B)
{ {
// GetKeyState(VK_CONTROL) - High-bit gesetzt = Taste gedrueckt
var ctrl = (NativeMethods.GetKeyState(0x11) & 0x8000) != 0; var ctrl = (NativeMethods.GetKeyState(0x11) & 0x8000) != 0;
if (ctrl) if (ctrl)
{
if ((int)wParam == VK_B)
Dispatcher.BeginInvoke(() => BtnSidebarToggle_Click(this, new RoutedEventArgs())); Dispatcher.BeginInvoke(() => BtnSidebarToggle_Click(this, new RoutedEventArgs()));
else if ((int)wParam == VK_U)
Dispatcher.BeginInvoke(() => ToggleClock());
}
} }
return IntPtr.Zero; return IntPtr.Zero;
} }
@ -1341,19 +1101,8 @@ public partial class MainWindow : Window
switch (e.Key) switch (e.Key)
{ {
case Key.F11: ToggleFullscreen(); e.Handled = true; break; case Key.F11: ToggleFullscreen(); e.Handled = true; break;
case Key.T when Keyboard.Modifiers == ModifierKeys.Control:
BtnAlwaysOnTop_Click(this, new RoutedEventArgs()); e.Handled = true; break;
case Key.Escape: case Key.Escape:
if (EpgOverlay.Visibility == Visibility.Visible) if (EpgOverlay.Visibility == Visibility.Visible) HideEpgOverlay();
{
HideEpgOverlay();
if (_currentChannel?.Kind == ChannelKind.Web)
{
VideoView.Visibility = Visibility.Collapsed;
VideoOverlay.Visibility = Visibility.Collapsed;
WebView.Visibility = Visibility.Visible;
}
}
else if (_isFullscreen) ToggleFullscreen(); else if (_isFullscreen) ToggleFullscreen();
e.Handled = true; e.Handled = true;
break; break;

View file

@ -1,16 +0,0 @@
namespace FritzTV.Models;
public class Reminder
{
public Guid Id { get; set; } = Guid.NewGuid();
public required string ChannelName { get; set; }
public required string Title { get; set; }
public string Description { get; set; } = "";
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public int MinutesBefore { get; set; } = 5;
public bool Fired { get; set; } = false;
/// <summary>Zeitpunkt zu dem die Benachrichtigung ausgelöst wird</summary>
public DateTime NotifyAt => StartTime.AddMinutes(-MinutesBefore);
}

View file

@ -36,5 +36,4 @@ public static class AppPaths
public static string Epg => Path.Combine(Root, "epg"); public static string Epg => Path.Combine(Root, "epg");
public static string CrashLog => Path.Combine(Root, "crash.log"); public static string CrashLog => Path.Combine(Root, "crash.log");
public static string WebView2Profile => Path.Combine(Root, "webview2"); public static string WebView2Profile => Path.Combine(Root, "webview2");
public static string Reminders => Path.Combine(Root, "reminders.json");
} }

View file

@ -13,9 +13,6 @@ public class AppSettings
/// <summary>Online-Sender (ÖR-TV + Webradio aus Assets\online-sources.json) zusätzlich zur FritzBox-Liste anzeigen</summary> /// <summary>Online-Sender (ÖR-TV + Webradio aus Assets\online-sources.json) zusätzlich zur FritzBox-Liste anzeigen</summary>
public bool ShowOnlineSources { get; set; } = true; public bool ShowOnlineSources { get; set; } = true;
/// <summary>Startkategorie beim App-Start (default: Favoriten)</summary>
public string StartCategory { get; set; } = "fav";
private static readonly string ConfigPath = AppPaths.Settings; private static readonly string ConfigPath = AppPaths.Settings;
public static AppSettings Load() public static AppSettings Load()

View file

@ -1,107 +0,0 @@
using System.IO;
using System.Text.Json;
using System.Windows.Threading;
using FritzTV.Models;
namespace FritzTV.Services;
public class ReminderService
{
private static readonly string RemindersPath = AppPaths.Reminders;
private readonly DispatcherTimer _checkTimer;
private List<Reminder> _reminders = new();
/// <summary>Feuert wenn eine Erinnerung faellig ist. Callback laeuft auf UI-Thread.</summary>
public event Action<Reminder>? ReminderDue;
public IReadOnlyList<Reminder> Reminders => _reminders.AsReadOnly();
public ReminderService()
{
Load();
_checkTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) };
_checkTimer.Tick += (_, _) => CheckReminders();
_checkTimer.Start();
}
public void Add(Reminder reminder)
{
_reminders.Add(reminder);
Save();
}
public void Remove(Guid id)
{
_reminders.RemoveAll(r => r.Id == id);
Save();
}
public bool HasReminder(string channelName, DateTime startTime)
=> _reminders.Any(r => r.ChannelName == channelName && r.StartTime == startTime);
private void CheckReminders()
{
var now = DateTime.Now;
var due = _reminders
.Where(r => !r.Fired && r.NotifyAt <= now && r.StartTime > now.AddMinutes(-30))
.ToList();
foreach (var r in due)
{
r.Fired = true;
FireNotification(r);
ReminderDue?.Invoke(r);
}
_reminders.RemoveAll(r => r.Fired && r.StartTime < now.AddHours(-1));
if (due.Any()) Save();
}
private void FireNotification(Reminder r)
{
try
{
var minLeft = (int)(r.StartTime - DateTime.Now).TotalMinutes;
var when = minLeft <= 0 ? "Jetzt" : $"In {minLeft} Minute{(minLeft == 1 ? "" : "n")}";
var msg = $"{when}: {r.Title}\n{r.ChannelName} {r.StartTime:HH:mm}\u2013{r.EndTime:HH:mm}";
var result = System.Windows.MessageBox.Show(
msg + "\n\nJetzt einschalten?",
"\u23f0 HomeStream-Erinnerung",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Information);
if (result == System.Windows.MessageBoxResult.Yes)
SwitchToChannel?.Invoke(r.ChannelName);
}
catch { }
}
/// <summary>Wird aufgerufen wenn User bei Erinnerung "Jetzt einschalten" klickt.</summary>
public Action<string>? SwitchToChannel { get; set; }
private void Load()
{
try
{
if (File.Exists(RemindersPath))
{
var json = File.ReadAllText(RemindersPath);
_reminders = JsonSerializer.Deserialize<List<Reminder>>(json) ?? new();
_reminders.RemoveAll(r => r.StartTime < DateTime.Now.AddHours(-1));
}
}
catch { _reminders = new(); }
}
private void Save()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(RemindersPath)!);
var json = JsonSerializer.Serialize(_reminders, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(RemindersPath, json);
}
catch { }
}
}

View file

@ -1,291 +0,0 @@
# HomeStream Benutzerhandbuch
HomeStream ist ein DVB-C Streaming-Client für Windows, der AVM FRITZ!Box-Router als Quelle nutzt und zusätzlich öffentlich-rechtliche Online-Streams sowie Streaming-Dienste (YouTube, Netflix, Mediatheken) integriert.
---
## Inhaltsverzeichnis
1. [Systemvoraussetzungen](#1-systemvoraussetzungen)
2. [Installation](#2-installation)
3. [Erster Start](#3-erster-start)
4. [Oberfläche](#4-oberfläche)
5. [Sender & Kategorien](#5-sender--kategorien)
6. [Wiedergabe & Steuerung](#6-wiedergabe--steuerung)
7. [Favoriten](#7-favoriten)
8. [EPG Programmführer](#8-epg--programmführer)
9. [Erinnerungen](#9-erinnerungen)
10. [Streaming-Browser](#10-streaming-browser)
11. [Einstellungen](#11-einstellungen)
12. [Tastenkürzel](#12-tastenkürzel)
13. [Häufige Probleme](#13-häufige-probleme)
---
## 1. Systemvoraussetzungen
| Anforderung | Details |
|---|---|
| Betriebssystem | Windows 10 oder Windows 11 (64-Bit) |
| .NET-Runtime | Nicht erforderlich selbst-enthaltener Build |
| FRITZ!Box | DVB-C muss aktiviert und konfiguriert sein |
| Netzwerk | App und FRITZ!Box im gleichen Heimnetzwerk |
| WebView2 Runtime | Für Streaming-Browser (auf Win10/11 meist vorinstalliert) |
---
## 2. Installation
1. Neueste Version von der [Release-Seite](https://www.dimedtec.net/dimedtec/HomeStream/releases) herunterladen
2. ZIP-Datei entpacken (z.B. nach `C:\Programme\HomeStream\`)
3. `HomeStream.exe` starten keine Installation erforderlich
> **Hinweis:** Windows SmartScreen kann beim ersten Start eine Warnung anzeigen. „Weitere Informationen" → „Trotzdem ausführen" wählen.
---
## 3. Erster Start
Beim ersten Start lädt HomeStream automatisch:
- Die Senderliste von der FRITZ!Box (`192.168.178.1` ist der Standard)
- Online-Sender (ÖR-TV und Webradio) aus der integrierten Quellenliste
- EPG-Daten von iptv-epg.org (~6 MB, einmalig pro Tag)
Falls die FRITZ!Box-Adresse abweicht, unter **Einstellungen** die korrekte IP eintragen.
---
## 4. Oberfläche
```
┌─────────────┬──────────────────────┬────────────────────────────────┐
│ Kategorien │ Senderliste │ Videobild │
│ (Sidebar) │ │ │
│ │ ▶ Das Erste HD │ │
│ 📺 TV Fritz │ ZDF HD │ │
│ 📺 TV Online│ 3sat HD │ │
│ 📡 Radio │ ... │ │
│ 🌐 Streaming│ │ │
│ ⭐ Favoriten│ │ │
│ 📅 Programm │ │ │
├─────────────┴──────────────────────┴────────────────────────────────┤
│ Das Erste HD ● FritzBox ▶ Jetzt: Tagesschau │
│ 🔊 ────────────── 📌 ⛶ │
└─────────────────────────────────────────────────────────────────────┘
```
**Bereiche:**
- **Linke Spalte:** Kategorie-Buttons zur Filterung
- **Mittlere Spalte:** Senderliste mit Suchfeld oben
- **Hauptbereich:** Videobild / Streaming-Browser
- **Untere Leiste:** Aktueller Sender, EPG-Info, Lautstärke, Buttons
---
## 5. Sender & Kategorien
### Kategorien
| Kategorie | Inhalt |
|---|---|
| 📺 TV (FritzBox) | DVB-C Fernsehen über die FRITZ!Box |
| 📺 TV (Online) | Öffentlich-rechtliche TV-Streams (ARD, ZDF, 3sat, ARTE …) |
| 📡 Radio (FritzBox) | DVB-C Radiosender über die FRITZ!Box |
| 📡 Radio (Online) | Webradio (Bayern, DLF, WDR, NDR, SWR …) |
| 🌐 Streaming | Browser-basierte Streaming-Dienste |
| ⭐ Favoriten | Alle als Favorit markierten Sender |
> Die App startet immer mit der **Favoriten**-Ansicht.
### Sendersuche
Im Suchfeld oben in der Senderliste tippen die Liste filtert in Echtzeit.
### Sidebar ein-/ausblenden
**Strg+B** blendet die beiden linken Spalten ein oder aus. Wenn ausgeblendet, erscheint ein Hinweis in der Titelleiste.
---
## 6. Wiedergabe & Steuerung
### Sender wechseln
Sender in der Liste anklicken nach einer kurzen Verzögerung (250 ms Debounce) startet die Wiedergabe.
### Lautstärke
- Schieberegler in der Statusleiste unten
- **🔊-Button** für Stummschalten (Toggle)
- Tastenkürzel: **M** für Stummschalten
### Vollbild
- **F11** oder Doppelklick auf das Video
- **ESC** beendet den Vollbildmodus
### Immer im Vordergrund
- **📌-Button** (Pin) in der Statusleiste
- Oder **Strg+T**
- Aktiv: Button leuchtet blau
---
## 7. Favoriten
- **⭐-Button** in der Statusleiste neben dem Sendernamen klicken
- Favorit ist gesetzt wenn der Stern gefüllt ist (★)
- Favoriten sind **medienübergreifend** FritzBox-, Online- und Streaming-Sender können gleichermaßen als Favorit gespeichert werden
- Favoriten werden in `%APPDATA%\HomeStream\settings.json` gespeichert
---
## 8. EPG Programmführer
### Aktuelles Programm
Unter dem Sendernamen in der Statusleiste wird angezeigt:
- **▶ Jetzt:** Aktuelle Sendung (FritzBox: aus EIT-Signal; Online: aus EPG-Daten)
- Danach: Die nächsten 23 Sendungen mit Uhrzeit
Diese Anzeige aktualisiert sich automatisch alle 60 Sekunden.
### Programmübersicht (Grid)
Klick auf **📅 Programm** in der Sidebar öffnet ein Joyn-ähnliches EPG-Grid:
- Alle TV-Sender in einer Übersicht
- 8 Stunden Programmvorschau
- Aktuelle Sendung rot hervorgehoben
- **Linksklick** auf eine Sendung: sofort einschalten
- **Rechtsklick** auf eine Sendung: Erinnerung setzen
- Tooltip zeigt Titel, Uhrzeit und Beschreibung
**Navigation im Grid:**
- Horizontales Scrollen: Mausrad oder Scrollbar
- Vertikales Scrollen: Mausrad oder Scrollbar
> EPG-Daten werden täglich frisch von [iptv-epg.org](https://iptv-epg.org) geladen und lokal in `%APPDATA%\HomeStream\epg\` gecacht.
---
## 9. Erinnerungen
Im EPG-Grid eine zukünftige Sendung **rechts klicken** → „⏰ 5 min vorher erinnern".
- Die Sendung zeigt ein ⏰-Symbol im Grid
- 5 Minuten vor Sendungsstart erscheint eine Benachrichtigung
- Auf **„Ja"** klicken schaltet automatisch auf den Sender um und schließt das EPG
- Erinnerungen werden in `%APPDATA%\HomeStream\reminders.json` gespeichert und überleben App-Neustarts
Zum Entfernen: Rechtsklick auf die Sendung → „⏰ Erinnerung entfernen".
---
## 10. Streaming-Browser
Unter **🌐 Streaming** sind Browser-basierte Dienste verfügbar:
| Dienst | URL |
|---|---|
| YouTube | youtube.com |
| ARD Mediathek | ardmediathek.de |
| ZDF Mediathek | zdf.de |
| Netflix | netflix.com |
| Disney+ | disneyplus.com |
| Joyn | joyn.de |
| RTL+ | plus.rtl.de |
| ARTE | arte.tv/de |
| Apple TV+ | tv.apple.com |
| Amazon Prime Video | amazon.de/gp/video |
| n-tv | n-tv.de |
### Login speichern
Der Streaming-Browser verwendet ein **persistentes WebView2-Profil** unter `%APPDATA%\HomeStream\webview2\`. Einmal einloggen genügt der Login bleibt beim nächsten App-Start erhalten.
### Steuerung im Browser-Modus
- Normale Browser-Navigation per Maus
- **Strg+B** öffnet die Sidebar wieder wenn sie eingeklappt ist
- **🔊-Button** (Stummschalten) funktioniert auch im Browser-Modus
- **📌-Button** (Immer im Vordergrund) funktioniert auch im Browser-Modus
### Wechsel zurück zu TV
Einfach einen TV- oder Radio-Sender in der Senderliste anklicken der Browser wird automatisch gestoppt und der VLC-Player übernimmt.
---
## 11. Einstellungen
Erreichbar über den **⚙-Button** in der Statusleiste.
| Einstellung | Beschreibung |
|---|---|
| FritzBox-Adresse | IP oder Hostname der FRITZ!Box (Standard: 192.168.178.1) |
| Online-Sender anzeigen | ÖR-TV und Webradio ein-/ausschalten |
---
## 12. Tastenkürzel
| Taste | Funktion |
|---|---|
| **Strg+B** | Sidebar ein-/ausblenden |
| **Strg+T** | Immer im Vordergrund an/aus |
| **F11** | Vollbild an/aus |
| **ESC** | Vollbild beenden / EPG schließen |
| **M** | Stummschalten |
| **↑ / ↓** | Sender in der Liste wechseln |
---
## 13. Häufige Probleme
### Senderliste leer / FRITZ!Box nicht erreichbar
- FRITZ!Box-Adresse in den Einstellungen prüfen
- DVB-C in der FRITZ!Box aktiviert? → FRITZ!Box-Oberfläche → Heimnetz → Multimedia → DVB-C
- App und FRITZ!Box im gleichen Netzwerk?
- Online-Sender funktionieren auch ohne FRITZ!Box
### Bild friert ein / ruckelt
- FRITZ!Box neu starten
- Prüfen ob ein anderes Gerät gleichzeitig streamt (FRITZ!Box erlaubt nur einen Stream pro IP-Adresse)
- Im originalen VLC-Player testen: wenn dort auch Probleme auftreten, liegt es am DVB-Signal
### EPG zeigt falsche Zeiten oder keine Daten
- EPG-Cache löschen: `%APPDATA%\HomeStream\epg\` leeren
- App neu starten EPG wird automatisch neu geladen
### Streaming-Browser startet nicht / WebView2 fehlt
- WebView2 Runtime installieren: [Microsoft WebView2 Runtime](https://developer.microsoft.com/de-de/microsoft-edge/webview2/)
- Auf Windows 10 Version 1803 oder neuer aktualisieren
### Login im Streaming-Browser geht verloren
- Das WebView2-Profil liegt in `%APPDATA%\HomeStream\webview2\`
- Wird dieser Ordner gelöscht, gehen alle gespeicherten Logins verloren
---
## Datenspeicherung
Alle Benutzerdaten werden lokal gespeichert unter `%APPDATA%\HomeStream\`:
| Datei/Ordner | Inhalt |
|---|---|
| `settings.json` | Einstellungen, Favoriten, letzter Sender |
| `reminders.json` | Gespeicherte EPG-Erinnerungen |
| `epg\` | Gecachte EPG-Daten (täglich aktualisiert) |
| `logos\` | Gecachte Sender-Logos |
| `webview2\` | Browser-Profil (Logins, Cookies) |
---
*HomeStream wird entwickelt von [dimedtec GmbH](https://www.dimedtec.net) · [Quellcode & Releases](https://www.dimedtec.net/dimedtec/HomeStream)*

View file

@ -77,17 +77,16 @@ if (-not $Tag) {
} }
Write-Host "→ Forgejo-Release $Tag erstellen..." Write-Host "→ Forgejo-Release $Tag erstellen..."
$headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json; charset=utf-8' } $headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json' }
$bodyObj = @{ $body = @{
tag_name = $Tag tag_name = $Tag
name = "HomeStream $version" name = "HomeStream $version"
body = "Self-contained Release fuer Windows 10/11 (x64). Keine .NET-Installation noetig." body = "Self-contained Release für Windows 10/11 (x64). Keine .NET-Installation nötig."
draft = $false draft = $false
prerelease = $false prerelease = $false
} } | ConvertTo-Json
# Explizit UTF-8 kodieren damit Umlaute korrekt uebertragen werden
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes(($bodyObj | ConvertTo-Json)) $release = Invoke-RestMethod -Uri "$ApiBase/releases" -Method Post -Headers $headers -Body $body
$release = Invoke-RestMethod -Uri "$ApiBase/releases" -Method Post -Headers $headers -Body $bodyBytes
Write-Host " Release-ID: $($release.id)" Write-Host " Release-ID: $($release.id)"
# ── 5. ZIP als Asset hochladen ──────────────────────────────────────────── # ── 5. ZIP als Asset hochladen ────────────────────────────────────────────