add partial background downloading and update prompt

This commit is contained in:
2025-12-15 11:03:30 +01:00
parent 5cb7895e24
commit c6097ab6dc
5 changed files with 153 additions and 59 deletions

View File

@@ -53,10 +53,6 @@ namespace BlueMine
/// </summary> /// </summary>
private async void OnStartup(object sender, StartupEventArgs e) private async void OnStartup(object sender, StartupEventArgs e)
{ {
#if !DEBUG
var update = new UpdateManager();
await update.CheckAndInstallAsync();
#endif
await _host.StartAsync(); await _host.StartAsync();
var mainWindow = _host.Services.GetRequiredService<MainWindow>(); var mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show(); mainWindow.Show();

View File

@@ -28,5 +28,41 @@
"Nem elszámolható hívások, email, chat", "Nem elszámolható hívások, email, chat",
"Nem elszámolható telefon, chat, email kommunikáció", "Nem elszámolható telefon, chat, email kommunikáció",
]; ];
public static readonly string UpdateScript = @"# Wait for the main app to close completely
Start-Sleep -Seconds 2
$exePath = '{currentExe}'
$zipPath = '{tempZip}'
$destDir = '{appDir}'
# Retry logic for deletion (in case antivirus or OS holds the lock)
$maxRetries = 10
$retryCount = 0
while ($retryCount -lt $maxRetries) {{
try {{
# Attempt to delete the old executable
if (Test-Path $exePath) {{ Remove-Item $exePath -Force -ErrorAction Stop }}
break # If successful, exit loop
}}
catch {{
Start-Sleep -Milliseconds 500
$retryCount++
}}
}}
# Unzip the new version
Expand-Archive -Path $zipPath -DestinationPath $destDir -Force
# CLEANUP: Delete the zip
Remove-Item $zipPath -Force
# RESTART: Launch the new executable
# 'Start-Process' is the robust way to launch detached processes in PS
Start-Process -FilePath $exePath -WorkingDirectory $destDir
# SELF-DESTRUCT: Remove this script
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force";
} }
} }

View File

@@ -256,6 +256,15 @@
<ui:ProgressRing x:Name="progressRing" Grid.Row="4" Height="10" Width="10" Margin="10" HorizontalAlignment="Left" IsIndeterminate="True" /> <ui:ProgressRing x:Name="progressRing" Grid.Row="4" Height="10" Width="10" Margin="10" HorizontalAlignment="Left" IsIndeterminate="True" />
<ui:TextBlock x:Name="statusTextBlock" Grid.Row="4" Grid.ColumnSpan="6" FontSize="8" Text="Staus: OK" Margin="30, 10, 10, 10" /> <ui:TextBlock x:Name="statusTextBlock" Grid.Row="4" Grid.ColumnSpan="6" FontSize="8" Text="Staus: OK" Margin="30, 10, 10, 10" />
<ui:TextBlock x:Name="versionTextBlock" Grid.Row="4" Grid.Column="2" HorizontalAlignment="Right" FontSize="8" Text="0.0.0" Margin="10" /> <Grid Grid.Row="4" Grid.Column="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon x:Name="updateIcon" Symbol="ArrowCircleUp24" Grid.Column="1" Margin="10" Filled="True"
Foreground="{ui:ThemeResource AccentTextFillColorPrimaryBrush}" Visibility="Hidden" />
<ui:TextBlock x:Name="versionTextBlock" Grid.Column="2" HorizontalAlignment="Right" FontSize="8" Text="0.0.0" Margin="10" />
</Grid>
</Grid> </Grid>
</ui:FluentWindow> </ui:FluentWindow>

View File

@@ -45,7 +45,33 @@ namespace BlueMine
Task getHoursTask = GetHours(); Task getHoursTask = GetHours();
await Task.WhenAll(loadIssuesTask, getHoursTask); await Task.WhenAll(loadIssuesTask, getHoursTask);
#if !DEBUG
if(await UpdateManager.IsUpdateAvailable())
{
updateIcon.Visibility = Visibility.Visible;
UpdateManager.DownloadCompleted += UpdateManager_DownloadCompleted;
await UpdateManager.DownloadUpdateAsync();
} }
#endif
}
}
private async void UpdateManager_DownloadCompleted()
{
await Dispatcher.Invoke(async () =>
{
var result = await new Wpf.Ui.Controls.MessageBox
{
Title = "Frissítés elérhető",
Content = "Szeretnél most frissíteni?",
PrimaryButtonText = "Frissítés",
SecondaryButtonText = "Később",
IsCloseButtonEnabled = false,
}.ShowDialogAsync();
if (result == Wpf.Ui.Controls.MessageBoxResult.Primary)
await UpdateManager.PerformUpdate();
});
} }
private void CalendarButtonClicked(object sender, RoutedEventArgs e) private void CalendarButtonClicked(object sender, RoutedEventArgs e)

View File

