Public Access
Give the application a settings area built from what really exists
This commit is contained in:
@@ -206,6 +206,54 @@ internal enum ShellSurface
|
||||
Terminal = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which page the settings mode is showing, while <see cref="MainWindowViewModel.ActiveSettingsPage"/> is
|
||||
/// not null.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// v5c: the design's Settings area is a full-window mode that replaces the titlebar, the rail and the page
|
||||
/// area with its own — see <c>SettingsView.axaml</c> and the settings-mode remark on
|
||||
/// <see cref="MainWindowViewModel.ActiveSettingsPage"/>. This is a second, orthogonal notion of "where am I"
|
||||
/// from <see cref="ShellScreen"/>, not a replacement for it: <see cref="Preferences"/> and <see cref="Vaults"/>
|
||||
/// still set <see cref="MainWindowViewModel.Screen"/> to the matching <see cref="ShellScreen"/> member, so
|
||||
/// every existing binding and test that asks "is the screen Preferences" keeps its answer. <see cref="General"/>,
|
||||
/// <see cref="Account"/> and <see cref="Security"/> have no <see cref="ShellScreen"/> counterpart — nothing
|
||||
/// outside settings mode ever asked "which one of these three am I on" before this wave existed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>v5c-2: Groups and Tags joined.</b> The design's rail lists them beside Security and Preferences; v5c-1
|
||||
/// omitted both from <c>SettingsView.axaml</c> rather than building a placeholder for either, and this wave
|
||||
/// is the page each was waiting on — see design-notes/v5c-fidelity-notes.md. Neither has a
|
||||
/// <see cref="ShellScreen"/> counterpart: managing groups and tags has never been its own screen before this,
|
||||
/// only a panel inside the hosts board and the keychain screen respectively, so there is no existing binding
|
||||
/// for either to keep in step with.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal enum SettingsPage
|
||||
{
|
||||
/// <summary>Updates, and the refused items from the design's General page, as an essay.</summary>
|
||||
General = 0,
|
||||
|
||||
/// <summary>The vaults themselves and the people in them — the existing <see cref="ShellScreen.Vaults"/> screen.</summary>
|
||||
Vaults = 1,
|
||||
|
||||
/// <summary>The signed-in profile, the sign-in fact, and signing out of this machine.</summary>
|
||||
Account = 2,
|
||||
|
||||
/// <summary>The end-to-end explainer, Windows Hello, and approved host keys.</summary>
|
||||
Security = 3,
|
||||
|
||||
/// <summary>This machine's terminal and keychain settings — the existing <see cref="ShellScreen.Preferences"/> screen.</summary>
|
||||
Preferences = 4,
|
||||
|
||||
/// <summary>Every group, and the hosts filed under each — the existing group commands, given their own page.</summary>
|
||||
Groups = 5,
|
||||
|
||||
/// <summary>Every tag, and how many hosts wear each — the existing tag commands, given their own page.</summary>
|
||||
Tags = 6,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The shell: get to an unlocked vault, then hand over to <see cref="VaultViewModel"/>.
|
||||
/// </summary>
|
||||
@@ -368,6 +416,20 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Where <see cref="LeaveSettings"/> goes back to — captured once, on the turn settings mode is
|
||||
/// entered, and not touched again until it is left.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not re-captured on every <see cref="EnterSettings"/> call, which is what makes switching pages inside
|
||||
/// settings mode (Preferences, then Security, then Account) still come back to the one screen the user
|
||||
/// was actually on beforehand rather than to whichever settings page they last visited.
|
||||
/// </remarks>
|
||||
private ShellScreen settingsReturnScreen;
|
||||
|
||||
/// <inheritdoc cref="settingsReturnScreen" />
|
||||
private ShellSurface settingsReturnSurface;
|
||||
|
||||
/// <summary>
|
||||
/// Establishes a connection to a server.
|
||||
/// </summary>
|
||||
@@ -660,6 +722,19 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
[ObservableProperty]
|
||||
private string? email;
|
||||
|
||||
/// <summary>
|
||||
/// The OIDC issuer this account signs in through, when this machine has one cached — for the Account
|
||||
/// settings page's SIGN-IN row.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// v5c: <c>MeResponse.Issuer</c> was already being cached into <c>StoredUnlockMaterial.Issuer</c> by
|
||||
/// <see cref="AccountProvisioner"/>, for no reader — nothing before this wave surfaced it. Set from the
|
||||
/// same two places <see cref="AccountName"/> and <see cref="Email"/> are, in <see cref="AdoptIdentity"/>,
|
||||
/// so the three can never drift out of step with which account is actually signed in.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string? issuer;
|
||||
|
||||
/// <summary>Two letters for the rail's avatar circle, read off the signed-in display name.</summary>
|
||||
/// <remarks>
|
||||
/// The first letter of the first two words in <see cref="AccountName"/> — which is already
|
||||
@@ -700,10 +775,11 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// kept them from drifting apart the day one of the two calls gained <see cref="Email"/> and the other
|
||||
/// did not.
|
||||
/// </remarks>
|
||||
private void AdoptIdentity(string? displayName, string? emailAddress, string subject)
|
||||
private void AdoptIdentity(string? displayName, string? emailAddress, string subject, string? issuer = null)
|
||||
{
|
||||
AccountName = displayName ?? emailAddress ?? subject;
|
||||
Email = emailAddress;
|
||||
Issuer = issuer;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -980,9 +1056,6 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// <inheritdoc cref="IsHostsScreen" />
|
||||
internal bool IsKnownHostsScreen => Screen is ShellScreen.KnownHosts;
|
||||
|
||||
/// <inheritdoc cref="IsHostsScreen" />
|
||||
internal bool IsImportScreen => Screen is ShellScreen.Import;
|
||||
|
||||
/// <inheritdoc cref="IsHostsScreen" />
|
||||
internal bool IsSnippetsScreen => Screen is ShellScreen.Snippets;
|
||||
|
||||
@@ -1186,10 +1259,175 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
[RelayCommand]
|
||||
private void ShowScreen(ShellScreen target)
|
||||
{
|
||||
// v5c: Preferences and Vaults are settings pages now, and everywhere that used to navigate to either
|
||||
// of them — the rail's own popover, the phone's hub, a test calling this command by hand — is meant
|
||||
// to land in settings mode rather than on the bare screen the design retired. Redirecting here,
|
||||
// rather than at every caller, is what makes that true without hunting down every existing call.
|
||||
if (target is ShellScreen.Preferences)
|
||||
{
|
||||
EnterSettings(SettingsPage.Preferences);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target is ShellScreen.Vaults)
|
||||
{
|
||||
EnterSettings(SettingsPage.Vaults);
|
||||
return;
|
||||
}
|
||||
|
||||
// v5c: Import sits inside the settings chrome too, per Import.dc.html — SettingsNav stays lit on
|
||||
// Preferences, and what changes underneath it is the content column and the titlebar's own back
|
||||
// label, both driven by IsImportOpen rather than by a SettingsPage of its own. See OpenImport.
|
||||
if (target is ShellScreen.Import)
|
||||
{
|
||||
OpenImport();
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other screen leaves settings mode outright rather than restoring whatever was remembered on
|
||||
// the way in — the caller named a destination, and that destination wins over "go back".
|
||||
ActiveSettingsPage = null;
|
||||
Screen = target;
|
||||
Surface = ShellSurface.Page;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The full-window settings mode: its own titlebar, its own 340px rail, and a centred content column —
|
||||
/// see <c>SettingsView.axaml</c>. Not null exactly while that chrome, rather than the ordinary titlebar
|
||||
/// and nav rail, is what <c>MainWindow.axaml</c> draws.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A second notion of "where am I" from <see cref="Screen"/> rather than a replacement for it — see the
|
||||
/// remark on <see cref="SettingsPage"/>. Two of its five members, <see cref="SettingsPage.Preferences"/>
|
||||
/// and <see cref="SettingsPage.Vaults"/>, keep <see cref="Screen"/> in step with the matching
|
||||
/// <see cref="ShellScreen"/> member so every binding and test written against that screen before this
|
||||
/// mode existed keeps working; the other three have nothing to keep in step with.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private SettingsPage? activeSettingsPage;
|
||||
|
||||
/// <summary>Whether the settings chrome, rather than the ordinary one, is what the window is drawing.</summary>
|
||||
internal bool IsSettingsMode => ActiveSettingsPage is not null;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsGeneralPage => ActiveSettingsPage is SettingsPage.General;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsVaultsPage => ActiveSettingsPage is SettingsPage.Vaults;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsAccountPage => ActiveSettingsPage is SettingsPage.Account;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsSecurityPage => ActiveSettingsPage is SettingsPage.Security;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsPreferencesPage => ActiveSettingsPage is SettingsPage.Preferences;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsGroupsPage => ActiveSettingsPage is SettingsPage.Groups;
|
||||
|
||||
/// <inheritdoc cref="IsSettingsMode" />
|
||||
internal bool IsSettingsTagsPage => ActiveSettingsPage is SettingsPage.Tags;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the importer is showing over the Preferences page, inside settings mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A flag layered on top of <see cref="ActiveSettingsPage"/> rather than a <see cref="SettingsPage"/>
|
||||
/// member of its own — Import.dc.html draws <c>SettingsNav</c> lit on Preferences the whole time the
|
||||
/// importer is up, which this makes true for free: <see cref="ActiveSettingsPage"/> never leaves
|
||||
/// <see cref="SettingsPage.Preferences"/>, so <see cref="IsSettingsPreferencesPage"/> and the nav row it
|
||||
/// drives stay exactly as they were. What moves is only the content column, via
|
||||
/// <see cref="IsSettingsPreferencesContentShowing"/>, and the titlebar's own back label — see
|
||||
/// <c>SettingsTitleBar.axaml</c>.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private bool isImportOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Preferences page itself, rather than the importer drawn over it, is what settings mode's
|
||||
/// content column shows.
|
||||
/// </summary>
|
||||
internal bool IsSettingsPreferencesContentShowing => IsSettingsPreferencesPage && !IsImportOpen;
|
||||
|
||||
partial void OnActiveSettingsPageChanged(SettingsPage? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsSettingsMode));
|
||||
OnPropertyChanged(nameof(IsSettingsGeneralPage));
|
||||
OnPropertyChanged(nameof(IsSettingsVaultsPage));
|
||||
OnPropertyChanged(nameof(IsSettingsAccountPage));
|
||||
OnPropertyChanged(nameof(IsSettingsSecurityPage));
|
||||
OnPropertyChanged(nameof(IsSettingsPreferencesPage));
|
||||
OnPropertyChanged(nameof(IsSettingsGroupsPage));
|
||||
OnPropertyChanged(nameof(IsSettingsTagsPage));
|
||||
OnPropertyChanged(nameof(IsSettingsPreferencesContentShowing));
|
||||
}
|
||||
|
||||
partial void OnIsImportOpenChanged(bool value) =>
|
||||
OnPropertyChanged(nameof(IsSettingsPreferencesContentShowing));
|
||||
|
||||
/// <summary>Enters settings mode on a page, remembering where "Back to application" returns to.</summary>
|
||||
/// <remarks>
|
||||
/// The return screen is captured only on the way in from outside settings mode — see
|
||||
/// <see cref="settingsReturnScreen"/> — so switching between settings pages, which calls this
|
||||
/// repeatedly, cannot overwrite it with another settings page.
|
||||
/// <para>
|
||||
/// v5c: also closes the importer, on the same reasoning. Naming a page — including Preferences again — is
|
||||
/// a request for that page, not for whatever was drawn over it the last time settings mode was up.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void EnterSettings(SettingsPage page)
|
||||
{
|
||||
if (ActiveSettingsPage is null)
|
||||
{
|
||||
settingsReturnScreen = Screen;
|
||||
settingsReturnSurface = Surface;
|
||||
}
|
||||
|
||||
ActiveSettingsPage = page;
|
||||
IsImportOpen = false;
|
||||
|
||||
Screen = page switch
|
||||
{
|
||||
SettingsPage.Preferences => ShellScreen.Preferences,
|
||||
SettingsPage.Vaults => ShellScreen.Vaults,
|
||||
_ => Screen,
|
||||
};
|
||||
|
||||
Surface = ShellSurface.Page;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the importer over the Preferences page — the Preferences row's own "OPEN IMPORTER" button, and
|
||||
/// <see cref="ShowScreen"/>'s translation of <see cref="ShellScreen.Import"/> for every other caller.
|
||||
/// </summary>
|
||||
private void OpenImport()
|
||||
{
|
||||
EnterSettings(SettingsPage.Preferences);
|
||||
IsImportOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>"Back to preferences": closes the importer without leaving settings mode.</summary>
|
||||
[RelayCommand]
|
||||
private void CloseImport() => IsImportOpen = false;
|
||||
|
||||
/// <summary>"Back to application": leaves settings mode for wherever it was entered from.</summary>
|
||||
[RelayCommand]
|
||||
private void LeaveSettings()
|
||||
{
|
||||
if (ActiveSettingsPage is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveSettingsPage = null;
|
||||
IsImportOpen = false;
|
||||
Screen = settingsReturnScreen;
|
||||
Surface = settingsReturnSurface;
|
||||
}
|
||||
|
||||
/// <summary>Switches to the terminal surface.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -1911,7 +2149,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
return;
|
||||
}
|
||||
|
||||
AdoptIdentity(profile.DisplayName, profile.Email, profile.Subject);
|
||||
AdoptIdentity(profile.DisplayName, profile.Email, profile.Subject, profile.Issuer);
|
||||
ServerUrl = profile.ServerUrl;
|
||||
State = ShellState.Locked;
|
||||
StatusMessage = $"Enrolled against {profile.ServerUrl}.";
|
||||
@@ -1972,7 +2210,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
.RefreshAsync(ServerUrl, cancellationToken)
|
||||
.ConfigureAwait(true);
|
||||
|
||||
AdoptIdentity(outcome.Me.DisplayName, outcome.Me.Email, outcome.Me.Subject);
|
||||
AdoptIdentity(outcome.Me.DisplayName, outcome.Me.Email, outcome.Me.Subject, outcome.Me.Issuer);
|
||||
StatusMessage = outcome.Message;
|
||||
|
||||
if (outcome.Status == ProvisionStatus.EnrollmentRequired)
|
||||
@@ -2659,21 +2897,30 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
[RelayCommand]
|
||||
private void CancelSignOut() => IsConfirmingSignOut = false;
|
||||
|
||||
/// <summary>Starts a sign-out from the rail's user popover, from wherever the window is showing.</summary>
|
||||
/// <summary>
|
||||
/// Starts a sign-out from the rail's user popover, or from settings mode's own Logout row, from wherever
|
||||
/// the window is showing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="SignOut"/> only arms <see cref="IsConfirmingSignOut"/>; the confirmation itself is drawn
|
||||
/// inline on the Preferences screen while the vault is unlocked — see <c>PreferencesScreen.axaml</c> —
|
||||
/// and nowhere else, because <c>MainWindow.axaml</c>'s own copy of <c>SignOutCard</c> is inside the
|
||||
/// inline on the Account settings page while the vault is unlocked — see <c>SettingsAccountPage.axaml</c>
|
||||
/// — and nowhere else, because <c>MainWindow.axaml</c>'s own copy of <c>SignOutCard</c> is inside the
|
||||
/// setup half of the window, which is hidden the whole time this one is reachable. Calling
|
||||
/// <see cref="SignOut"/> straight from the popover on, say, the hosts screen would arm the flag with
|
||||
/// nothing on screen to show it — a card raised nobody can see. Going to Preferences first is what the
|
||||
/// popover's own "New vault" and "New bucket" rows already do for the same reason; see
|
||||
/// nothing on screen to show it — a card raised nobody can see. Entering settings on Account first is
|
||||
/// what the popover's own "New vault" and "New bucket" rows already do for the same reason; see
|
||||
/// <see cref="ShowNewVault"/>.
|
||||
/// <para>
|
||||
/// v5c: went to <c>ShellScreen.Preferences</c> before this wave, because that bare screen was the only
|
||||
/// place the confirmation card could be seen. It moved to the Account settings page with the card — see
|
||||
/// design-notes/v5c-fidelity-notes.md — and this is the one command both the rail's popover Logout row
|
||||
/// and settings mode's own bottom Logout row are wired to, so the confirmation has exactly one home.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void SignOutFromPopover()
|
||||
{
|
||||
ShowScreen(ShellScreen.Preferences);
|
||||
EnterSettings(SettingsPage.Account);
|
||||
SignOut();
|
||||
}
|
||||
|
||||
@@ -2744,6 +2991,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
|
||||
AccountName = null;
|
||||
Email = null;
|
||||
Issuer = null;
|
||||
Passphrase = string.Empty;
|
||||
ConfirmPassphrase = string.Empty;
|
||||
RecoveryCode = null;
|
||||
@@ -3017,8 +3265,19 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
// vault is opened or closed. It holds a subscription to the vault's pin list, so leaving one behind
|
||||
// would keep a disposed vault alive and repaint a screen nobody can reach.
|
||||
KnownHostsScreen?.Detach();
|
||||
KnownHostsScreen = newValue is null ? null : new KnownHostsViewModel(newValue);
|
||||
ImportScreen = newValue is null ? null : new ImportViewModel(newValue, new SshConfigLocator());
|
||||
|
||||
// v5c-3: the back arrow's own destination, on the same reasoning as ImportViewModel's onCancel below
|
||||
// — KnownHostsViewModel has no business knowing ShellScreen exists.
|
||||
KnownHostsScreen = newValue is null
|
||||
? null
|
||||
: new KnownHostsViewModel(newValue, () => ShowScreen(ShellScreen.Keychain));
|
||||
|
||||
// v5c-3: CloseImport, so the importer's own Cancel button can back out to the Preferences page
|
||||
// beneath it without ImportViewModel knowing anything about settings mode — the same reasoning
|
||||
// VaultViewModel's copyToClipboard delegate is built on.
|
||||
ImportScreen = newValue is null
|
||||
? null
|
||||
: new ImportViewModel(newValue, new SshConfigLocator(), CloseImport);
|
||||
|
||||
SnippetsScreen?.Detach();
|
||||
SnippetsScreen = newValue is null
|
||||
@@ -4086,7 +4345,6 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
OnPropertyChanged(nameof(IsVaultsScreen));
|
||||
OnPropertyChanged(nameof(IsPreferencesScreen));
|
||||
OnPropertyChanged(nameof(IsKnownHostsScreen));
|
||||
OnPropertyChanged(nameof(IsImportScreen));
|
||||
OnPropertyChanged(nameof(IsSnippetsScreen));
|
||||
OnPropertyChanged(nameof(IsLogsScreen));
|
||||
OnPropertyChanged(nameof(IsMoreScreen));
|
||||
|
||||
Reference in New Issue
Block a user