feat(launcher): implement verified atomic updates
This commit is contained in:
parent
2198a0cc8e
commit
2d2a5b5046
34 changed files with 6755 additions and 61 deletions
520
src/AcDream.Launcher/ViewModels/LauncherUpdateViewModel.cs
Normal file
520
src/AcDream.Launcher/ViewModels/LauncherUpdateViewModel.cs
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
using AcDream.Launcher.Core.Updates;
|
||||
|
||||
namespace AcDream.Launcher.ViewModels;
|
||||
|
||||
public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly ILauncherUpdater _updater;
|
||||
private readonly IUiDispatcher _dispatcher;
|
||||
private readonly Action _onClientChanged;
|
||||
private readonly Func<bool> _canOpen;
|
||||
private readonly Func<bool> _canMutate;
|
||||
private CancellationTokenSource? _cancellation;
|
||||
private LauncherUpdateCheckResult? _check;
|
||||
private bool _isOpen;
|
||||
private bool _isBusy;
|
||||
private bool _disposed;
|
||||
private string _status = "No update check has run yet.";
|
||||
private string? _error;
|
||||
private LauncherUpdatePhase _phase = LauncherUpdatePhase.Idle;
|
||||
private double _progressPercent;
|
||||
private bool _isProgressIndeterminate;
|
||||
private string? _launcherRestartStatus;
|
||||
|
||||
public LauncherUpdateViewModel(
|
||||
ILauncherUpdater updater,
|
||||
IUiDispatcher dispatcher,
|
||||
Action onClientChanged,
|
||||
Func<bool>? canOpen = null,
|
||||
Func<bool>? canMutate = null)
|
||||
{
|
||||
_updater = updater ?? throw new ArgumentNullException(nameof(updater));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_onClientChanged = onClientChanged
|
||||
?? throw new ArgumentNullException(nameof(onClientChanged));
|
||||
_canOpen = canOpen ?? (() => true);
|
||||
_canMutate = canMutate ?? (() => true);
|
||||
|
||||
OpenCommand = new AsyncRelayCommand(OpenAndCheckAsync, () => _canOpen() && !IsBusy);
|
||||
CloseCommand = new RelayCommand(Close, () => !IsBusy);
|
||||
CheckCommand = new AsyncRelayCommand(
|
||||
() => CheckAsync(startup: false),
|
||||
() => IsOpen && !IsBusy);
|
||||
InstallClientCommand = new AsyncRelayCommand(
|
||||
InstallClientAsync,
|
||||
() => IsOpen
|
||||
&& !IsBusy
|
||||
&& _canMutate()
|
||||
&& _check is
|
||||
{
|
||||
IsClientUpdateAvailable: true,
|
||||
IsLauncherMinimumSatisfied: true,
|
||||
});
|
||||
StageLauncherCommand = new AsyncRelayCommand(
|
||||
StageLauncherAsync,
|
||||
() => IsOpen
|
||||
&& !IsBusy
|
||||
&& _canMutate()
|
||||
&& !IsLauncherRestartRequired
|
||||
&& _check is { IsLauncherUpdateAvailable: true });
|
||||
RollbackCommand = new AsyncRelayCommand(
|
||||
RollbackAsync,
|
||||
() => IsOpen
|
||||
&& !IsBusy
|
||||
&& _canMutate()
|
||||
&& !string.IsNullOrEmpty(_updater.CurrentClient.PreviousVersion));
|
||||
CancelCommand = new RelayCommand(
|
||||
() => _cancellation?.Cancel(),
|
||||
() => IsBusy && _cancellation is not null);
|
||||
}
|
||||
|
||||
public string Title => "Client and launcher updates";
|
||||
|
||||
public string Body =>
|
||||
"Releases are downloaded from the pinned eriknihlen/acdream GitHub feed. "
|
||||
+ "Every archive is size/SHA-256 verified and safely extracted before "
|
||||
+ "the active client pointer can change.";
|
||||
|
||||
public bool IsOpen
|
||||
{
|
||||
get => _isOpen;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isOpen, value))
|
||||
{
|
||||
NotifyCommandStates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isBusy, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanClose));
|
||||
NotifyCommandStates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanClose => !IsBusy;
|
||||
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
private set => SetProperty(ref _status, value);
|
||||
}
|
||||
|
||||
public string? Error
|
||||
{
|
||||
get => _error;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _error, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(HasError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasError => !string.IsNullOrWhiteSpace(Error);
|
||||
|
||||
public LauncherUpdatePhase Phase
|
||||
{
|
||||
get => _phase;
|
||||
private set => SetProperty(ref _phase, value);
|
||||
}
|
||||
|
||||
public double ProgressPercent
|
||||
{
|
||||
get => _progressPercent;
|
||||
private set => SetProperty(ref _progressPercent, value);
|
||||
}
|
||||
|
||||
public bool IsProgressIndeterminate
|
||||
{
|
||||
get => _isProgressIndeterminate;
|
||||
private set => SetProperty(ref _isProgressIndeterminate, value);
|
||||
}
|
||||
|
||||
public string CurrentClientVersion =>
|
||||
_updater.CurrentClient.Version?.Value ?? "not installed";
|
||||
|
||||
public string AvailableVersion => _check?.Manifest.Version.Value ?? "not checked";
|
||||
|
||||
public string CurrentLauncherVersion =>
|
||||
_check?.LauncherVersion.Value ?? "loading";
|
||||
|
||||
public bool IsClientUpdateAvailable => _check?.IsClientUpdateAvailable == true;
|
||||
|
||||
public bool IsLauncherUpdateAvailable => _check?.IsLauncherUpdateAvailable == true;
|
||||
|
||||
public bool IsLauncherRestartRequired =>
|
||||
!string.IsNullOrWhiteSpace(_launcherRestartStatus);
|
||||
|
||||
public string LauncherRestartStatus => _launcherRestartStatus ?? string.Empty;
|
||||
|
||||
public bool IsLauncherMinimumBlocked => _check is
|
||||
{
|
||||
IsClientUpdateAvailable: true,
|
||||
IsLauncherMinimumSatisfied: false,
|
||||
};
|
||||
|
||||
public string MinimumLauncherStatus => _check is null
|
||||
? string.Empty
|
||||
: _check.IsLauncherMinimumSatisfied
|
||||
? $"Launcher meets minimum {_check.Manifest.MinimumLauncherVersion}."
|
||||
: $"Install launcher {_check.Manifest.MinimumLauncherVersion} or newer before the client update.";
|
||||
|
||||
public AsyncRelayCommand OpenCommand { get; }
|
||||
|
||||
public RelayCommand CloseCommand { get; }
|
||||
|
||||
public AsyncRelayCommand CheckCommand { get; }
|
||||
|
||||
public AsyncRelayCommand InstallClientCommand { get; }
|
||||
|
||||
public AsyncRelayCommand StageLauncherCommand { get; }
|
||||
|
||||
public AsyncRelayCommand RollbackCommand { get; }
|
||||
|
||||
public RelayCommand CancelCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Launch-time polling is deliberately nonfatal: an offline or malformed
|
||||
/// feed changes only this status and never prevents profile/session use.
|
||||
/// </summary>
|
||||
public async Task StartupCheckAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await CheckAsync(startup: true).ConfigureAwait(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// CheckAsync owns visible state and never lets startup polling
|
||||
// escape into the Avalonia initialization transaction.
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!IsBusy)
|
||||
{
|
||||
IsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyCommandStates()
|
||||
{
|
||||
OpenCommand.NotifyCanExecuteChanged();
|
||||
CloseCommand.NotifyCanExecuteChanged();
|
||||
CheckCommand.NotifyCanExecuteChanged();
|
||||
InstallClientCommand.NotifyCanExecuteChanged();
|
||||
StageLauncherCommand.NotifyCanExecuteChanged();
|
||||
RollbackCommand.NotifyCanExecuteChanged();
|
||||
CancelCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_cancellation?.Cancel();
|
||||
_cancellation?.Dispose();
|
||||
_cancellation = null;
|
||||
}
|
||||
|
||||
private async Task OpenAndCheckAsync()
|
||||
{
|
||||
IsOpen = true;
|
||||
await CheckAsync(startup: false).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
private async Task CheckAsync(bool startup)
|
||||
{
|
||||
if (IsBusy || _disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
_cancellation = cancellation;
|
||||
IsBusy = true;
|
||||
Error = null;
|
||||
Phase = LauncherUpdatePhase.Checking;
|
||||
Status = "Checking the pinned GitHub release manifest...";
|
||||
IsProgressIndeterminate = true;
|
||||
ProgressPercent = 0;
|
||||
try
|
||||
{
|
||||
_check = await _updater.CheckAsync(cancellation.Token)
|
||||
.ConfigureAwait(true);
|
||||
Status = _check.Status;
|
||||
Phase = LauncherUpdatePhase.Completed;
|
||||
RefreshVersionProperties();
|
||||
if (startup
|
||||
&& (_check.IsClientUpdateAvailable
|
||||
|| _check.IsLauncherUpdateAvailable))
|
||||
{
|
||||
IsOpen = true;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status = "Update check cancelled.";
|
||||
Phase = LauncherUpdatePhase.Cancelled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string detail = SafeDisplayError(ex);
|
||||
Status = startup
|
||||
? $"Automatic update check unavailable; continuing offline. {detail}"
|
||||
: "Update check failed.";
|
||||
Error = startup ? null : detail;
|
||||
Phase = LauncherUpdatePhase.Failed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsProgressIndeterminate = false;
|
||||
if (ReferenceEquals(_cancellation, cancellation))
|
||||
{
|
||||
_cancellation = null;
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task InstallClientAsync() => RunMutationAsync(
|
||||
(check, progress, token) => _updater.InstallClientAsync(check, progress, token),
|
||||
"Installing the client update...",
|
||||
"Client update installed and activated.",
|
||||
clientChanged: true);
|
||||
|
||||
private Task StageLauncherAsync() => RunMutationAsync(
|
||||
async (check, progress, token) =>
|
||||
{
|
||||
SelfUpdateStageResult staged = await _updater
|
||||
.StageLauncherAsync(check, progress, token)
|
||||
.ConfigureAwait(true);
|
||||
_launcherRestartStatus = staged.Status;
|
||||
OnPropertyChanged(nameof(IsLauncherRestartRequired));
|
||||
OnPropertyChanged(nameof(LauncherRestartStatus));
|
||||
return _updater.CurrentClient;
|
||||
},
|
||||
"Staging the launcher update...",
|
||||
"Launcher update staged; restart the launcher to apply it.",
|
||||
clientChanged: false);
|
||||
|
||||
private async Task RollbackAsync()
|
||||
{
|
||||
if (IsBusy || _disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
_cancellation = cancellation;
|
||||
IsBusy = true;
|
||||
Error = null;
|
||||
Status = "Rolling back the client...";
|
||||
IsProgressIndeterminate = true;
|
||||
try
|
||||
{
|
||||
var progress = new UiProgress<LauncherUpdateProgress>(
|
||||
_dispatcher,
|
||||
ApplyProgress);
|
||||
_ = await _updater.RollbackClientAsync(progress, cancellation.Token)
|
||||
.ConfigureAwait(true);
|
||||
_onClientChanged();
|
||||
RefreshVersionProperties();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status = "Rollback cancelled; the active version was not changed.";
|
||||
Phase = LauncherUpdatePhase.Cancelled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = SafeDisplayError(ex);
|
||||
Status = "Rollback failed.";
|
||||
Phase = LauncherUpdatePhase.Failed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsProgressIndeterminate = false;
|
||||
if (ReferenceEquals(_cancellation, cancellation))
|
||||
{
|
||||
_cancellation = null;
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunMutationAsync(
|
||||
Func<
|
||||
LauncherUpdateCheckResult,
|
||||
IProgress<LauncherUpdateProgress>,
|
||||
CancellationToken,
|
||||
Task<ClientVersionResolution>> operation,
|
||||
string initialStatus,
|
||||
string completedStatus,
|
||||
bool clientChanged)
|
||||
{
|
||||
if (IsBusy || _disposed || _check is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
_cancellation = cancellation;
|
||||
IsBusy = true;
|
||||
Error = null;
|
||||
Status = initialStatus;
|
||||
IsProgressIndeterminate = true;
|
||||
ProgressPercent = 0;
|
||||
try
|
||||
{
|
||||
var progress = new UiProgress<LauncherUpdateProgress>(
|
||||
_dispatcher,
|
||||
ApplyProgress);
|
||||
_ = await operation(_check, progress, cancellation.Token)
|
||||
.ConfigureAwait(true);
|
||||
if (clientChanged)
|
||||
{
|
||||
_onClientChanged();
|
||||
}
|
||||
|
||||
RefreshVersionProperties();
|
||||
Phase = LauncherUpdatePhase.Completed;
|
||||
Status = IsLauncherRestartRequired
|
||||
? LauncherRestartStatus
|
||||
: completedStatus;
|
||||
try
|
||||
{
|
||||
_check = await _updater.CheckAsync(CancellationToken.None)
|
||||
.ConfigureAwait(true);
|
||||
if (!IsLauncherRestartRequired)
|
||||
{
|
||||
Status = _check.Status;
|
||||
}
|
||||
|
||||
RefreshVersionProperties();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status += " Release status refresh is unavailable: "
|
||||
+ SafeDisplayError(ex);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Status = "Update operation cancelled; published state was not changed.";
|
||||
Phase = LauncherUpdatePhase.Cancelled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = SafeDisplayError(ex);
|
||||
Status = "Update operation failed.";
|
||||
Phase = LauncherUpdatePhase.Failed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsProgressIndeterminate = false;
|
||||
if (ReferenceEquals(_cancellation, cancellation))
|
||||
{
|
||||
_cancellation = null;
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyProgress(LauncherUpdateProgress value)
|
||||
{
|
||||
Phase = value.Phase;
|
||||
Status = value.Status;
|
||||
ProgressPercent = value.Percent;
|
||||
IsProgressIndeterminate = value.Total <= 0
|
||||
&& value.Phase is not (
|
||||
LauncherUpdatePhase.Completed
|
||||
or LauncherUpdatePhase.Cancelled
|
||||
or LauncherUpdatePhase.Failed);
|
||||
}
|
||||
|
||||
private void RefreshVersionProperties()
|
||||
{
|
||||
OnPropertyChanged(nameof(CurrentClientVersion));
|
||||
OnPropertyChanged(nameof(AvailableVersion));
|
||||
OnPropertyChanged(nameof(CurrentLauncherVersion));
|
||||
OnPropertyChanged(nameof(IsClientUpdateAvailable));
|
||||
OnPropertyChanged(nameof(IsLauncherUpdateAvailable));
|
||||
OnPropertyChanged(nameof(IsLauncherRestartRequired));
|
||||
OnPropertyChanged(nameof(LauncherRestartStatus));
|
||||
OnPropertyChanged(nameof(IsLauncherMinimumBlocked));
|
||||
OnPropertyChanged(nameof(MinimumLauncherStatus));
|
||||
NotifyCommandStates();
|
||||
}
|
||||
|
||||
private static string SafeDisplayError(Exception exception) =>
|
||||
string.IsNullOrWhiteSpace(exception.Message)
|
||||
? "The update operation failed."
|
||||
: exception.Message;
|
||||
|
||||
private sealed class UiProgress<T>(IUiDispatcher dispatcher, Action<T> callback)
|
||||
: IProgress<T>
|
||||
{
|
||||
public void Report(T value) => dispatcher.Post(() => callback(value));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UnavailableLauncherUpdater : ILauncherUpdater
|
||||
{
|
||||
private static readonly ClientVersionResolution Missing = new(
|
||||
ClientVersionState.Missing,
|
||||
"Versioned client updater is unavailable.",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
public ClientVersionResolution CurrentClient => Missing;
|
||||
|
||||
public Task<ClientVersionResolution> InitializeAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(Missing);
|
||||
|
||||
public Task<LauncherUpdateCheckResult> CheckAsync(
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromException<LauncherUpdateCheckResult>(
|
||||
new LauncherUpdateException("Versioned client updater is unavailable."));
|
||||
|
||||
public Task<ClientVersionResolution> InstallClientAsync(
|
||||
LauncherUpdateCheckResult check,
|
||||
IProgress<LauncherUpdateProgress>? progress = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromException<ClientVersionResolution>(
|
||||
new LauncherUpdateException("Versioned client updater is unavailable."));
|
||||
|
||||
public Task<SelfUpdateStageResult> StageLauncherAsync(
|
||||
LauncherUpdateCheckResult check,
|
||||
IProgress<LauncherUpdateProgress>? progress = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromException<SelfUpdateStageResult>(
|
||||
new LauncherUpdateException("Versioned client updater is unavailable."));
|
||||
|
||||
public Task<ClientVersionResolution> RollbackClientAsync(
|
||||
IProgress<LauncherUpdateProgress>? progress = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromException<ClientVersionResolution>(
|
||||
new LauncherUpdateException("Versioned client updater is unavailable."));
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using AcDream.Launcher.Core.Installation;
|
|||
using AcDream.Launcher.Core.Launching;
|
||||
using AcDream.Launcher.Core.Orchestration;
|
||||
using AcDream.Launcher.Core.Profiles;
|
||||
using AcDream.Launcher.Core.Updates;
|
||||
|
||||
namespace AcDream.Launcher.ViewModels;
|
||||
|
||||
|
|
@ -25,7 +26,8 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
public LauncherWindowViewModel(
|
||||
ILauncherOrchestrator orchestrator,
|
||||
IUiDispatcher dispatcher,
|
||||
ILauncherInstaller? installer = null)
|
||||
ILauncherInstaller? installer = null,
|
||||
ILauncherUpdater? updater = null)
|
||||
{
|
||||
_orchestrator = orchestrator ?? throw new ArgumentNullException(nameof(orchestrator));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
|
|
@ -38,17 +40,16 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
OnInstallCompleted,
|
||||
() => CanInteract,
|
||||
() => !IsBusy && Sessions.All(session => !session.IsActive));
|
||||
UpdatePromptShell = new LauncherShellViewModel(
|
||||
"Client update",
|
||||
"Review a signed release manifest, verify the downloaded archive, "
|
||||
+ "and atomically switch the installed client version. The updater "
|
||||
+ "transaction lands in Campaign LA slice LA10.",
|
||||
"Updater shell ready — implementation arrives in LA10.",
|
||||
() => CanInteract);
|
||||
UpdatePrompt = new LauncherUpdateViewModel(
|
||||
updater ?? new UnavailableLauncherUpdater(),
|
||||
dispatcher,
|
||||
OnClientVersionChanged,
|
||||
() => CanInteract,
|
||||
() => !IsBusy && Sessions.All(session => !session.IsActive));
|
||||
|
||||
EditorDialog.PropertyChanged += OnModalPropertyChanged;
|
||||
FirstRunWizardShell.PropertyChanged += OnModalPropertyChanged;
|
||||
UpdatePromptShell.PropertyChanged += OnModalPropertyChanged;
|
||||
UpdatePrompt.PropertyChanged += OnModalPropertyChanged;
|
||||
|
||||
AddServerCommand = new RelayCommand(OpenAddServerDialog, () => CanInteract);
|
||||
AddAccountCommand = new RelayCommand(OpenAddAccountDialog, CanAddAccount);
|
||||
|
|
@ -92,7 +93,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
|
||||
public FirstRunInstallerViewModel FirstRunWizardShell { get; }
|
||||
|
||||
public LauncherShellViewModel UpdatePromptShell { get; }
|
||||
public LauncherUpdateViewModel UpdatePrompt { get; }
|
||||
|
||||
public LauncherTreeNodeViewModel? SelectedNode
|
||||
{
|
||||
|
|
@ -136,7 +137,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
public bool IsModalOpen =>
|
||||
EditorDialog.IsOpen
|
||||
|| FirstRunWizardShell.IsOpen
|
||||
|| UpdatePromptShell.IsOpen;
|
||||
|| UpdatePrompt.IsOpen;
|
||||
|
||||
private bool CanInteract => !IsBusy && !IsModalOpen;
|
||||
|
||||
|
|
@ -300,6 +301,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
}
|
||||
|
||||
RefreshFromCore();
|
||||
_ = UpdatePrompt.StartupCheckAsync();
|
||||
}
|
||||
|
||||
public void PollStatus()
|
||||
|
|
@ -333,8 +335,9 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
_orchestrator.StateChanged -= OnOrchestratorStateChanged;
|
||||
EditorDialog.PropertyChanged -= OnModalPropertyChanged;
|
||||
FirstRunWizardShell.PropertyChanged -= OnModalPropertyChanged;
|
||||
UpdatePromptShell.PropertyChanged -= OnModalPropertyChanged;
|
||||
UpdatePrompt.PropertyChanged -= OnModalPropertyChanged;
|
||||
FirstRunWizardShell.Dispose();
|
||||
UpdatePrompt.Dispose();
|
||||
}
|
||||
|
||||
private void OnOrchestratorStateChanged(object? sender, EventArgs e) =>
|
||||
|
|
@ -350,7 +353,8 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
{
|
||||
if (e.PropertyName != nameof(ProfileEditorDialogViewModel.IsOpen)
|
||||
&& e.PropertyName != nameof(LauncherShellViewModel.IsOpen)
|
||||
&& e.PropertyName != nameof(FirstRunInstallerViewModel.IsOpen))
|
||||
&& e.PropertyName != nameof(FirstRunInstallerViewModel.IsOpen)
|
||||
&& e.PropertyName != nameof(LauncherUpdateViewModel.IsOpen))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -376,9 +380,9 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
{
|
||||
FirstRunWizardShell.Close();
|
||||
}
|
||||
else if (UpdatePromptShell.IsOpen)
|
||||
else if (UpdatePrompt.IsOpen)
|
||||
{
|
||||
UpdatePromptShell.IsOpen = false;
|
||||
UpdatePrompt.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -976,6 +980,13 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
RefreshFromCore();
|
||||
}
|
||||
|
||||
private void OnClientVersionChanged()
|
||||
{
|
||||
OperationStatus = "Versioned client activation changed.";
|
||||
LastError = null;
|
||||
RefreshFromCore();
|
||||
}
|
||||
|
||||
private void NotifyCommandStates()
|
||||
{
|
||||
AddServerCommand.NotifyCanExecuteChanged();
|
||||
|
|
@ -996,7 +1007,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
|
|||
session.NotifyCommandState();
|
||||
}
|
||||
FirstRunWizardShell.NotifyCommandStates();
|
||||
UpdatePromptShell.NotifyCommandStates();
|
||||
UpdatePrompt.NotifyCommandStates();
|
||||
}
|
||||
|
||||
private readonly record struct SelectionKey(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue