From 1ec34cd0f6b40a3e10520c7aa6b018a7fa28bafc Mon Sep 17 00:00:00 2001 From: rimtty Date: Sat, 8 Aug 2026 15:59:05 +0900 Subject: [PATCH] Add GitHub release update checks --- CHANGELOG.md | 6 + README.ja.md | 3 +- README.md | 3 +- docs/architecture/README.md | 4 +- docs/privacy.md | 4 +- docs/release/update-policy.md | 2 +- docs/user-guide.md | 5 +- src/QuickPods.App/App.xaml.cs | 201 ++++++++++++++++++ .../ApplicationUpdateViewState.cs | 31 +++ src/QuickPods.App/MainWindow.xaml.cs | 42 +++- src/QuickPods.App/SettingsWindow.xaml | 45 +++- src/QuickPods.App/SettingsWindow.xaml.cs | 76 +++++++ src/QuickPods.App/TrayIconController.cs | 39 ++++ .../Models/ApplicationUpdates.cs | 10 + src/QuickPods.Core/Models/QuickPodsState.cs | 4 + .../Ports/IApplicationUpdateChecker.cs | 10 + .../Updates/GitHubReleaseUpdateChecker.cs | 89 ++++++++ .../ApplicationUpdatePolicy.cs | 48 +++++ .../ProductLocalization.cs | 42 +++- .../ApplicationUpdatePolicyTests.cs | 54 +++++ .../GitHubReleaseUpdateCheckerTests.cs | 115 ++++++++++ .../JsonSettingsStoreTests.cs | 24 ++- .../ProductLocalizationTests.cs | 22 ++ 23 files changed, 863 insertions(+), 16 deletions(-) create mode 100644 src/QuickPods.App/ApplicationUpdateViewState.cs create mode 100644 src/QuickPods.Core/Models/ApplicationUpdates.cs create mode 100644 src/QuickPods.Core/Ports/IApplicationUpdateChecker.cs create mode 100644 src/QuickPods.Infrastructure/Updates/GitHubReleaseUpdateChecker.cs create mode 100644 src/QuickPods.Presentation/ApplicationUpdatePolicy.cs create mode 100644 tests/QuickPods.Foundation.Tests/ApplicationUpdatePolicyTests.cs create mode 100644 tests/QuickPods.Foundation.Tests/GitHubReleaseUpdateCheckerTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 736c478..30d4eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,18 @@ All notable changes to QuickPods will be documented in this file. The format fol ## [Unreleased] +### Added + +- Added manual latest-release checks from the notification-area menu and Settings. +- Added an opt-in startup update check limited to once every 24 hours. + ### Changed - Reorganized the portable package around an obvious root `QuickPods.exe` launcher. - Moved the application runtime and helper executables into an internal `app` directory. - Collected license and third-party notice files in a dedicated `licenses` directory. - Added a bilingual package README with startup, security, update, and internal-file guidance. +- Clarified the existing tray refresh action as a device-status refresh. ## [0.1.1] - 2026-08-08 diff --git a/README.ja.md b/README.ja.md index f354a1e..36d6396 100644 --- a/README.ja.md +++ b/README.ja.md @@ -27,6 +27,7 @@ - Windows側で行われた音声・Bluetooth状態変更をアプリ再起動なしで追従 - 安全なタスクバー配置を確認できない場合は通知領域へフォールバック - 日本語Windowsでは日本語、それ以外では英語を使用し、設定画面から明示的に変更可能 +- GitHubの最新安定版Releaseを手動、または明示的に有効化した場合のみ起動時に1日1回確認 - 管理者権限を要求せず、設定と診断ログをユーザー単位で保存 ## 動作要件 @@ -50,7 +51,7 @@ ## プライバシーと安全性 -QuickPodsはローカルで動作し、テレメトリやネットワークサービスを含みません。ログにはBluetoothアドレス、Container ID、PnP ID、Endpoint ID、アカウント名などの生の識別子を記録しない設計です。Issueへ診断情報を添付する前に[プライバシー情報](docs/privacy.md)を確認してください。 +QuickPodsはローカルで動作し、テレメトリやネットワークサービスを含みません。手動確認時、および明示的に有効化した起動時確認では、GitHubから最新の公開Release情報だけを取得します。ログにはBluetoothアドレス、Container ID、PnP ID、Endpoint ID、アカウント名などの生の識別子を記録しない設計です。Issueへ診断情報を添付する前に[プライバシー情報](docs/privacy.md)を確認してください。 ## 開発・コントリビューション diff --git a/README.md b/README.md index 1172f01..fda85a7 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ - Follow external Windows audio and Bluetooth changes without requiring an app restart. - Fall back to the notification area when safe taskbar placement cannot be verified. - Use English by default, Japanese on Japanese Windows, or an explicit language selected in Settings. +- Check the latest stable GitHub Release manually or once per day at startup when explicitly enabled. - Store settings and diagnostics per user without requiring administrator privileges. ## Requirements @@ -63,7 +64,7 @@ Some Windows desktop capabilities used by QuickPods are not stable public extens ## Privacy and safety -QuickPods runs locally. It does not include telemetry or a network service. Logs intentionally omit raw Bluetooth addresses, Container IDs, PnP IDs, endpoint IDs, account names, and other device identifiers. Review the [privacy notes](docs/privacy.md) before attaching diagnostics to an issue. +QuickPods runs locally and does not include telemetry or a network service. Manual update checks—and optional startup checks when enabled—request only the latest public release metadata from GitHub. Logs intentionally omit raw Bluetooth addresses, Container IDs, PnP IDs, endpoint IDs, account names, and other device identifiers. Review the [privacy notes](docs/privacy.md) before attaching diagnostics to an issue. ## Build and test diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 15b0ae0..5234208 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -28,7 +28,7 @@ The app owns settings, logs, startup registration, and child-process lifetime. H | `QuickPods.Core` | product state and use-case coordination | | `QuickPods.Presentation` | language-independent presentation and localization | | `QuickPods.Contracts` | versioned process contracts and shared glyph definitions | -| `QuickPods.Infrastructure` | settings, logging, single-instance, and host supervision | +| `QuickPods.Infrastructure` | settings, logging, update metadata retrieval, single-instance, and host supervision | | `QuickPods.Windows` | Core Audio, Bluetooth, registry, and Windows Settings adapters | | `QuickPods.TaskbarHost` | out-of-process taskbar surface and placement policy | | `QuickPods.TaskbarObserver` | Explorer-generation UI Automation observation | @@ -60,6 +60,8 @@ Settings use atomic JSON replacement under `%LocalAppData%\QuickPods`. A malform Structured diagnostics contain state classifications and counts, not raw device or account identifiers. See [Privacy](../privacy.md). +Update availability is checked through the public GitHub latest-release endpoint behind `IApplicationUpdateChecker`. Requests run asynchronously, stable-release metadata is validated against the official repository URL, and successful check metadata is stored with the user settings. Automatic checks are opt-in and throttled to once every 24 hours. Downloading and applying packages remains a separate manual operation. + ## Distribution Release payloads are x64 and self-contained. The portable root contains one obvious launcher, keeps runtime files under `app`, and collects legal notices under `licenses`. The MSI installs the same layout per user without elevation, preserves settings during upgrade and uninstall, and removes the startup value owned by QuickPods on complete uninstall. Release signing remains fail-closed when MSI distribution is enabled. diff --git a/docs/privacy.md b/docs/privacy.md index 5b5175e..c6ad003 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -1,12 +1,12 @@ # Privacy -QuickPods is a local Windows desktop application. It does not include telemetry, analytics, advertising, cloud synchronization, or a network service. +QuickPods is a local Windows desktop application. It does not include telemetry, analytics, advertising, cloud synchronization, or a network service. A manual update check—and an optional startup check when explicitly enabled—makes an HTTPS request to GitHub's public Releases API for `rimtty/QuickPods`. The request includes the QuickPods version in its standard HTTP user-agent and does not include Bluetooth, audio, account, or settings data. Automatic checks are disabled by default and limited to once every 24 hours. ## Local data QuickPods stores data below `%LocalAppData%\QuickPods`: -- `settings.json` — user preferences and an opaque selected-device key; +- `settings.json` — user preferences, an opaque selected-device key, and the time, version, and official page URL from the last successful update check; - `logs\quickpods-YYYYMMDD.jsonl` — structured operational diagnostics; - `settings.corrupt-*.json` — a quarantined settings file only when malformed JSON is detected. diff --git a/docs/release/update-policy.md b/docs/release/update-policy.md index 64524b9..71bc979 100644 --- a/docs/release/update-policy.md +++ b/docs/release/update-policy.md @@ -1,6 +1,6 @@ # Update policy -QuickPods currently uses manual portable ZIP updates. +QuickPods checks update availability but currently applies portable ZIP updates manually. Use **Check for updates...** in the notification-area menu or **Check for updates** in Settings. Optional startup checks are disabled by default and run at most once every 24 hours when enabled. The checker reads the latest stable public Release metadata and opens the official Release page; it does not download or execute an update. 1. Exit QuickPods from the notification-area menu. 2. Download the ZIP only from the official GitHub Release and verify its published SHA-256 checksum. diff --git a/docs/user-guide.md b/docs/user-guide.md index 36ca6dd..52e9085 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -55,8 +55,11 @@ Open Settings from the flyout gear button or the notification-area menu. - **Set connected device as the default audio device** — update Console and Multimedia roles after connection. - **Confirm before disconnecting Bluetooth** — require confirmation for disconnect actions. - **Start QuickPods when signing in to Windows** — create or remove the current-user startup entry. +- **Check for updates when QuickPods starts** — when explicitly enabled, check the latest stable GitHub Release at most once every 24 hours. -Settings are saved automatically. **Reset to defaults** restores editable preferences, **Restart** restarts QuickPods after orderly shutdown, and **Close** closes only the Settings window. +Choose **Check for updates** in Settings or **Check for updates...** in the notification-area menu for an immediate manual check. When a newer version is available, QuickPods shows the version and opens the official GitHub Release page on request. It does not download or execute the package automatically; follow the checksum and replacement steps in the [update policy](release/update-policy.md). + +Settings are saved automatically. Automatic update checks are disabled by default. **Reset to defaults** restores editable preferences, **Restart** restarts QuickPods after orderly shutdown, and **Close** closes only the Settings window. ## Exit QuickPods diff --git a/src/QuickPods.App/App.xaml.cs b/src/QuickPods.App/App.xaml.cs index ceb4b24..76891dc 100644 --- a/src/QuickPods.App/App.xaml.cs +++ b/src/QuickPods.App/App.xaml.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Net.Http; using System.Text; using System.Windows; using System.Windows.Media; @@ -13,6 +14,7 @@ using QuickPods.Infrastructure.Logging; using QuickPods.Infrastructure.Runtime; using QuickPods.Infrastructure.Settings; +using QuickPods.Infrastructure.Updates; using QuickPods.Presentation; using QuickPods.Windows.Audio; using QuickPods.Windows.Bluetooth; @@ -43,13 +45,18 @@ public partial class App : WpfApplication, IDisposable private TrayIconController? trayIcon; private WindowsCoreAudioEndpointPort? windowsAudio; private JsonLineLogger? logger; + private HttpClient? updateHttpClient; + private IApplicationUpdateChecker? updateChecker; private string logsDirectory = string.Empty; private string? restartExecutable; + private readonly object updateCheckSync = new(); private readonly ProductLocalizer localizer = new( ProductLanguageResolver.Resolve(QuickPodsLanguageMode.System)); private readonly ProductLifetimePolicy lifetimePolicy = new(); private readonly HashSet pendingLifecycleReasons = new(StringComparer.Ordinal); private QuickPodsSettings productSettings = QuickPodsSettings.Default; + private ApplicationUpdateViewState updateViewState = ApplicationUpdateViewState.NotChecked; + private Task? activeUpdateCheck; private TaskbarThemeMode resolvedTaskbarTheme = TaskbarThemeMode.Dark; private DispatcherTimer? lifecycleRecoveryTimer; private DispatcherTimer? flyoutDismissTimer; @@ -107,6 +114,11 @@ protected override void OnStartup(StartupEventArgs e) controller = new AudioController(audioPort); InitializeSettingsStore(); + updateHttpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(12), + }; + updateChecker = new GitHubReleaseUpdateChecker(updateHttpClient); StartBluetoothCatalog(); var settingsLauncher = new WindowsSettingsLauncher(); string applicationExecutable = Path.Combine(AppContext.BaseDirectory, "QuickPods.exe"); @@ -120,6 +132,9 @@ protected override void OnStartup(StartupEventArgs e) startupRegistration, lifetimePolicy, RestartApplication, + () => updateViewState, + () => CheckForUpdatesAsync(ApplicationUpdateCheckTrigger.Settings), + OpenReleasePage, localizer, logsDirectory, startupDiagnostic); @@ -154,6 +169,7 @@ protected override void OnStartup(StartupEventArgs e) trayIcon = new TrayIconController( ShowMainWindow, () => _ = window.RequestRefreshAsync(), + () => _ = CheckForUpdatesAsync(ApplicationUpdateCheckTrigger.Tray), window.ShowSettingsWindow, visible => _ = window.SetTaskbarSurfaceVisibleAsync(visible), () => settingsLauncher.TryOpenSoundSettings(), @@ -316,6 +332,9 @@ public void Dispose() bluetoothCatalogPort?.Dispose(); bluetoothLifetime?.Dispose(); windowsAudio?.Dispose(); + updateHttpClient?.Dispose(); + updateHttpClient = null; + updateChecker = null; logger?.DisposeAsync().AsTask().GetAwaiter().GetResult(); logger = null; if (singleInstance is not null) @@ -415,6 +434,8 @@ private void OnProductSettingsChanged(QuickPodsSettings settings) return; } + bool firstApplication = !productSettingsInitialized; + bool automaticChecksWereEnabled = productSettings.CheckForUpdatesAtStartup; productSettings = normalized; productSettingsInitialized = true; ProductLanguage resolvedLanguage = ProductLanguageResolver.Resolve( @@ -444,14 +465,194 @@ private void OnProductSettingsChanged(QuickPodsSettings settings) ["ConfirmBluetoothDisconnect"] = productSettings.ConfirmBluetoothDisconnect, ["StartWithWindows"] = productSettings.StartWithWindows, + ["CheckForUpdatesAtStartup"] = productSettings.CheckForUpdatesAtStartup, }); ApplyTheme(productSettings.Theme); trayIcon?.SetTaskbarSurfaceVisible( productSettings.DisplayMode != QuickPodsDisplayMode.TrayOnly); taskbarHost?.Publish(CreateTaskbarSnapshot( controller?.State ?? AudioState.Unavailable)); + + if (firstApplication) + { + ApplicationUpdateCheckResult? cached = ApplicationUpdatePolicy.TryCreateCachedResult( + productSettings, + GetProductVersion()); + if (cached is not null) + { + SetUpdateViewState(CreateUpdateViewState(cached)); + } + } + + if (productSettings.CheckForUpdatesAtStartup && + (firstApplication || !automaticChecksWereEnabled) && + ApplicationUpdatePolicy.ShouldCheckAutomatically( + productSettings, + DateTimeOffset.UtcNow)) + { + _ = Dispatcher.InvokeAsync( + () => _ = CheckForUpdatesAsync(ApplicationUpdateCheckTrigger.Automatic), + DispatcherPriority.Background); + } + } + + private Task CheckForUpdatesAsync( + ApplicationUpdateCheckTrigger trigger) + { + Task check; + lock (updateCheckSync) + { + if (activeUpdateCheck is null || activeUpdateCheck.IsCompleted) + { + activeUpdateCheck = CheckForUpdatesCoreAsync(); + } + + check = activeUpdateCheck; + } + + return CompleteUpdateCheckRequestAsync(check, trigger); + } + + private async Task CompleteUpdateCheckRequestAsync( + Task check, + ApplicationUpdateCheckTrigger trigger) + { + ApplicationUpdateViewState state = await check; + if (trigger is ApplicationUpdateCheckTrigger.Tray || + (trigger is ApplicationUpdateCheckTrigger.Automatic && + state.Status == ApplicationUpdateViewStatus.UpdateAvailable)) + { + ShowUpdateNotification(state); + } + + return state; + } + + private async Task CheckForUpdatesCoreAsync() + { + SetUpdateViewState(ApplicationUpdateViewState.Checking); + try + { + IApplicationUpdateChecker checker = updateChecker ?? + throw new InvalidOperationException("The update service is unavailable."); + ApplicationUpdateCheckResult result = await checker.CheckAsync(GetProductVersion()); + if (MainWindow is MainWindow window) + { + try + { + await window.RecordUpdateCheckMetadataAsync(result); + } + catch (Exception exception) + { + window.ReportSettingsFailure( + localizer.Format("SettingsSaveFailed", exception.Message)); + } + } + + ApplicationUpdateViewState state = CreateUpdateViewState(result); + SetUpdateViewState(state); + Log( + QuickPodsLogLevel.Information, + "ApplicationUpdateChecked", + "The latest QuickPods release was checked.", + new Dictionary + { + ["CurrentVersion"] = result.CurrentVersion.ToString(3), + ["LatestVersion"] = result.LatestVersion.ToString(3), + ["UpdateAvailable"] = result.IsUpdateAvailable, + }); + return state; + } + catch (Exception exception) + { + var state = new ApplicationUpdateViewState( + ApplicationUpdateViewStatus.Failed, + ErrorDetail: exception.Message); + SetUpdateViewState(state); + Log( + QuickPodsLogLevel.Warning, + "ApplicationUpdateCheckFailed", + "The latest QuickPods release could not be checked.", + new Dictionary + { + ["FailureType"] = exception.GetType().Name, + }); + return state; + } + } + + private void SetUpdateViewState(ApplicationUpdateViewState state) + { + updateViewState = state; + if (MainWindow is MainWindow window) + { + window.ReportUpdateCheckState(state); + } + } + + private static ApplicationUpdateViewState CreateUpdateViewState( + ApplicationUpdateCheckResult result) => new( + result.IsUpdateAvailable + ? ApplicationUpdateViewStatus.UpdateAvailable + : ApplicationUpdateViewStatus.UpToDate, + result.CheckedAtUtc, + result.LatestVersion, + result.ReleasePage); + + private void ShowUpdateNotification(ApplicationUpdateViewState state) + { + switch (state.Status) + { + case ApplicationUpdateViewStatus.UpdateAvailable + when state.LatestVersion is not null && state.ReleasePage is not null: + trayIcon?.ShowNotification( + localizer["UpdateNotificationTitle"], + localizer.Format( + "UpdateAvailableNotification", + state.LatestVersion.ToString(3)), + () => OpenReleasePage(state.ReleasePage)); + break; + case ApplicationUpdateViewStatus.UpToDate: + trayIcon?.ShowNotification( + localizer["UpdateNotificationTitle"], + localizer.Format( + "UpToDateNotification", + GetProductVersion().ToString(3))); + break; + case ApplicationUpdateViewStatus.Failed: + trayIcon?.ShowNotification( + localizer["UpdateNotificationTitle"], + localizer["UpdateCheckFailedNotification"]); + break; + } } + private void OpenReleasePage(Uri releasePage) + { + ArgumentNullException.ThrowIfNull(releasePage); + try + { + _ = Process.Start(new ProcessStartInfo + { + FileName = releasePage.AbsoluteUri, + UseShellExecute = true, + }); + } + catch (Exception exception) + { + if (MainWindow is MainWindow window) + { + window.ReportSettingsFailure( + localizer.Format("ReleasePageOpenFailed", exception.Message)); + } + } + } + + private static Version GetProductVersion() => + typeof(App).Assembly.GetName().Version is { } version + ? new Version(version.Major, version.Minor, Math.Max(0, version.Build)) + : new Version(0, 1, 0); + private void StartBluetoothCatalog() { try diff --git a/src/QuickPods.App/ApplicationUpdateViewState.cs b/src/QuickPods.App/ApplicationUpdateViewState.cs new file mode 100644 index 0000000..72e0f7d --- /dev/null +++ b/src/QuickPods.App/ApplicationUpdateViewState.cs @@ -0,0 +1,31 @@ +namespace QuickPods.App; + +internal enum ApplicationUpdateViewStatus +{ + NotChecked, + Checking, + UpToDate, + UpdateAvailable, + Failed, +} + +internal sealed record ApplicationUpdateViewState( + ApplicationUpdateViewStatus Status, + DateTimeOffset? CheckedAtUtc = null, + Version? LatestVersion = null, + Uri? ReleasePage = null, + string? ErrorDetail = null) +{ + internal static ApplicationUpdateViewState NotChecked { get; } = new( + ApplicationUpdateViewStatus.NotChecked); + + internal static ApplicationUpdateViewState Checking { get; } = new( + ApplicationUpdateViewStatus.Checking); +} + +internal enum ApplicationUpdateCheckTrigger +{ + Tray, + Settings, + Automatic, +} diff --git a/src/QuickPods.App/MainWindow.xaml.cs b/src/QuickPods.App/MainWindow.xaml.cs index f26b9fa..bef415c 100644 --- a/src/QuickPods.App/MainWindow.xaml.cs +++ b/src/QuickPods.App/MainWindow.xaml.cs @@ -36,6 +36,9 @@ public partial class MainWindow : Window private readonly IStartupRegistration startupRegistration; private readonly ProductLifetimePolicy lifetimePolicy; private readonly Action restartApplication; + private readonly Func readUpdateState; + private readonly Func> checkForUpdates; + private readonly Action openReleasePage; private readonly ProductLocalizer localizer; private readonly string logsDirectory; private bool applyingAudioState; @@ -56,7 +59,7 @@ public partial class MainWindow : Window internal event EventHandler? FlyoutPointerExited; - public MainWindow( + internal MainWindow( AudioController audio, BluetoothCatalogController? bluetoothCatalog, BluetoothOperationController? bluetoothOperations, @@ -65,6 +68,9 @@ public MainWindow( IStartupRegistration startupRegistration, ProductLifetimePolicy lifetimePolicy, Action restartApplication, + Func readUpdateState, + Func> checkForUpdates, + Action openReleasePage, ProductLocalizer localizer, string logsDirectory, string? startupDiagnostic) @@ -81,6 +87,12 @@ public MainWindow( throw new ArgumentNullException(nameof(lifetimePolicy)); this.restartApplication = restartApplication ?? throw new ArgumentNullException(nameof(restartApplication)); + this.readUpdateState = readUpdateState ?? + throw new ArgumentNullException(nameof(readUpdateState)); + this.checkForUpdates = checkForUpdates ?? + throw new ArgumentNullException(nameof(checkForUpdates)); + this.openReleasePage = openReleasePage ?? + throw new ArgumentNullException(nameof(openReleasePage)); this.localizer = localizer ?? throw new ArgumentNullException(nameof(localizer)); ArgumentException.ThrowIfNullOrWhiteSpace(logsDirectory); this.logsDirectory = Path.GetFullPath(logsDirectory); @@ -127,9 +139,13 @@ internal void ReportSettingsFailure(string message) settingsWindow?.ReportDiagnostic(settingsDiagnostic); } + internal void ReportUpdateCheckState(ApplicationUpdateViewState state) => + settingsWindow?.ApplyUpdateState(state); + internal void ApplyLocalization() { settingsWindow?.ApplyLocalization(); + settingsWindow?.ApplyUpdateState(readUpdateState()); ApplyAudioState(audio.State); ApplyBluetoothPresentation(); } @@ -145,12 +161,16 @@ internal void ShowSettingsWindow() OpenLogs, CreateDiagnosticSummary, restartApplication, + readUpdateState, + checkForUpdates, + openReleasePage, localizer, GetProductVersion()); settingsWindow.Closed += OnSettingsWindowClosed; } settingsWindow.ApplySettings(productSettings); + settingsWindow.ApplyUpdateState(readUpdateState()); settingsWindow.ReportDiagnostic(settingsDiagnostic); settingsWindow.Show(); if (settingsWindow.WindowState == WindowState.Minimized) @@ -670,12 +690,26 @@ private async Task UpdateProductSettingsAsync( MouseWheelStepPercent = requested.MouseWheelStepPercent, SetConnectedDeviceAsDefault = requested.SetConnectedDeviceAsDefault, ConfirmBluetoothDisconnect = requested.ConfirmBluetoothDisconnect, + CheckForUpdatesAtStartup = requested.CheckForUpdatesAtStartup, }); ClearSettingsDiagnostic(); ApplyProductSettings(saved); return saved; } + internal async Task RecordUpdateCheckMetadataAsync( + ApplicationUpdateCheckResult result) + { + ArgumentNullException.ThrowIfNull(result); + QuickPodsSettings saved = await settingsStore.UpdateSettingsAsync(current => current with + { + LastUpdateCheckUtc = result.CheckedAtUtc, + LastKnownLatestVersion = result.LatestVersion.ToString(3), + LastKnownReleasePage = result.ReleasePage.AbsoluteUri, + }); + ApplyProductSettings(saved); + } + private async Task UpdateStartupSettingAsync(bool requested) { bool previous = productSettings.StartWithWindows; @@ -832,6 +866,12 @@ private string CreateDiagnosticSummary() _ = builder.AppendLine( CultureInfo.InvariantCulture, $"Start with Windows: {productSettings.StartWithWindows}"); + _ = builder.AppendLine( + CultureInfo.InvariantCulture, + $"Automatic update checks: {productSettings.CheckForUpdatesAtStartup}"); + _ = builder.AppendLine( + CultureInfo.InvariantCulture, + $"Last update check: {productSettings.LastUpdateCheckUtc?.ToString("O", CultureInfo.InvariantCulture) ?? "Never"}"); return builder.ToString(); } diff --git a/src/QuickPods.App/SettingsWindow.xaml b/src/QuickPods.App/SettingsWindow.xaml index daed446..7198c9c 100644 --- a/src/QuickPods.App/SettingsWindow.xaml +++ b/src/QuickPods.App/SettingsWindow.xaml @@ -4,8 +4,8 @@ Title="{DynamicResource SettingsWindowTitle}" Width="720" MinWidth="660" - Height="760" - MinHeight="680" + Height="880" + MinHeight="700" WindowStartupLocation="CenterScreen" Background="{DynamicResource WindowBrush}" Foreground="{DynamicResource PrimaryTextBrush}" @@ -404,6 +404,12 @@ Unchecked="OnStartWithWindowsChanged" Style="{StaticResource SettingsToggleStyle}" Content="{DynamicResource StartWithWindows}" /> + + @@ -441,7 +447,42 @@ Text="{DynamicResource CopyDiagnostics}" /> + + + + + + + +