Added checking for updates
This commit is contained in:
parent
cc15863e81
commit
5ec6525257
19 changed files with 1231 additions and 100 deletions
3
App.xaml
3
App.xaml
|
|
@ -1,8 +1,7 @@
|
|||
<Application x:Class="MossyUpdater.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:MossyUpdater"
|
||||
StartupUri="MainWindow.xaml">
|
||||
xmlns:local="clr-namespace:MossyUpdater">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
|
|
|
|||
50
App.xaml.cs
50
App.xaml.cs
|
|
@ -1,14 +1,52 @@
|
|||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
using System.Windows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Http;
|
||||
using MossyUpdater.Services;
|
||||
using MossyUpdater.ViewModels;
|
||||
|
||||
namespace MossyUpdater
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var mainWindow = new MainWindow
|
||||
{
|
||||
DataContext = _serviceProvider.GetRequiredService<MainViewModel>()
|
||||
};
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
var mainViewModel = _serviceProvider?.GetService<MainViewModel>();
|
||||
mainViewModel?.Dispose();
|
||||
_serviceProvider?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
private static void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ILogger, FileLogger>();
|
||||
services.AddSingleton<ISettingsService, SettingsService>();
|
||||
services.AddSingleton<IQuotesService, QuotesService>();
|
||||
|
||||
services.AddHttpClient<IDownloadService, DownloadService>(client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromMinutes(10);
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "MossyUpdater/1.0");
|
||||
});
|
||||
|
||||
services.AddSingleton<MainViewModel>();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
47
Converters.cs
Normal file
47
Converters.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace MossyUpdater
|
||||
{
|
||||
public class StringToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return string.IsNullOrEmpty(value as string) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class BoolToFontWeightConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is true ? FontWeights.Bold : FontWeights.Normal;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class BoolToUpdateColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is true ? Brushes.Orange : Brushes.Green;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
MainWindow.xaml
102
MainWindow.xaml
|
|
@ -1,7 +1,9 @@
|
|||
<Window x:Class="MossyUpdater.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="MossyUpdater" Height="331" Width="500">
|
||||
xmlns:local="clr-namespace:MossyUpdater"
|
||||
Title="MossyUpdater" Height="450" Width="550" MinHeight="400" MinWidth="500" ResizeMode="CanResize"
|
||||
Icon="pack://application:,,,/MossyUpdater;component/favicon.ico">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
|
|
@ -13,32 +15,38 @@
|
|||
<RowDefinition Height="Auto"/>
|
||||
<!-- 3: filename -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 4: download button -->
|
||||
<!-- 4: update status -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 5: status text -->
|
||||
<!-- 5: download buttons -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 6: progress bar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 7: status text -->
|
||||
<RowDefinition Height="*"/>
|
||||
<!-- 6: image (fills rest) -->
|
||||
<!-- 8: image (fills rest) -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 0 -->
|
||||
<TextBlock Text="MosswartMassacre URL to DLL:" Grid.Row="0" Margin="0,0,0,5"/>
|
||||
|
||||
<!-- 1 -->
|
||||
<TextBox x:Name="UrlTextBox"
|
||||
Grid.Row="1"
|
||||
<TextBox Grid.Row="1"
|
||||
Height="25"
|
||||
Margin="0,0,0,10"
|
||||
Text="https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll"/>
|
||||
Text="{Binding Url, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding CanInteract}"/>
|
||||
|
||||
<!-- 2 -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBox x:Name="FolderTextBox"
|
||||
Width="350"
|
||||
<TextBox Width="350"
|
||||
Height="25"
|
||||
Margin="0,0,5,0"/>
|
||||
Margin="0,0,5,0"
|
||||
Text="{Binding Folder, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding CanInteract}"/>
|
||||
<Button Content="Browse"
|
||||
Width="75"
|
||||
Click="Browse_Click"/>
|
||||
Command="{Binding BrowseCommand}"
|
||||
IsEnabled="{Binding CanInteract}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 3 -->
|
||||
|
|
@ -46,31 +54,71 @@
|
|||
<TextBlock Text="Filename:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,5,0"/>
|
||||
<TextBox x:Name="FilenameTextBox"
|
||||
Width="200"
|
||||
Text="MosswartMassacre.dll"/>
|
||||
<TextBox Width="200"
|
||||
Text="{Binding Filename, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding CanInteract}"/>
|
||||
<Button Content="Check Updates"
|
||||
Width="100"
|
||||
Margin="10,0,0,0"
|
||||
Command="{Binding CheckUpdatesCommand}"
|
||||
IsEnabled="{Binding CanInteract}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 4 -->
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="Download & Unblock"
|
||||
Width="150"
|
||||
Click="Download_Click"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Row="4"
|
||||
Text="{Binding UpdateStatus}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<!-- 5 -->
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
Grid.Row="5"
|
||||
Margin="0,10,0,10"
|
||||
Foreground="Green"/>
|
||||
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,10">
|
||||
<Button Content="Download & Unblock"
|
||||
Width="150"
|
||||
Margin="0,0,10,0"
|
||||
Command="{Binding DownloadCommand}"/>
|
||||
<Button Content="Cancel"
|
||||
Width="75"
|
||||
Command="{Binding CancelCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 6: your image as embedded Resource -->
|
||||
<Image Grid.Row="6"
|
||||
<!-- 6 -->
|
||||
<ProgressBar Grid.Row="6"
|
||||
Height="20"
|
||||
Margin="0,0,0,10"
|
||||
Value="{Binding ProgressValue}"/>
|
||||
|
||||
<!-- 7 -->
|
||||
<TextBlock Grid.Row="7"
|
||||
Text="{Binding StatusMessage}"
|
||||
Margin="0,0,0,10"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{Binding StatusColor}"/>
|
||||
|
||||
<!-- 8: Quote display with image -->
|
||||
<Grid Grid.Row="8" Margin="0,10,0,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Quote -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding CurrentQuote}"
|
||||
Opacity="{Binding QuoteOpacity}"
|
||||
TextWrapping="Wrap"
|
||||
FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Foreground="DarkSlateGray"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
Margin="10,0,10,10"/>
|
||||
|
||||
<!-- Image -->
|
||||
<Image Grid.Row="1"
|
||||
Source="pack://application:,,,/MossyUpdater;component/Mosswart_Mask_Live.png"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,10,0,0"
|
||||
Height="100"/>
|
||||
MaxHeight="100"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Ookii.Dialogs.Wpf; // ← add this
|
||||
using System.Windows;
|
||||
|
||||
namespace MossyUpdater
|
||||
{
|
||||
|
|
@ -13,60 +8,5 @@ namespace MossyUpdater
|
|||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Browse_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new VistaFolderBrowserDialog
|
||||
{
|
||||
Description = "Select a destination folder",
|
||||
UseDescriptionForTitle = true
|
||||
};
|
||||
|
||||
// ShowDialog returns true if the user clicked OK
|
||||
if (dlg.ShowDialog() == true)
|
||||
FolderTextBox.Text = dlg.SelectedPath;
|
||||
}
|
||||
|
||||
private async void Download_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
string url = UrlTextBox.Text.Trim();
|
||||
string folder = FolderTextBox.Text.Trim();
|
||||
string filename = FilenameTextBox.Text.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(url)
|
||||
|| string.IsNullOrEmpty(folder)
|
||||
|| string.IsNullOrEmpty(filename))
|
||||
{
|
||||
StatusTextBlock.Text = "Please fill in all fields.";
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Red;
|
||||
return;
|
||||
}
|
||||
|
||||
string fullPath = Path.Combine(folder, filename);
|
||||
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var data = await client.GetByteArrayAsync(url);
|
||||
await File.WriteAllBytesAsync(fullPath, data);
|
||||
|
||||
UnblockFile(fullPath);
|
||||
|
||||
StatusTextBlock.Text = $"Downloaded and unblocked:\n{fullPath}";
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Green;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusTextBlock.Text = $"Error: {ex.Message}";
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Red;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnblockFile(string path)
|
||||
{
|
||||
var zone = path + ":Zone.Identifier";
|
||||
if (File.Exists(zone))
|
||||
File.Delete(zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Remove="Mosswart_Mask_Live.png" />
|
||||
<None Remove="favicon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.5" />
|
||||
<PackageReference Include="System.Windows.Extensions" Version="9.0.5" />
|
||||
|
|
@ -20,6 +23,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Resource Include="Mosswart_Mask_Live.png" />
|
||||
<Resource Include="favicon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
85
README.md
85
README.md
|
|
@ -1,2 +1,87 @@
|
|||
# MossyUpdater
|
||||
|
||||
A modern Windows desktop application for downloading and unblocking files, featuring an entertaining quote rotation system and robust download management.
|
||||
|
||||
## Features
|
||||
|
||||
### 🚀 **Core Functionality**
|
||||
- **Smart Downloads**: Download files with real-time progress tracking
|
||||
- **Automatic Unblocking**: Removes Windows security restrictions (Zone.Identifier)
|
||||
- **Update Detection**: Checks if remote files have changed
|
||||
- **Retry Logic**: Automatically retries failed downloads (3 attempts)
|
||||
- **Cancellation Support**: Cancel downloads in progress
|
||||
|
||||
### 🎨 **User Experience**
|
||||
- **Quote Rotation**: 50 hilarious "Time According to Alex" quotes with smooth fade transitions
|
||||
- **Progress Visualization**: Real-time download progress with status updates
|
||||
- **Responsive Design**: Resizable window with modern WPF interface
|
||||
- **Settings Persistence**: Remembers your preferences between sessions
|
||||
- **Custom Icon**: Features favicon.ico as the application icon
|
||||
|
||||
### 🔧 **Technical Features**
|
||||
- **MVVM Architecture**: Clean separation of concerns with data binding
|
||||
- **Dependency Injection**: Modern IoC container with service registration
|
||||
- **Streaming Downloads**: Efficient handling of large files
|
||||
- **Comprehensive Logging**: Detailed logs saved to `%AppData%/MossyUpdater/log.txt`
|
||||
- **Error Handling**: User-friendly error messages with detailed logging
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Build the application**:
|
||||
```bash
|
||||
dotnet build
|
||||
```
|
||||
|
||||
2. **Run the application**:
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
3. **Use the application**:
|
||||
- Enter a download URL (defaults to MosswartMassacre.dll)
|
||||
- Browse and select destination folder
|
||||
- Click "Check Updates" to see if file has changed
|
||||
- Click "Download & Unblock" to start the download
|
||||
- Enjoy the rotating quotes while you wait!
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **Platform**: Windows 10/11
|
||||
- **Framework**: .NET 8.0
|
||||
- **Architecture**: x64
|
||||
|
||||
## Publishing
|
||||
|
||||
Create a single-file executable:
|
||||
```bash
|
||||
dotnet publish -c Release -p:PublishProfile=FolderProfile
|
||||
```
|
||||
|
||||
Output location: `bin\Release\net8.0-windows\publish\win-x64\`
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are automatically saved to:
|
||||
- **Settings**: `%AppData%/MossyUpdater/settings.json`
|
||||
- **Logs**: `%AppData%/MossyUpdater/log.txt`
|
||||
|
||||
## Quote System
|
||||
|
||||
Features 50 witty quotes from "Time According to Alex" about time management, rotating every 5 seconds with smooth fade transitions. Sample quotes include:
|
||||
|
||||
> "I don't run late. I glide fashionably through time." - Time According to Alex
|
||||
|
||||
> "Time is an illusion. My ETA is performance art." - Time According to Alex
|
||||
|
||||
## Development
|
||||
|
||||
Built with modern C# and WPF, featuring:
|
||||
- **Services Pattern**: Modular business logic
|
||||
- **MVVM Pattern**: Clean UI separation
|
||||
- **Async/Await**: Non-blocking operations
|
||||
- **Dependency Injection**: Testable architecture
|
||||
- **Resource Management**: Proper disposal patterns
|
||||
|
||||
## License
|
||||
|
||||
Built for the SawatoMosswartsEnjoyersClub/MosswartMassacre project.
|
||||
|
|
|
|||
224
Services/DownloadService.cs
Normal file
224
Services/DownloadService.cs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class DownloadService : IDownloadService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private const int MaxRetries = 3;
|
||||
private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(2);
|
||||
|
||||
public DownloadService(HttpClient httpClient, ILogger logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<DownloadResult> DownloadFileAsync(string url, string destinationPath, IProgress<DownloadProgress>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var result = new DownloadResult();
|
||||
|
||||
await _logger.LogInfoAsync($"Starting download from {url} to {destinationPath}");
|
||||
|
||||
for (int attempt = 1; attempt <= MaxRetries; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (attempt > 1)
|
||||
{
|
||||
progress?.Report(new DownloadProgress { Status = $"Retrying download (attempt {attempt}/{MaxRetries})..." });
|
||||
await _logger.LogInfoAsync($"Retry attempt {attempt}/{MaxRetries}");
|
||||
await Task.Delay(RetryDelay, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
progress?.Report(new DownloadProgress { Status = "Starting download..." });
|
||||
}
|
||||
|
||||
using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var totalBytes = response.Content.Headers.ContentLength;
|
||||
progress?.Report(new DownloadProgress { TotalBytes = totalBytes, Status = "Downloading..." });
|
||||
|
||||
var directory = Path.GetDirectoryName(destinationPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true);
|
||||
|
||||
var buffer = new byte[8192];
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
progress?.Report(new DownloadProgress
|
||||
{
|
||||
BytesReceived = totalBytesRead,
|
||||
TotalBytes = totalBytes,
|
||||
Status = $"Downloaded {FormatBytes(totalBytesRead)}" + (totalBytes.HasValue ? $" of {FormatBytes(totalBytes.Value)}" : "")
|
||||
});
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
result.BytesDownloaded = totalBytesRead;
|
||||
progress?.Report(new DownloadProgress
|
||||
{
|
||||
BytesReceived = totalBytesRead,
|
||||
TotalBytes = totalBytes,
|
||||
Status = "Download completed successfully"
|
||||
});
|
||||
|
||||
await _logger.LogInfoAsync($"Download completed successfully. Downloaded {FormatBytes(totalBytesRead)} in {stopwatch.Elapsed:mm\\:ss}");
|
||||
break;
|
||||
}
|
||||
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException && attempt < MaxRetries)
|
||||
{
|
||||
await _logger.LogWarningAsync($"Timeout on attempt {attempt}: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
result.ErrorMessage = "Download was cancelled";
|
||||
progress?.Report(new DownloadProgress { Status = "Download cancelled" });
|
||||
await _logger.LogInfoAsync("Download was cancelled by user");
|
||||
|
||||
if (File.Exists(destinationPath))
|
||||
{
|
||||
try { File.Delete(destinationPath); } catch { }
|
||||
}
|
||||
break;
|
||||
}
|
||||
catch (HttpRequestException ex) when (IsRetryableHttpError(ex) && attempt < MaxRetries)
|
||||
{
|
||||
await _logger.LogWarningAsync($"Retryable error on attempt {attempt}: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ErrorMessage = GetFriendlyErrorMessage(ex);
|
||||
progress?.Report(new DownloadProgress { Status = $"Error: {result.ErrorMessage}" });
|
||||
await _logger.LogErrorAsync($"Download failed on attempt {attempt}", ex);
|
||||
|
||||
if (File.Exists(destinationPath))
|
||||
{
|
||||
try { File.Delete(destinationPath); } catch { }
|
||||
}
|
||||
|
||||
if (attempt >= MaxRetries)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
result.Duration = stopwatch.Elapsed;
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FileUpdateInfo> CheckForUpdatesAsync(string url, string localFilePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var updateInfo = new FileUpdateInfo();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
updateInfo.RemoteLastModified = response.Content.Headers.LastModified?.DateTime;
|
||||
updateInfo.RemoteSize = response.Content.Headers.ContentLength;
|
||||
|
||||
if (File.Exists(localFilePath))
|
||||
{
|
||||
var localFileInfo = new FileInfo(localFilePath);
|
||||
updateInfo.LocalLastModified = localFileInfo.LastWriteTime;
|
||||
updateInfo.LocalSize = localFileInfo.Length;
|
||||
|
||||
updateInfo.UpdateAvailable =
|
||||
updateInfo.RemoteLastModified.HasValue &&
|
||||
updateInfo.RemoteLastModified > updateInfo.LocalLastModified ||
|
||||
(updateInfo.RemoteSize.HasValue && updateInfo.RemoteSize != updateInfo.LocalSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
updateInfo.UpdateAvailable = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
updateInfo.ErrorMessage = $"Failed to check for updates: {response.StatusCode}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
updateInfo.ErrorMessage = $"Error checking for updates: {ex.Message}";
|
||||
}
|
||||
|
||||
return updateInfo;
|
||||
}
|
||||
|
||||
public void UnblockFile(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var zoneIdentifier = filePath + ":Zone.Identifier";
|
||||
if (File.Exists(zoneIdentifier))
|
||||
{
|
||||
File.Delete(zoneIdentifier);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
|
||||
int counter = 0;
|
||||
decimal number = bytes;
|
||||
|
||||
while (Math.Round(number / 1024) >= 1)
|
||||
{
|
||||
number /= 1024;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return $"{number:n1} {suffixes[counter]}";
|
||||
}
|
||||
|
||||
private static bool IsRetryableHttpError(HttpRequestException ex)
|
||||
{
|
||||
return ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase) ||
|
||||
ex.Message.Contains("connection", StringComparison.OrdinalIgnoreCase) ||
|
||||
ex.Message.Contains("network", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetFriendlyErrorMessage(Exception ex)
|
||||
{
|
||||
return ex switch
|
||||
{
|
||||
HttpRequestException httpEx when httpEx.Message.Contains("404") => "File not found on server",
|
||||
HttpRequestException httpEx when httpEx.Message.Contains("403") => "Access denied to file",
|
||||
HttpRequestException httpEx when httpEx.Message.Contains("timeout") => "Download timed out",
|
||||
UnauthorizedAccessException => "Access denied to destination folder",
|
||||
DirectoryNotFoundException => "Destination folder does not exist",
|
||||
IOException => "File operation failed",
|
||||
_ => ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Services/FileLogger.cs
Normal file
52
Services/FileLogger.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.IO;
|
||||
|
||||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class FileLogger : ILogger
|
||||
{
|
||||
private readonly string _logPath;
|
||||
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||
|
||||
public FileLogger()
|
||||
{
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var appFolder = Path.Combine(appDataPath, "MossyUpdater");
|
||||
Directory.CreateDirectory(appFolder);
|
||||
_logPath = Path.Combine(appFolder, "log.txt");
|
||||
}
|
||||
|
||||
public async Task LogInfoAsync(string message)
|
||||
{
|
||||
await WriteLogAsync(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
public async Task LogWarningAsync(string message)
|
||||
{
|
||||
await WriteLogAsync(LogLevel.Warning, message);
|
||||
}
|
||||
|
||||
public async Task LogErrorAsync(string message, Exception? exception = null)
|
||||
{
|
||||
var fullMessage = exception != null ? $"{message} - Exception: {exception}" : message;
|
||||
await WriteLogAsync(LogLevel.Error, fullMessage);
|
||||
}
|
||||
|
||||
private async Task WriteLogAsync(LogLevel level, string message)
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
var logEntry = $"[{timestamp}] [{level}] {message}{Environment.NewLine}";
|
||||
await File.AppendAllTextAsync(_logPath, logEntry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Services/IDownloadService.cs
Normal file
37
Services/IDownloadService.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface IDownloadService
|
||||
{
|
||||
Task<DownloadResult> DownloadFileAsync(string url, string destinationPath, IProgress<DownloadProgress>? progress = null, CancellationToken cancellationToken = default);
|
||||
Task<FileUpdateInfo> CheckForUpdatesAsync(string url, string localFilePath, CancellationToken cancellationToken = default);
|
||||
void UnblockFile(string filePath);
|
||||
}
|
||||
|
||||
public class DownloadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public long BytesDownloaded { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
}
|
||||
|
||||
public class DownloadProgress
|
||||
{
|
||||
public long BytesReceived { get; set; }
|
||||
public long? TotalBytes { get; set; }
|
||||
public double ProgressPercentage => TotalBytes.HasValue && TotalBytes > 0
|
||||
? (double)BytesReceived / TotalBytes.Value * 100
|
||||
: 0;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class FileUpdateInfo
|
||||
{
|
||||
public bool UpdateAvailable { get; set; }
|
||||
public DateTime? RemoteLastModified { get; set; }
|
||||
public DateTime? LocalLastModified { get; set; }
|
||||
public long? RemoteSize { get; set; }
|
||||
public long? LocalSize { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
16
Services/ILogger.cs
Normal file
16
Services/ILogger.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface ILogger
|
||||
{
|
||||
Task LogInfoAsync(string message);
|
||||
Task LogWarningAsync(string message);
|
||||
Task LogErrorAsync(string message, Exception? exception = null);
|
||||
}
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
9
Services/IQuotesService.cs
Normal file
9
Services/IQuotesService.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface IQuotesService
|
||||
{
|
||||
string GetRandomQuote();
|
||||
string GetNextQuote();
|
||||
IEnumerable<string> GetAllQuotes();
|
||||
}
|
||||
}
|
||||
19
Services/ISettingsService.cs
Normal file
19
Services/ISettingsService.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<AppSettings> LoadSettingsAsync();
|
||||
Task SaveSettingsAsync(AppSettings settings);
|
||||
}
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public string LastUsedFolder { get; set; } = string.Empty;
|
||||
public string DefaultUrl { get; set; } = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll";
|
||||
public string DefaultFilename { get; set; } = "MosswartMassacre.dll";
|
||||
public double WindowWidth { get; set; } = 550;
|
||||
public double WindowHeight { get; set; } = 450;
|
||||
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
||||
public int DownloadTimeoutMinutes { get; set; } = 10;
|
||||
}
|
||||
}
|
||||
80
Services/QuotesService.cs
Normal file
80
Services/QuotesService.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class QuotesService : IQuotesService
|
||||
{
|
||||
private readonly string[] _quotes = new[]
|
||||
{
|
||||
"I don't run late. I glide fashionably through time.",
|
||||
"Time is a flat circle—and I'm still trying to find the starting point.",
|
||||
"Being on time is like spotting a unicorn: rare, beautiful, and definitely not me.",
|
||||
"Time zones are a social construct. I operate on instinct.",
|
||||
"Every time I try to be early, time responds with traffic.",
|
||||
"Time is an illusion. My ETA is performance art.",
|
||||
"I treat deadlines like speed limits: suggestions.",
|
||||
"I don't set alarms. I set vague intentions.",
|
||||
"If you want me to be on time, lie about when it starts.",
|
||||
"Why stress about time when you can just embrace chaos?",
|
||||
"My to-do list is a historical document.",
|
||||
"Time and I broke up. It was toxic.",
|
||||
"I'm not late. I'm temporally creative.",
|
||||
"'In five minutes' means something different in my language.",
|
||||
"I don't track time. I let it track me—poorly.",
|
||||
"Time management is easy. Just avoid doing anything.",
|
||||
"Time is fleeting. So is my attention span.",
|
||||
"I live in the moment. Just never the right one.",
|
||||
"Time doesn't control me. It just heavily inconveniences me.",
|
||||
"I'm a time optimist: always wrong, always hopeful.",
|
||||
"Punctuality is for people who don't trust spontaneity.",
|
||||
"My life is a series of missed trains and strong coffee.",
|
||||
"If time is a river, I'm definitely upstream without a paddle.",
|
||||
"I don't watch the clock. I avoid eye contact with it.",
|
||||
"'ASAP' means 'as soon as procrastination ends.'",
|
||||
"Early is suspicious. On time is impressive. Late is expected.",
|
||||
"Time management? I prefer time improvisation.",
|
||||
"I like to keep time on its toes. Mostly by ignoring it.",
|
||||
"Alarms are like plot twists. I didn't see it coming, and I still ignore it.",
|
||||
"If being late was a sport, I'd already have missed the medal ceremony.",
|
||||
"I don't lose track of time. I just pretend it doesn't exist.",
|
||||
"Watches are decorative lies.",
|
||||
"Time flies. I miss every flight.",
|
||||
"My planner is just a coloring book for stress.",
|
||||
"I'm not in a hurry. I'm in denial.",
|
||||
"I live in the now, just usually a little bit after everyone else.",
|
||||
"My calendar and I are estranged, but we're working on it.",
|
||||
"The early bird catches the worm. I order food later.",
|
||||
"Time moves fast. I move slower.",
|
||||
"I was going to be on time, but then I remembered who I am.",
|
||||
"I see '9:00 AM' and read it as 'guideline.'",
|
||||
"Every plan is a maybe with extra steps.",
|
||||
"I don't have time blindness—I just have time indifference.",
|
||||
"I'm not late. Reality is early.",
|
||||
"Time and I aren't speaking after what happened last Monday.",
|
||||
"Being on time is impressive. Being consistently late is a brand.",
|
||||
"I'm in sync with the universe—just in a different dimension.",
|
||||
"My sense of time is like my sock drawer: chaotic and mostly missing.",
|
||||
"Some people chase time. I let it wander off.",
|
||||
"I'm not running late. I'm setting the tone for a relaxed experience."
|
||||
};
|
||||
|
||||
private int _currentIndex = 0;
|
||||
private readonly Random _random = new();
|
||||
|
||||
public string GetRandomQuote()
|
||||
{
|
||||
var index = _random.Next(_quotes.Length);
|
||||
return $"\"{_quotes[index]}\" - Time According to Alex";
|
||||
}
|
||||
|
||||
public string GetNextQuote()
|
||||
{
|
||||
var quote = $"\"{_quotes[_currentIndex]}\" - Time According to Alex";
|
||||
_currentIndex = (_currentIndex + 1) % _quotes.Length;
|
||||
return quote;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAllQuotes()
|
||||
{
|
||||
return _quotes.Select(q => $"\"{q}\" - Time According to Alex");
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Services/SettingsService.cs
Normal file
50
Services/SettingsService.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MossyUpdater.Services
|
||||
{
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private readonly string _settingsPath;
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var appFolder = Path.Combine(appDataPath, "MossyUpdater");
|
||||
Directory.CreateDirectory(appFolder);
|
||||
_settingsPath = Path.Combine(appFolder, "settings.json");
|
||||
}
|
||||
|
||||
public async Task<AppSettings> LoadSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_settingsPath))
|
||||
return new AppSettings();
|
||||
|
||||
var json = await File.ReadAllTextAsync(_settingsPath);
|
||||
var settings = JsonSerializer.Deserialize<AppSettings>(json);
|
||||
return settings ?? new AppSettings();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new AppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveSettingsAsync(AppSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
await File.WriteAllTextAsync(_settingsPath, json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
364
ViewModels/MainViewModel.cs
Normal file
364
ViewModels/MainViewModel.cs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Input;
|
||||
using MossyUpdater.Services;
|
||||
using Ookii.Dialogs.Wpf;
|
||||
|
||||
namespace MossyUpdater.ViewModels
|
||||
{
|
||||
public class MainViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
private readonly IDownloadService _downloadService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IQuotesService _quotesService;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private System.Windows.Threading.DispatcherTimer? _quoteTimer;
|
||||
|
||||
private string _url = "https://git.snakedesert.se/SawatoMosswartsEnjoyersClub/MosswartMassacre/raw/branch/spawn-detection/MosswartMassacre/bin/Release/MosswartMassacre.dll";
|
||||
private string _folder = string.Empty;
|
||||
private string _filename = "MosswartMassacre.dll";
|
||||
private string _statusMessage = string.Empty;
|
||||
private System.Windows.Media.Brush _statusColor = System.Windows.Media.Brushes.Green;
|
||||
private double _progressValue = 0;
|
||||
private bool _isProgressVisible = false;
|
||||
private bool _isDownloading = false;
|
||||
private string _updateStatus = string.Empty;
|
||||
private bool _updateAvailable = false;
|
||||
private string _currentQuote = string.Empty;
|
||||
private double _quoteOpacity = 1.0;
|
||||
|
||||
public MainViewModel(IDownloadService downloadService, ISettingsService settingsService, ILogger logger, IQuotesService quotesService)
|
||||
{
|
||||
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
||||
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_quotesService = quotesService ?? throw new ArgumentNullException(nameof(quotesService));
|
||||
|
||||
BrowseCommand = new RelayCommand(BrowseFolder);
|
||||
DownloadCommand = new AsyncRelayCommand(DownloadAndUnblock, CanDownload);
|
||||
CancelCommand = new RelayCommand(CancelDownload, () => IsDownloading);
|
||||
CheckUpdatesCommand = new AsyncRelayCommand(CheckForUpdates);
|
||||
|
||||
InitializeQuoteRotation();
|
||||
_ = LoadSettingsAsync();
|
||||
}
|
||||
|
||||
public string Url
|
||||
{
|
||||
get => _url;
|
||||
set => SetProperty(ref _url, value);
|
||||
}
|
||||
|
||||
public string Folder
|
||||
{
|
||||
get => _folder;
|
||||
set => SetProperty(ref _folder, value);
|
||||
}
|
||||
|
||||
public string Filename
|
||||
{
|
||||
get => _filename;
|
||||
set => SetProperty(ref _filename, value);
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public System.Windows.Media.Brush StatusColor
|
||||
{
|
||||
get => _statusColor;
|
||||
set => SetProperty(ref _statusColor, value);
|
||||
}
|
||||
|
||||
public double ProgressValue
|
||||
{
|
||||
get => _progressValue;
|
||||
set => SetProperty(ref _progressValue, value);
|
||||
}
|
||||
|
||||
public bool IsProgressVisible
|
||||
{
|
||||
get => _isProgressVisible;
|
||||
set => SetProperty(ref _isProgressVisible, value);
|
||||
}
|
||||
|
||||
public bool IsDownloading
|
||||
{
|
||||
get => _isDownloading;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _isDownloading, value);
|
||||
OnPropertyChanged(nameof(CanInteract));
|
||||
}
|
||||
}
|
||||
|
||||
public string UpdateStatus
|
||||
{
|
||||
get => _updateStatus;
|
||||
set => SetProperty(ref _updateStatus, value);
|
||||
}
|
||||
|
||||
public bool UpdateAvailable
|
||||
{
|
||||
get => _updateAvailable;
|
||||
set => SetProperty(ref _updateAvailable, value);
|
||||
}
|
||||
|
||||
public string CurrentQuote
|
||||
{
|
||||
get => _currentQuote;
|
||||
set => SetProperty(ref _currentQuote, value);
|
||||
}
|
||||
|
||||
public double QuoteOpacity
|
||||
{
|
||||
get => _quoteOpacity;
|
||||
set => SetProperty(ref _quoteOpacity, value);
|
||||
}
|
||||
|
||||
public bool CanInteract => !IsDownloading;
|
||||
|
||||
public ICommand BrowseCommand { get; }
|
||||
public ICommand DownloadCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
public ICommand CheckUpdatesCommand { get; }
|
||||
|
||||
private void BrowseFolder()
|
||||
{
|
||||
var dialog = new VistaFolderBrowserDialog
|
||||
{
|
||||
Description = "Select a destination folder",
|
||||
UseDescriptionForTitle = true,
|
||||
SelectedPath = Folder
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
Folder = dialog.SelectedPath;
|
||||
_ = SaveSettingsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanDownload()
|
||||
{
|
||||
return !IsDownloading &&
|
||||
!string.IsNullOrWhiteSpace(Url) &&
|
||||
!string.IsNullOrWhiteSpace(Folder) &&
|
||||
!string.IsNullOrWhiteSpace(Filename);
|
||||
}
|
||||
|
||||
private async Task DownloadAndUnblock()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Url) || string.IsNullOrWhiteSpace(Folder) || string.IsNullOrWhiteSpace(Filename))
|
||||
{
|
||||
SetStatus("Please fill in all fields.", System.Windows.Media.Brushes.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
var fullPath = Path.Combine(Folder, Filename);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
IsDownloading = true;
|
||||
IsProgressVisible = true;
|
||||
ProgressValue = 0;
|
||||
|
||||
var progress = new Progress<DownloadProgress>(OnDownloadProgress);
|
||||
var result = await _downloadService.DownloadFileAsync(Url, fullPath, progress, _cancellationTokenSource.Token);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_downloadService.UnblockFile(fullPath);
|
||||
SetStatus($"Downloaded and unblocked successfully!\n{fullPath}\nSize: {FormatBytes(result.BytesDownloaded)}, Duration: {result.Duration:mm\\:ss}", System.Windows.Media.Brushes.Green);
|
||||
ProgressValue = 100;
|
||||
|
||||
await CheckForUpdates();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(result.ErrorMessage))
|
||||
{
|
||||
SetStatus($"Download failed: {result.ErrorMessage}", System.Windows.Media.Brushes.Red);
|
||||
ProgressValue = 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus($"Unexpected error: {ex.Message}", System.Windows.Media.Brushes.Red);
|
||||
ProgressValue = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsDownloading = false;
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
|
||||
await Task.Delay(3000);
|
||||
IsProgressVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelDownload()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
SetStatus("Download cancelled by user.", System.Windows.Media.Brushes.Orange);
|
||||
}
|
||||
|
||||
private async Task CheckForUpdates()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Url) || string.IsNullOrWhiteSpace(Folder) || string.IsNullOrWhiteSpace(Filename))
|
||||
{
|
||||
UpdateStatus = "";
|
||||
UpdateAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var localPath = Path.Combine(Folder, Filename);
|
||||
|
||||
try
|
||||
{
|
||||
var updateInfo = await _downloadService.CheckForUpdatesAsync(Url, localPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(updateInfo.ErrorMessage))
|
||||
{
|
||||
UpdateStatus = $"Update check failed: {updateInfo.ErrorMessage}";
|
||||
UpdateAvailable = false;
|
||||
}
|
||||
else if (updateInfo.UpdateAvailable)
|
||||
{
|
||||
UpdateStatus = "Update available! Remote file is newer or different.";
|
||||
UpdateAvailable = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus = "File is up to date.";
|
||||
UpdateAvailable = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus = $"Update check error: {ex.Message}";
|
||||
UpdateAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDownloadProgress(DownloadProgress progress)
|
||||
{
|
||||
ProgressValue = progress.ProgressPercentage;
|
||||
SetStatus(progress.Status, System.Windows.Media.Brushes.Blue);
|
||||
}
|
||||
|
||||
private void SetStatus(string message, System.Windows.Media.Brush color)
|
||||
{
|
||||
StatusMessage = message;
|
||||
StatusColor = color;
|
||||
}
|
||||
|
||||
private async Task LoadSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = await _settingsService.LoadSettingsAsync();
|
||||
Url = settings.DefaultUrl;
|
||||
Folder = settings.LastUsedFolder;
|
||||
Filename = settings.DefaultFilename;
|
||||
|
||||
if (settings.CheckForUpdatesOnStartup && !string.IsNullOrWhiteSpace(Url) && !string.IsNullOrWhiteSpace(Folder) && !string.IsNullOrWhiteSpace(Filename))
|
||||
{
|
||||
await CheckForUpdates();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _logger.LogErrorAsync("Failed to load settings", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSettingsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = new AppSettings
|
||||
{
|
||||
DefaultUrl = Url,
|
||||
LastUsedFolder = Folder,
|
||||
DefaultFilename = Filename,
|
||||
CheckForUpdatesOnStartup = true,
|
||||
DownloadTimeoutMinutes = 10
|
||||
};
|
||||
|
||||
await _settingsService.SaveSettingsAsync(settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _logger.LogErrorAsync("Failed to save settings", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
|
||||
int counter = 0;
|
||||
decimal number = bytes;
|
||||
|
||||
while (Math.Round(number / 1024) >= 1)
|
||||
{
|
||||
number /= 1024;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return $"{number:n1} {suffixes[counter]}";
|
||||
}
|
||||
|
||||
private void InitializeQuoteRotation()
|
||||
{
|
||||
CurrentQuote = _quotesService.GetNextQuote();
|
||||
|
||||
_quoteTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(5)
|
||||
};
|
||||
_quoteTimer.Tick += OnQuoteTimerTick;
|
||||
_quoteTimer.Start();
|
||||
}
|
||||
|
||||
private async void OnQuoteTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
await RotateQuoteWithFade();
|
||||
}
|
||||
|
||||
private async Task RotateQuoteWithFade()
|
||||
{
|
||||
const double fadeOutDuration = 300;
|
||||
const double fadeInDuration = 300;
|
||||
const int fadeSteps = 30;
|
||||
|
||||
var fadeOutDelay = TimeSpan.FromMilliseconds(fadeOutDuration / fadeSteps);
|
||||
var fadeInDelay = TimeSpan.FromMilliseconds(fadeInDuration / fadeSteps);
|
||||
|
||||
for (int i = fadeSteps; i >= 0; i--)
|
||||
{
|
||||
QuoteOpacity = (double)i / fadeSteps;
|
||||
await Task.Delay(fadeOutDelay);
|
||||
}
|
||||
|
||||
CurrentQuote = _quotesService.GetNextQuote();
|
||||
|
||||
for (int i = 0; i <= fadeSteps; i++)
|
||||
{
|
||||
QuoteOpacity = (double)i / fadeSteps;
|
||||
await Task.Delay(fadeInDelay);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_quoteTimer?.Stop();
|
||||
_quoteTimer = null;
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
94
ViewModels/RelayCommand.cs
Normal file
94
ViewModels/RelayCommand.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
using System.Windows.Input;
|
||||
|
||||
namespace MossyUpdater.ViewModels
|
||||
{
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action<object?> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
|
||||
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
: this(_ => execute(), canExecute == null ? null : _ => canExecute())
|
||||
{
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add { CommandManager.RequerySuggested += value; }
|
||||
remove { CommandManager.RequerySuggested -= value; }
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return _canExecute?.Invoke(parameter) ?? true;
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
_execute(parameter);
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
}
|
||||
|
||||
public class AsyncRelayCommand : ICommand
|
||||
{
|
||||
private readonly Func<object?, Task> _execute;
|
||||
private readonly Predicate<object?>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
|
||||
public AsyncRelayCommand(Func<object?, Task> execute, Predicate<object?>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
: this(_ => execute(), canExecute == null ? null : _ => canExecute())
|
||||
{
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged
|
||||
{
|
||||
add { CommandManager.RequerySuggested += value; }
|
||||
remove { CommandManager.RequerySuggested -= value; }
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
|
||||
}
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_isExecuting = true;
|
||||
RaiseCanExecuteChanged();
|
||||
await _execute(parameter);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged()
|
||||
{
|
||||
CommandManager.InvalidateRequerySuggested();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
ViewModels/ViewModelBase.cs
Normal file
25
ViewModels/ViewModelBase.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MossyUpdater.ViewModels
|
||||
{
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
return false;
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
Loading…
Add table
Add a link
Reference in a new issue