F1: the crash reporter comment claimed the launcher never holds a password in any field - false (ProfileEditorDialogViewModel, AccountProfile.Password, StartRequest.Password). Reworded to the true, narrower invariant (no throw site interpolates a credential VALUE into an exception message) and pinned it with CrashReportNeverContainsAStoredPassword: a real STJ failure over a profiles document containing a known password, corrupted after the credential, must yield a crash file with the stack and without the value. F2: the co-deploy Inputs covered only Bake own sources; a Content edit never refreshed the 83 MB exe. Now the full reference closure. Fixing it surfaced two more incrementality traps, both fixed and comment-documented: SkipUnchangedFiles left the output older than the triggering input (target re-ran forever - added an explicit Touch), and %(Item.Metadata) in a plain Include does not batch (the literal percent-text became a permanently out-of-date phantom input - globs are now spelled per project). Verified: Core edit retriggers, then two consecutive clean incremental builds. F3: RID publishes ran BOTH co-deploy paths (two self-contained bake publishes). Build-time target now guarded on _IsPublishing; verified a real win-x64 publish runs zero build-target co-deploys and still ships both exes. F4: comment misattributed PublishBakeTool=false to CI lanes; it is target-local recursion guarding. F5: the x:Name reflection sweep now walks the markup as XML and tolerates template-scoped names (no generated field exists for those). F6: dead using removed. Hardening: the crash reporter positional --data-dir fallback requires a fully-qualified path so a relative or flag-shaped value cannot create ./crash-reports at an arbitrary CWD. Launcher 67/67, Launcher.Core 317/317. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
173 lines
5.4 KiB
C#
173 lines
5.4 KiB
C#
using System.ComponentModel;
|
|
using AcDream.Launcher.ViewModels;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Platform.Storage;
|
|
using Avalonia.Threading;
|
|
|
|
namespace AcDream.Launcher;
|
|
|
|
public sealed partial class MainWindow : Window
|
|
{
|
|
private readonly DispatcherTimer _statusTimer;
|
|
private LauncherWindowViewModel? _observedViewModel;
|
|
private Control? _focusBeforeModal;
|
|
private bool _wasModalOpen;
|
|
|
|
public MainWindow()
|
|
{
|
|
// InitializeComponent(), not AvaloniaXamlLoader.Load(this): only the
|
|
// generated method assigns the x:Name backing fields. Loading the XAML
|
|
// directly leaves every named control (ProfilesTree, the editor text
|
|
// boxes, FirstRunDatDirectoryTextBox, UpdateCloseButton, ...) null, so
|
|
// the first modal open or close threw NullReferenceException out of the
|
|
// dispatcher and took the whole process down through Program's guard.
|
|
InitializeComponent();
|
|
_statusTimer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromMilliseconds(250),
|
|
};
|
|
_statusTimer.Tick += OnStatusTimerTick;
|
|
DataContextChanged += OnDataContextChanged;
|
|
Opened += OnOpened;
|
|
Closed += OnClosed;
|
|
}
|
|
|
|
private void OnOpened(object? sender, EventArgs e) => _statusTimer.Start();
|
|
|
|
private void OnClosed(object? sender, EventArgs e)
|
|
{
|
|
_statusTimer.Stop();
|
|
_statusTimer.Tick -= OnStatusTimerTick;
|
|
DataContextChanged -= OnDataContextChanged;
|
|
ObserveViewModel(null);
|
|
Opened -= OnOpened;
|
|
Closed -= OnClosed;
|
|
}
|
|
|
|
private void OnStatusTimerTick(object? sender, EventArgs e)
|
|
{
|
|
if (DataContext is LauncherWindowViewModel viewModel)
|
|
{
|
|
viewModel.PollStatus();
|
|
}
|
|
}
|
|
|
|
private void OnDataContextChanged(object? sender, EventArgs e) =>
|
|
ObserveViewModel(DataContext as LauncherWindowViewModel);
|
|
|
|
private void ObserveViewModel(LauncherWindowViewModel? viewModel)
|
|
{
|
|
if (ReferenceEquals(_observedViewModel, viewModel))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_observedViewModel is not null)
|
|
{
|
|
_observedViewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
|
}
|
|
|
|
_observedViewModel = viewModel;
|
|
if (_observedViewModel is not null)
|
|
{
|
|
_observedViewModel.PropertyChanged += OnViewModelPropertyChanged;
|
|
}
|
|
|
|
_wasModalOpen = viewModel?.IsModalOpen == true;
|
|
}
|
|
|
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName != nameof(LauncherWindowViewModel.IsModalOpen)
|
|
|| sender is not LauncherWindowViewModel viewModel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isModalOpen = viewModel.IsModalOpen;
|
|
if (isModalOpen && !_wasModalOpen)
|
|
{
|
|
_focusBeforeModal = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() as Control;
|
|
Dispatcher.UIThread.Post(() => FocusActiveModal(viewModel));
|
|
}
|
|
else if (!isModalOpen && _wasModalOpen)
|
|
{
|
|
Control? focusToRestore = _focusBeforeModal;
|
|
_focusBeforeModal = null;
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
if (focusToRestore?.Focus() != true)
|
|
{
|
|
ProfilesTree.Focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
_wasModalOpen = isModalOpen;
|
|
}
|
|
|
|
private void FocusActiveModal(LauncherWindowViewModel viewModel)
|
|
{
|
|
if (viewModel.EditorDialog.IsOpen)
|
|
{
|
|
Control target = viewModel.EditorDialog.Kind switch
|
|
{
|
|
ProfileEditorKind.AddServer or ProfileEditorKind.EditServer => ServerNameTextBox,
|
|
ProfileEditorKind.AddAccount or ProfileEditorKind.EditAccount => AccountNameTextBox,
|
|
ProfileEditorKind.AddCharacter or ProfileEditorKind.EditCharacter => CharacterNameTextBox,
|
|
_ => EditorSubmitButton,
|
|
};
|
|
target.Focus();
|
|
}
|
|
else if (viewModel.FirstRunWizardShell.IsOpen)
|
|
{
|
|
FirstRunDatDirectoryTextBox.Focus();
|
|
}
|
|
else if (viewModel.UpdatePrompt.IsOpen)
|
|
{
|
|
UpdateCloseButton.Focus();
|
|
}
|
|
}
|
|
|
|
private void OnModalKeyDown(object? sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key != Key.Escape || DataContext is not LauncherWindowViewModel viewModel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
viewModel.CloseActiveModal();
|
|
e.Handled = true;
|
|
}
|
|
|
|
private async void OnBrowseDatDirectory(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (DataContext is not LauncherWindowViewModel viewModel)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
IReadOnlyList<IStorageFolder> folders = await StorageProvider
|
|
.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
{
|
|
Title = "Choose the retail Asheron's Call DAT directory",
|
|
AllowMultiple = false,
|
|
});
|
|
if (folders.Count > 0)
|
|
{
|
|
viewModel.FirstRunWizardShell.SelectDatDirectory(
|
|
folders[0].Path.LocalPath);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
viewModel.FirstRunWizardShell.ReportPickerError(ex.Message);
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
}
|