diff --git a/Assets/online-sources.json b/Assets/online-sources.json
index 316a0aa..3f497be 100644
--- a/Assets/online-sources.json
+++ b/Assets/online-sources.json
@@ -47,14 +47,13 @@
"web": [
{ "name": "YouTube", "url": "https://www.youtube.com" },
{ "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": "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" },
- { "name": "n-tv", "url": "https://www.n-tv.de" }
+ { "name": "Amazon Prime Video", "url": "https://www.amazon.de/gp/video/storefront" }
]
}
diff --git a/ClockWindow.xaml b/ClockWindow.xaml
deleted file mode 100644
index ef4fe54..0000000
--- a/ClockWindow.xaml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
diff --git a/ClockWindow.xaml.cs b/ClockWindow.xaml.cs
deleted file mode 100644
index 169c1ef..0000000
--- a/ClockWindow.xaml.cs
+++ /dev/null
@@ -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;
-}
diff --git a/MainWindow.xaml b/MainWindow.xaml
index ef7e74d..9769afd 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -227,50 +227,41 @@
-
+
-
-
-
-
-
-
-
+
+
+ HorizontalAlignment="Center"
+ Visibility="Collapsed"/>
+ TextWrapping="Wrap" TextAlignment="Center"
+ MaxWidth="600"/>
-
+
+
@@ -286,11 +277,20 @@
+ Cursor="Hand"
+ Click="BtnCloseEpg_Click"
+ ToolTip="Schließen (Esc)"/>
+
+
+ Foreground="#888" FontSize="11" Padding="24,8"
+ Background="#0A0A0A"/>
+
+
+
-
-
+
+
+
+
+
-
-
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index d07c37f..74dc07d 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -19,12 +19,11 @@ public partial class MainWindow : Window
private Media? _currentMedia;
private AppSettings _settings = AppSettings.Load();
private readonly EpgService _epgService = new();
- private readonly ReminderService _reminderService = new();
private readonly LogoService _logoService = new();
private readonly ObservableCollection _allChannels = new();
private readonly ObservableCollection _filteredChannels = new();
- private string _currentCategory = "fav"; // wird in MainWindow_Loaded aus Settings gesetzt
+ private string _currentCategory = "all";
private string _searchTerm = "";
private Channel? _currentChannel;
private double _volumeBeforeMute = 80;
@@ -42,11 +41,6 @@ public partial class MainWindow : Window
// WebView2 für Web-Sender (YouTube, Netflix etc.)
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)
private System.Windows.Interop.HwndSource? _hwndSource;
@@ -62,7 +56,6 @@ public partial class MainWindow : Window
InitializeComponent();
LstChannels.ItemsSource = _filteredChannels;
TxtFritzBox.Text = $"FritzBox: {_settings.FritzBoxIp}";
- _currentCategory = _settings.StartCategory;
SldVolume.Value = _settings.Volume;
DarkTitleBar.EnableFor(this);
@@ -73,44 +66,28 @@ public partial class MainWindow : Window
_epgTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_epgTimer.Tick += EpgTimer_Tick;
- // Jede 60s: XMLTV Jetzt/Danach refreshen + EIT neu lesen
- _epgRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(60) };
+ // Jede 10s: EIT-Daten aus Media neu lesen (libVLC feuert MetaChanged nicht bei laufendem EIT-Update)
+ // Jede 60s: XMLTV Jetzt/Danach refreshen
+ _epgRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(10) };
+ var _epgRefreshTick = 0;
_epgRefreshTimer.Tick += (_, _) =>
{
if (_currentChannel == null) return;
- // EIT auf Background-Thread lesen damit UI nicht blockiert
if (_currentChannel.Source == ChannelSource.FritzBox && _currentMedia != null)
+ UpdateEpgFromMedia(_currentMedia);
+ _epgRefreshTick++;
+ if (_epgRefreshTick >= 6)
{
- var media = _currentMedia;
- 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 { }
- });
+ _epgRefreshTick = 0;
+ UpdateNextFromEpgService(_currentChannel);
}
- UpdateNextFromEpgService(_currentChannel);
};
_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);
- PositionClockWindow();
- };
-
Loaded += MainWindow_Loaded;
Closing += MainWindow_Closing;
KeyDown += MainWindow_KeyDown;
+ PreviewKeyDown += MainWindow_KeyDown;
}
private void OnVideoDoubleClick(object sender, MouseButtonEventArgs e)
@@ -145,22 +122,6 @@ public partial class MainWindow : Window
_ = InitializeWebViewAsync();
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();
_ = LoadEpgInBackgroundAsync();
}
@@ -223,8 +184,6 @@ public partial class MainWindow : Window
_epgTimer.Stop();
_zapTimer.Stop();
_epgRefreshTimer.Stop();
- _clockTimer.Stop();
- _clockWindow?.Close();
_player?.Stop();
_settings.LastChannel = _currentChannel?.Name ?? "";
_settings.Volume = SldVolume.Value;
@@ -352,7 +311,7 @@ public partial class MainWindow : Window
"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" => 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
};
@@ -408,8 +367,6 @@ public partial class MainWindow : Window
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));
- // Aufnahme stoppen wenn Sender wechselt
- // if (_isRecording) StopRecording();
TxtNoChannel.Visibility = Visibility.Collapsed;
UpdateFavButton();
@@ -439,7 +396,7 @@ public partial class MainWindow : Window
_player.Media = media;
_player.Play();
- TxtEpgNow.Text = "EPG wird geladen\u2026";
+ TxtEpgNow.Text = "EPG wird geladen…";
TxtEpgNext.Text = "";
_epgTimer.Stop();
@@ -473,7 +430,7 @@ public partial class MainWindow : Window
if (_webViewReady)
{
- if (_webViewReady) WebView.CoreWebView2.Navigate(ch.Url); else WebView.Source = new Uri(ch.Url);
+ WebView.Source = new Uri(ch.Url);
}
else
{
@@ -483,7 +440,7 @@ public partial class MainWindow : Window
{
retryTimer.Stop();
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();
}
@@ -583,14 +540,8 @@ public partial class MainWindow : Window
RadioFallbackIcon.Visibility = Visibility.Visible;
}
- private DateTime _lastMetaUpdate = DateTime.MinValue;
-
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));
}
@@ -696,82 +647,15 @@ public partial class MainWindow : Window
{
_volumeBeforeMute = SldVolume.Value;
SldVolume.Value = 0;
- if (_webViewReady && WebView.Visibility == Visibility.Visible)
- WebView.CoreWebView2.ExecuteScriptAsync("document.querySelectorAll('video,audio').forEach(m => m.muted=true)");
}
else
{
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 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)
- {
- // WPF-Overlay (fuer TV-Modus innerhalb VideoView)
- TxtClock.Text = time;
- ClockOverlay.Visibility = Visibility.Visible;
-
- // Separates Fenster (fuer WebView-Modus, schwebt ueber allem)
- _clockWindow = new ClockWindow { Owner = this };
- _clockWindow.UpdateTime(time);
- _clockWindow.Show();
- // Nach Show() ist ActualWidth bekannt
- _clockWindow.Dispatcher.BeginInvoke(() => PositionClockWindow(),
- System.Windows.Threading.DispatcherPriority.Render);
-
- _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()
{
if (!_isFullscreen)
@@ -877,26 +761,12 @@ public partial class MainWindow : Window
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();
}
private void BtnCloseEpg_Click(object sender, RoutedEventArgs e)
{
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) ──────────
@@ -908,8 +778,6 @@ public partial class MainWindow : Window
private const int EpgTotalHours = 8;
private DateTime _epgStartTime;
- private double _epgScrollH = 0;
- private double _epgScrollV = 0;
private async Task ShowEpgOverlayAsync()
{
@@ -931,7 +799,7 @@ public partial class MainWindow : Window
}
TxtEpgOverlayStatus.Text = "Baue Programm\u2026";
- await BuildEpgGridAsync(resetScroll: true);
+ await BuildEpgGridAsync();
}
private void HideEpgOverlay()
@@ -940,13 +808,8 @@ public partial class MainWindow : Window
EpgCanvas.Children.Clear();
}
-
- private async Task BuildEpgGridAsync(bool resetScroll = false)
+ private async Task BuildEpgGridAsync()
{
- _epgScrollH = EpgScrollViewer.HorizontalOffset;
- _epgScrollV = EpgScrollViewer.VerticalOffset;
- await Dispatcher.InvokeAsync(() => EpgCanvas.Children.Clear(),
- System.Windows.Threading.DispatcherPriority.Background);
EpgCanvas.Children.Clear();
var channels = _allChannels
@@ -976,16 +839,8 @@ public partial class MainWindow : Window
}
TxtEpgOverlayStatus.Text = $"{channels.Count} Sender \u00b7 {EpgTotalHours} Stunden";
- if (resetScroll)
- {
- EpgScrollViewer.ScrollToHorizontalOffset(0);
- EpgScrollViewer.ScrollToVerticalOffset(0);
- }
- else
- {
- EpgScrollViewer.ScrollToHorizontalOffset(_epgScrollH);
- EpgScrollViewer.ScrollToVerticalOffset(_epgScrollV);
- }
+ EpgScrollViewer.ScrollToHorizontalOffset(0);
+ EpgScrollViewer.ScrollToVerticalOffset(0);
}
private void BuildEpgHeader(double totalMinutes, double contentWidth, double contentHeight)
@@ -1138,17 +993,10 @@ public partial class MainWindow : Window
BorderBrush = new System.Windows.Media.SolidColorBrush(borderColor),
BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(3),
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(
isCurrent ? System.Windows.Media.Color.FromRgb(0xB8, 0x21, 0x3A)
@@ -1168,67 +1016,6 @@ public partial class MainWindow : Window
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;
}
@@ -1281,20 +1068,15 @@ public partial class MainWindow : Window
// WM_KEYDOWN abfangen damit Strg+B auch funktioniert wenn WebView2 den Fokus hat
private const int WM_KEYDOWN = 0x0100;
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)
{
- 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;
if (ctrl)
- {
- if ((int)wParam == VK_B)
- Dispatcher.BeginInvoke(() => BtnSidebarToggle_Click(this, new RoutedEventArgs()));
- else if ((int)wParam == VK_U)
- Dispatcher.BeginInvoke(() => ToggleClock());
- }
+ Dispatcher.BeginInvoke(() => BtnSidebarToggle_Click(this, new RoutedEventArgs()));
}
return IntPtr.Zero;
}
@@ -1319,19 +1101,8 @@ public partial class MainWindow : Window
switch (e.Key)
{
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:
- if (EpgOverlay.Visibility == Visibility.Visible)
- {
- HideEpgOverlay();
- if (_currentChannel?.Kind == ChannelKind.Web)
- {
- VideoView.Visibility = Visibility.Collapsed;
- VideoOverlay.Visibility = Visibility.Collapsed;
- WebView.Visibility = Visibility.Visible;
- }
- }
+ if (EpgOverlay.Visibility == Visibility.Visible) HideEpgOverlay();
else if (_isFullscreen) ToggleFullscreen();
e.Handled = true;
break;
diff --git a/Models/Reminder.cs b/Models/Reminder.cs
deleted file mode 100644
index f465581..0000000
--- a/Models/Reminder.cs
+++ /dev/null
@@ -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;
-
- /// Zeitpunkt zu dem die Benachrichtigung ausgelöst wird
- public DateTime NotifyAt => StartTime.AddMinutes(-MinutesBefore);
-}
diff --git a/Services/AppPaths.cs b/Services/AppPaths.cs
index bdb7dce..6e55dc1 100644
--- a/Services/AppPaths.cs
+++ b/Services/AppPaths.cs
@@ -36,5 +36,4 @@ public static class AppPaths
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");
- public static string Reminders => Path.Combine(Root, "reminders.json");
}
diff --git a/Services/AppSettings.cs b/Services/AppSettings.cs
index 87fd4c8..c9ae0b8 100644
--- a/Services/AppSettings.cs
+++ b/Services/AppSettings.cs
@@ -13,9 +13,6 @@ public class AppSettings
/// Online-Sender (ÖR-TV + Webradio aus Assets\online-sources.json) zusätzlich zur FritzBox-Liste anzeigen
public bool ShowOnlineSources { get; set; } = true;
- /// Startkategorie beim App-Start (default: Favoriten)
- public string StartCategory { get; set; } = "fav";
-
private static readonly string ConfigPath = AppPaths.Settings;
public static AppSettings Load()
diff --git a/Services/ReminderService.cs b/Services/ReminderService.cs
deleted file mode 100644
index d21a571..0000000
--- a/Services/ReminderService.cs
+++ /dev/null
@@ -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 _reminders = new();
-
- /// Feuert wenn eine Erinnerung faellig ist. Callback laeuft auf UI-Thread.
- public event Action? ReminderDue;
-
- public IReadOnlyList 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 { }
- }
-
- /// Wird aufgerufen wenn User bei Erinnerung "Jetzt einschalten" klickt.
- public Action? SwitchToChannel { get; set; }
-
- private void Load()
- {
- try
- {
- if (File.Exists(RemindersPath))
- {
- var json = File.ReadAllText(RemindersPath);
- _reminders = JsonSerializer.Deserialize>(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 { }
- }
-}
diff --git a/_wiki_home.md b/_wiki_home.md
deleted file mode 100644
index 75ec965..0000000
--- a/_wiki_home.md
+++ /dev/null
@@ -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 2–3 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)*
diff --git a/publish.ps1 b/publish.ps1
index b80b4b5..4b3daec 100644
--- a/publish.ps1
+++ b/publish.ps1
@@ -77,17 +77,16 @@ if (-not $Tag) {
}
Write-Host "→ Forgejo-Release $Tag erstellen..."
-$headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json; charset=utf-8' }
-$bodyObj = @{
+$headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json' }
+$body = @{
tag_name = $Tag
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
prerelease = $false
-}
-# 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 $bodyBytes
+} | ConvertTo-Json
+
+$release = Invoke-RestMethod -Uri "$ApiBase/releases" -Method Post -Headers $headers -Body $body
Write-Host " Release-ID: $($release.id)"
# ── 5. ZIP als Asset hochladen ────────────────────────────────────────────