@@ -2,33 +2,69 @@
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using Windows.Media.Protection.PlayReady;
namespace Blueberry namespace Blueberry
{ {
public class UpdateManager public static class UpdateManager
{ {
private const string releaseUrl = "https://git.technopunk.space/api/v1/repos/tomi/Blueberry/releases/latest"; private const string releaseUrl = "https://git.technopunk.space/api/v1/repos/tomi/Blueberry/releases/latest";
public const string CurrentVersion = "0.1.1"; public const string CurrentVersion = "0.1.2";
private static readonly string AppDir = AppDomain.CurrentDomain.BaseDirectory;
private static readonly HttpClient client = new();
public delegate void DownloadCompletedEventArgs();
public static event DownloadCompletedEventArgs DownloadCompleted;
private static bool isDownloading = false;
public async Task CheckAndInstallAsync() public static async Task<bool> IsUpdateAvailable()
{ {
using var client = new HttpClient(); var json = await client.GetStringAsync(releaseUrl);
var release = JsonSerializer.Deserialize<Root>(json);
return release != null && release.tag_name != CurrentVersion;
}
public static async Task DownloadUpdateAsync()
{
client.DefaultRequestHeaders.Add("User-Agent", "Blueberry-Updater"); client.DefaultRequestHeaders.Add("User-Agent", "Blueberry-Updater");
try try
{ {
var json = client.GetStringAsync(releaseUrl).ConfigureAwait(false).GetAwaiter().GetResult(); // 1. Use await here, don't block
var json = await client.GetStringAsync(releaseUrl);
var release = JsonSerializer.Deserialize<Root>(json); var release = JsonSerializer.Deserialize<Root>(json);
if (release == null) if (release == null) return;
throw new NullReferenceException();
if (release.tag_name != CurrentVersion) if (release.tag_name != CurrentVersion)
{ {
var file = release.assets.Find(x => x.name.Contains(".zip")) ?? throw new NullReferenceException(); var file = release.assets.Find(x => x.name.Contains(".zip"));
string downloadUrl = file.browser_download_url; if (file == null) return;
await PerformUpdate(client, downloadUrl);
string zipPath = Path.Combine(AppDir, "blueberry_update.zip");
long offset = 0;
if (File.Exists(zipPath))
{
long localSize = new FileInfo(zipPath).Length;
if (localSize == file.size)
{
DownloadCompleted?.Invoke();
return;
}
if (localSize > file.size)
{
File.Delete(zipPath);
offset = 0;
}
else
{
offset = localSize;
}
}
if (!isDownloading)
await Download(client, file.browser_download_url, offset);
} }
} }
catch (Exception) catch (Exception)
@@ -37,55 +73,45 @@ namespace Blueberry
} }
} }
private async Task PerformUpdate(HttpClient client, string url) private static async Task Download(HttpClient client, string url, long offset = 0)
{
try
{
isDownloading = true;
var request = new HttpRequestMessage(HttpMethod.Get, url);
// If we have an offset, ask the server for the rest of the file
if (offset > 0)
{
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(offset, null);
}
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
// If offset > 0, we APPEND. If 0, we CREATE/OVERWRITE.
var fileMode = offset > 0 ? FileMode.Append : FileMode.Create;
string filePath = Path.Combine(AppDir, "blueberry_update.zip");
using var contentStream = await response.Content.ReadAsStreamAsync();
using var fileStream = new FileStream(filePath, fileMode, FileAccess.Write, FileShare.None);
await contentStream.CopyToAsync(fileStream);
isDownloading = false;
DownloadCompleted?.Invoke();
} catch (Exception)
{
isDownloading = false;
}
}
public static async Task PerformUpdate()
{ {
string tempZip = Path.Combine(Path.GetTempPath(), "blueberry_update.zip"); string tempZip = Path.Combine(Path.GetTempPath(), "blueberry_update.zip");
string currentExe = Process.GetCurrentProcess().MainModule.FileName; string currentExe = Process.GetCurrentProcess().MainModule.FileName;
string appDir = AppDomain.CurrentDomain.BaseDirectory; string appDir = AppDomain.CurrentDomain.BaseDirectory;
// 1. Download string psScript = Constants.UpdateScript;
var data = await client.GetByteArrayAsync(url);
File.WriteAllBytes(tempZip, data);
// 2. Create a temporary batch script to handle the swap
// We use a small delay (timeout) to allow the main app to close fully
string psScript = $@"
# Wait for the main app to close completely
Start-Sleep -Seconds 2
$exePath = '{currentExe}'
$zipPath = '{tempZip}'
$destDir = '{appDir}'
# Retry logic for deletion (in case antivirus or OS holds the lock)
$maxRetries = 10
$retryCount = 0
while ($retryCount -lt $maxRetries) {{
try {{
# Attempt to delete the old executable
if (Test-Path $exePath) {{ Remove-Item $exePath -Force -ErrorAction Stop }}
break # If successful, exit loop
}}
catch {{
Start-Sleep -Milliseconds 500
$retryCount++
}}
}}
# Unzip the new version
Expand-Archive -Path $zipPath -DestinationPath $destDir -Force
# CLEANUP: Delete the zip
Remove-Item $zipPath -Force
# RESTART: Launch the new executable
# 'Start-Process' is the robust way to launch detached processes in PS
Start-Process -FilePath $exePath -WorkingDirectory $destDir
# SELF-DESTRUCT: Remove this script
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force
";
string psPath = Path.Combine(Path.GetTempPath(), "blueberry_updater.ps1"); string psPath = Path.Combine(Path.GetTempPath(), "blueberry_updater.ps1");
File.WriteAllText(psPath, psScript); File.WriteAllText(psPath, psScript);
@@ -114,6 +140,7 @@ Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force
public class Asset public class Asset
{ {
public string name { get; set; } public string name { get; set; }
public long size { get; set; }
public string browser_download_url { get; set; } public string browser_download_url { get; set; }
} }
} }