diff --git a/Assets/online-sources.json b/Assets/online-sources.json
index 3f497be..316a0aa 100644
--- a/Assets/online-sources.json
+++ b/Assets/online-sources.json
@@ -47,13 +47,14 @@
"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": "ZDF Mediathek", "url": "https://www.zdf.de/" },
{ "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": "Amazon Prime Video", "url": "https://www.amazon.de/gp/video/storefront" },
+ { "name": "n-tv", "url": "https://www.n-tv.de" }
]
}
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 9769afd..4e35784 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -227,41 +227,39 @@
-
+
+
-
-
-
+
+
+ HorizontalAlignment="Center" Visibility="Collapsed"/>
+ TextWrapping="Wrap" TextAlignment="Center" MaxWidth="600"/>
-
+
-
@@ -277,20 +275,11 @@
+ 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 74dc07d..8caef16 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -19,11 +19,12 @@ 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 = "all";
+ private string _currentCategory = "fav"; // wird in MainWindow_Loaded aus Settings gesetzt
private string _searchTerm = "";
private Channel? _currentChannel;
private double _volumeBeforeMute = 80;
@@ -56,6 +57,7 @@ public partial class MainWindow : Window
InitializeComponent();
LstChannels.ItemsSource = _filteredChannels;
TxtFritzBox.Text = $"FritzBox: {_settings.FritzBoxIp}";
+ _currentCategory = _settings.StartCategory;
SldVolume.Value = _settings.Volume;
DarkTitleBar.EnableFor(this);
@@ -66,21 +68,28 @@ public partial class MainWindow : Window
_epgTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
_epgTimer.Tick += EpgTimer_Tick;
- // 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;
+ // Jede 60s: XMLTV Jetzt/Danach refreshen + EIT neu lesen
+ _epgRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(60) };
_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)
{
- _epgRefreshTick = 0;
- UpdateNextFromEpgService(_currentChannel);
+ 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 { }
+ });
}
+ UpdateNextFromEpgService(_currentChannel);
};
_epgRefreshTimer.Start();
@@ -122,6 +131,22 @@ 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();
}
@@ -311,7 +336,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" => MergeHdSd(q.Where(c => c.IsFavorite)),
+ "fav" => q.Where(c => c.IsFavorite), // kein MergeHdSd - Web-Sender wuerden sonst rausgefiltert
_ => MergeHdSd(q.Where(c => c.Kind != ChannelKind.Web)) // "all" ohne Web
};
@@ -367,6 +392,8 @@ 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();
@@ -396,7 +423,7 @@ public partial class MainWindow : Window
_player.Media = media;
_player.Play();
- TxtEpgNow.Text = "EPG wird geladen…";
+ TxtEpgNow.Text = "EPG wird geladen\u2026";
TxtEpgNext.Text = "";
_epgTimer.Stop();
@@ -430,7 +457,7 @@ public partial class MainWindow : Window
if (_webViewReady)
{
- WebView.Source = new Uri(ch.Url);
+ if (_webViewReady) WebView.CoreWebView2.Navigate(ch.Url); else WebView.Source = new Uri(ch.Url);
}
else
{
@@ -440,7 +467,7 @@ public partial class MainWindow : Window
{
retryTimer.Stop();
if (_webViewReady && _currentChannel?.Kind == ChannelKind.Web)
- WebView.Source = new Uri(ch.Url);
+ if (_webViewReady) WebView.CoreWebView2.Navigate(ch.Url); else WebView.Source = new Uri(ch.Url);
};
retryTimer.Start();
}
@@ -540,8 +567,14 @@ 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));
}
@@ -647,15 +680,30 @@ 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 ToggleFullscreen()
{
if (!_isFullscreen)
@@ -761,12 +809,26 @@ 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) ──────────
@@ -778,6 +840,8 @@ 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()
{
@@ -799,7 +863,7 @@ public partial class MainWindow : Window
}
TxtEpgOverlayStatus.Text = "Baue Programm\u2026";
- await BuildEpgGridAsync();
+ await BuildEpgGridAsync(resetScroll: true);
}
private void HideEpgOverlay()
@@ -808,8 +872,13 @@ public partial class MainWindow : Window
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();
var channels = _allChannels
@@ -839,8 +908,16 @@ public partial class MainWindow : Window
}
TxtEpgOverlayStatus.Text = $"{channels.Count} Sender \u00b7 {EpgTotalHours} Stunden";
- EpgScrollViewer.ScrollToHorizontalOffset(0);
- EpgScrollViewer.ScrollToVerticalOffset(0);
+ if (resetScroll)
+ {
+ EpgScrollViewer.ScrollToHorizontalOffset(0);
+ EpgScrollViewer.ScrollToVerticalOffset(0);
+ }
+ else
+ {
+ EpgScrollViewer.ScrollToHorizontalOffset(_epgScrollH);
+ EpgScrollViewer.ScrollToVerticalOffset(_epgScrollV);
+ }
}
private void BuildEpgHeader(double totalMinutes, double contentWidth, double contentHeight)
@@ -993,10 +1070,17 @@ public partial class MainWindow : Window
BorderBrush = new System.Windows.Media.SolidColorBrush(borderColor),
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
};
+ 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)
@@ -1016,6 +1100,67 @@ 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;
}
@@ -1073,7 +1218,6 @@ public partial class MainWindow : Window
{
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)
Dispatcher.BeginInvoke(() => BtnSidebarToggle_Click(this, new RoutedEventArgs()));
@@ -1101,8 +1245,19 @@ 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 (EpgOverlay.Visibility == Visibility.Visible)
+ {
+ HideEpgOverlay();
+ if (_currentChannel?.Kind == ChannelKind.Web)
+ {
+ VideoView.Visibility = Visibility.Collapsed;
+ VideoOverlay.Visibility = Visibility.Collapsed;
+ WebView.Visibility = Visibility.Visible;
+ }
+ }
else if (_isFullscreen) ToggleFullscreen();
e.Handled = true;
break;
diff --git a/Models/Reminder.cs b/Models/Reminder.cs
new file mode 100644
index 0000000..f465581
--- /dev/null
+++ b/Models/Reminder.cs
@@ -0,0 +1,16 @@
+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 6e55dc1..bdb7dce 100644
--- a/Services/AppPaths.cs
+++ b/Services/AppPaths.cs
@@ -36,4 +36,5 @@ 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 c9ae0b8..87fd4c8 100644
--- a/Services/AppSettings.cs
+++ b/Services/AppSettings.cs
@@ -13,6 +13,9 @@ 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
new file mode 100644
index 0000000..d21a571
--- /dev/null
+++ b/Services/ReminderService.cs
@@ -0,0 +1,107 @@
+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/publish.ps1 b/publish.ps1
index 4b3daec..b80b4b5 100644
--- a/publish.ps1
+++ b/publish.ps1
@@ -77,16 +77,17 @@ if (-not $Tag) {
}
Write-Host "→ Forgejo-Release $Tag erstellen..."
-$headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json' }
-$body = @{
+$headers = @{ 'Authorization' = "token $Token"; 'Content-Type' = 'application/json; charset=utf-8' }
+$bodyObj = @{
tag_name = $Tag
name = "HomeStream $version"
- body = "Self-contained Release für Windows 10/11 (x64). Keine .NET-Installation nötig."
+ body = "Self-contained Release fuer Windows 10/11 (x64). Keine .NET-Installation noetig."
draft = $false
prerelease = $false
-} | ConvertTo-Json
-
-$release = Invoke-RestMethod -Uri "$ApiBase/releases" -Method Post -Headers $headers -Body $body
+}
+# 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
Write-Host " Release-ID: $($release.id)"
# ── 5. ZIP als Asset hochladen ────────────────────────────────────────────