Add startup visual modes and attempt registry

Implement startup visual behavior, de-duplicate startup attempts, and improve failure UX.

Key changes:
- Add spec and docs for startup visuals and timing contract (.trae/specs and docs/LAUNCHER_STARTUP_VISUALS.md).
- Introduce StartupVisualPreferences contract and resolver; create SplashWindow via resolved mode.
- Add StartupAttemptRecord model and a file-backed StartupAttemptRegistry to persist and coordinate in-progress startup attempts (attach/adopt, soft/hard timeouts, IPC/connect state, lifecycle updates).
- Update LauncherFlowCoordinator to: adopt/attach to existing attempts, track IPC connection and soft/hard timeouts (30s/120s), show delayed UI state, attempt foreground recovery via public IPC, compose detailed launch result metadata, and mark registry states (soft timeout, detached waiting, succeeded, failed).
- Add TryActivateExistingInstanceAsync to attempt activating an existing desktop via IPC.
- Change failure flow: ShowFailureWindowAsync now returns user choice; ErrorWindow updated to present Activate/Wait/Open Logs/Exit semantics and new layouts/styles; improved button wiring and debug/dev mode handling.
- Add UI and resource tweaks (ErrorWindow and SplashWindow changes), project asset link for nightly logo, and unit tests for StartupVisualPreferences.

These changes prevent duplicate desktop processes during slow startups, provide clearer UX for delayed startups, and persist startup attempt state across Launcher invocations for safer recovery/attach behavior.
This commit is contained in:
lincube
2026-04-23 09:03:35 +08:00
parent 001d77968f
commit 33591a0a63
20 changed files with 2008 additions and 1076 deletions

View File

@@ -13,6 +13,7 @@ using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
using LanMountainDesktop.Shared.Contracts.Launcher;
namespace LanMountainDesktop.ViewModels;
@@ -201,7 +202,7 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
SelectedRenderMode = RenderModes.FirstOrDefault(option =>
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
?? RenderModes[0];
EnableSlideTransition = appSnapshot.EnableSlideTransition;
ApplyTransitionPreferences(appSnapshot.EnableFadeTransition, appSnapshot.EnableSlideTransition);
ShowInTaskbar = appSnapshot.ShowInTaskbar;
_isInitializing = false;
@@ -235,9 +236,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
return;
}
if (changedKeys.Contains(nameof(AppSettingsSnapshot.EnableSlideTransition)))
if (changedKeys.Contains(nameof(AppSettingsSnapshot.EnableSlideTransition)) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFadeTransition)))
{
EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableSlideTransition;
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
ApplyTransitionPreferences(snapshot.EnableFadeTransition, snapshot.EnableSlideTransition);
}
if (changedKeys.Contains(nameof(AppSettingsSnapshot.ShowInTaskbar)))
@@ -263,6 +266,9 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
[ObservableProperty]
private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default");
[ObservableProperty]
private bool _enableFadeTransition = true;
[ObservableProperty]
private bool _enableSlideTransition;
@@ -271,6 +277,12 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
public bool IsFadeTransitionToggleEnabled => !EnableSlideTransition;
public string FadeTransitionDescription => EnableSlideTransition
? "滑动模式已启用,淡入淡出不可同时使用。"
: "启用后,启动与恢复过程使用淡入淡出效果。";
[ObservableProperty]
private string _pageTitle = string.Empty;
@@ -372,8 +384,22 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
partial void OnEnableSlideTransitionChanged(bool value)
{
if (_isInitializing) return;
SaveField(nameof(AppSettingsSnapshot.EnableSlideTransition), value);
if (_isInitializing)
{
return;
}
SaveTransitionPreferences(EnableFadeTransition, value);
}
partial void OnEnableFadeTransitionChanged(bool value)
{
if (_isInitializing)
{
return;
}
SaveTransitionPreferences(value, EnableSlideTransition);
}
partial void OnShowInTaskbarChanged(bool value)
@@ -394,6 +420,35 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
_settingsFacade.Settings.SaveSnapshot(SettingsScope.App, snapshot, changedKeys: [key]);
}
private void SaveTransitionPreferences(bool enableFadeTransition, bool enableSlideTransition)
{
var normalized = StartupVisualPreferencesResolver.FromFlags(enableFadeTransition, enableSlideTransition);
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
snapshot.EnableFadeTransition = normalized.EnableFadeTransition;
snapshot.EnableSlideTransition = normalized.EnableSlideTransition;
ApplyTransitionPreferences(normalized.EnableFadeTransition, normalized.EnableSlideTransition);
_settingsFacade.Settings.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.EnableFadeTransition),
nameof(AppSettingsSnapshot.EnableSlideTransition)
]);
}
private void ApplyTransitionPreferences(bool enableFadeTransition, bool enableSlideTransition)
{
var normalized = StartupVisualPreferencesResolver.FromFlags(enableFadeTransition, enableSlideTransition);
var wasInitializing = _isInitializing;
_isInitializing = true;
EnableFadeTransition = normalized.EnableFadeTransition;
EnableSlideTransition = normalized.EnableSlideTransition;
_isInitializing = wasInitializing;
OnPropertyChanged(nameof(IsFadeTransitionToggleEnabled));
OnPropertyChanged(nameof(FadeTransitionDescription));
}
private IReadOnlyList<SelectionOption> CreateLanguageOptions()
{
return