Public Access
544 lines
34 KiB
XML
544 lines
34 KiB
XML
<Window xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:sys="using:System.Collections.Generic"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
xmlns:views="using:DodoSSH.Client.App.Views"
|
|
x:Class="DodoSSH.Client.App.Views.MainWindow"
|
|
x:DataType="vm:MainWindowViewModel"
|
|
Title="DodoSSH"
|
|
Icon="/Assets/dodossh.ico"
|
|
Width="1180"
|
|
Height="760"
|
|
MinWidth="1081"
|
|
MinHeight="583"
|
|
Background="{StaticResource Canvas}"
|
|
SystemDecorations="BorderOnly"
|
|
Focusable="True">
|
|
|
|
<!--
|
|
The shell window: a titlebar it draws itself, a nav rail, one surface at a time, and a status bar.
|
|
|
|
THE MINIMUM GREW, and it grew by exactly what v2 added rather than by a round number somebody liked.
|
|
The sidebar went from 54 pixels to 190 and the chrome from 72 tall to 86, so 880x560 became 1016x574 —
|
|
which leaves every screen the same 826x464 it was designed against. That is the whole point of moving
|
|
it: the layout suite's premise is that everything fits at the minimum, and four of the tables stop
|
|
fitting at 690 wide. Widening the sidebar without widening the window would have quietly broken them
|
|
somewhere nobody was looking.
|
|
|
|
v5b moves it again, by exactly the same reasoning: the titlebar's fidelity pass takes it from 44 to 53
|
|
and the rail's from 190 to 255, so 1016x574 becomes 1081x583 — nine pixels and sixty-five pixels, added
|
|
straight onto the minimum rather than absorbed by shrinking a screen. <c>LayoutHarness.ScreenWidth</c>
|
|
stays unchanged at 826, because the rail is the only thing beside a page that grew.
|
|
|
|
<c>ScreenHeight</c> did move, and in the other direction: v5b's own fidelity pass also retires the
|
|
window-wide tab strip this comment used to describe — see the paragraph below — and every full-bleed
|
|
page gets that 42 pixels back rather than the window losing height to compensate. A screen this suite
|
|
measures is 826 pixels wide and taller than it was, by exactly the strip's own height; see
|
|
<c>LayoutHarness</c>'s own remark on the budget for the arithmetic.
|
|
|
|
Windows is asked for a resize border and nothing else, so TitleBar does the dragging, the maximising and
|
|
the closing. That is a real cost, and the reason it is paid is that a stock grey system bar above a
|
|
near-black application is the one part of the window that would look borrowed.
|
|
|
|
SystemDecorations="BorderOnly" rather than extending the client area under the system chrome, which is
|
|
what this tried first and is measurably wrong on Avalonia 12: ExtendClientAreaChromeHints is gone from
|
|
that version, so extending leaves the caption buttons and the window title drawn on top of this
|
|
window's own — two sets of minimise/maximise/close, and "DodoSSH" printed twice, overlapping. BorderOnly
|
|
removes the caption and keeps the resize border and the drop shadow, which is the half of the system
|
|
chrome worth having.
|
|
|
|
TWO SURFACES, ONE RECTANGLE — AND EACH ONE OWNS ITS OWN TAB ROW NOW.
|
|
|
|
v5b retires the tab strip this file used to draw above the whole window — Vaults, SFTP and S3 left it
|
|
for the rail's own switcher in an earlier pass, and this one moves the remaining pills, one per open
|
|
terminal, off the window's own chrome entirely. Each of the two screens that carries a tab row —
|
|
<c>SessionTabRow.axaml</c> — draws its own, 38 pixels, inside its own 26-pixel padded column, per the
|
|
design; see the terminal surface's own Grid below and the SFTP one inside the pages Panel. A tab still
|
|
survives navigating away from either screen — that is what makes the terminal reachable from anywhere —
|
|
it simply is not drawn as chrome above every screen while it does.
|
|
|
|
The terminal and the pages still share the one rectangle beside the rail, and exactly one of them may
|
|
occupy it at a time: that is the whole of ShellSurface, an enum rather than two flags, so there is no way
|
|
to write the state where both are showing.
|
|
|
|
THE OCCLUSION RULE, which every arrangement in this file obeys.
|
|
|
|
NativeWebView hosts a real Win32 child window through NativeControlHost, and a child window composites
|
|
above everything the parent paints — so no sibling in this visual tree can cover it, whatever the
|
|
z-order says. Layering a screen over it leaves that screen sliced at the WebView's left edge with its
|
|
buttons unreachable, which this window has shipped once already.
|
|
|
|
So anything that would occupy the terminal's rectangle collapses the terminal instead, and
|
|
IsTerminalShowing is the one place that decision is made: a locked vault, the page area, or the
|
|
quick-connect palette. Collapsing is safe, and cheaply so — NativeControlHost creates the native
|
|
control when the control is attached to the visual tree, not when it is laid out or shown, so WebView2
|
|
still starts, still loads the page and still lets the renderer attach its socket while it is false. It
|
|
only swaps ShowInBounds for HideWithSize, and flipping it back re-pushes the bounds.
|
|
|
|
Note where IsShowingPages is bound: on the one Panel that holds every screen, not on each screen. That
|
|
is what makes the rule hard to break rather than merely documented — a sixth screen added inside that
|
|
Panel cannot forget to collapse, because it is not the thing doing the collapsing. Its own IsVisible
|
|
only chooses between the pages.
|
|
|
|
Two nearby alternatives are wrong. Removing the control from the tree instead — conditional content, a
|
|
template swap — detaches it, and detaching destroys the native control and the whole WebView2 process
|
|
tree, so every unlock would pay a cold start. Hoisting the binding to an ancestor looks tidier and is
|
|
unverified: NativeControlHost does watch ancestors, but NativeWebView's own bounds-and-scaling re-push
|
|
fires only for its own IsVisible. v5b nests the WebView three levels deeper than it used to sit, inside
|
|
the terminal surface's own session shell — see that Grid's own remark below for why the rule still holds
|
|
with the control that much further from the Panel that used to be its only parent.
|
|
-->
|
|
|
|
<Grid RowDefinitions="Auto,*,Auto,Auto">
|
|
|
|
<views:TitleBar Grid.Row="0" IsVisible="{Binding !IsSettingsMode}" />
|
|
|
|
<Panel Grid.Row="1">
|
|
|
|
<!-- The unlocked application. -->
|
|
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding IsUnlocked}"
|
|
IsEnabled="{Binding !IsSettingsMode}">
|
|
|
|
<!--
|
|
The window's own furniture, at 255 pixels — see NavRail.axaml's own v5b remark. It no longer sits
|
|
under a strip: v5b retired the window-wide tab strip entirely, and with it the row this Grid used
|
|
to give up its own first row to. See the remark below on where a session's tabs live now.
|
|
-->
|
|
<views:NavRail Grid.Column="0" />
|
|
|
|
<Panel Grid.Column="1">
|
|
|
|
<!-- ============ THE PAGES ============ -->
|
|
<Panel IsVisible="{Binding IsShowingPages}">
|
|
|
|
<!--
|
|
Wrapped, like every other screen here. It used to be bound directly — its data context was
|
|
the shell's, so IsHostsScreen resolved on the same element — and that was only so it could
|
|
hand the vault to the host sidebar from inside its own markup. The sidebar is gone and the
|
|
drawer that replaced it is a plain child, so the screen takes the vault like the rest and
|
|
its visibility goes on the wrapper.
|
|
-->
|
|
<Panel IsVisible="{Binding IsHostsScreen}">
|
|
<views:HostsScreen x:Name="HostsPane" DataContext="{Binding Vault}" />
|
|
</Panel>
|
|
|
|
<!-- ============ SFTP ============ -->
|
|
<!--
|
|
◆ v5b's session shell, wrapping this screen's existing content rather than replacing it — the
|
|
two-pane grid and the queue inside TransfersScreen.axaml are untouched; wave C restyles their
|
|
internals. What is new here is everything design-notes/v5b-fidelity-notes.md calls the session
|
|
shell: a 26px padded column, an in-screen tab row, a bordered rounded-bottom container holding
|
|
a host header and a status bar around the screen's own content, and a 300px sidebar.
|
|
|
|
Gated on IsTransfersScreen exactly as before — IsShowingPages means "the Avalonia page area,
|
|
not the WebView", which is the occlusion question, and which mode the switcher is on is this
|
|
wrapper's own separate question.
|
|
|
|
The tab row's own click does not select a terminal tab — there is no per-tab SFTP session in
|
|
this application, and building one is out of this wave's scope; see the notes' own open
|
|
question and MainWindowViewModel.SelectFilesHostCommand for how this resolves it: a click
|
|
reuses the same "Browse files" plumbing a pin click already does, honestly opening (or
|
|
reusing) a second, SFTP-specific connection to that tab's host rather than pretending a session
|
|
exists that does not.
|
|
-->
|
|
<Panel IsVisible="{Binding IsTransfersScreen}">
|
|
<Grid RowDefinitions="Auto,*" Margin="26">
|
|
<views:SessionTabRow Grid.Row="0" Classes="sftp"
|
|
TabCommand="{Binding SelectFilesHostCommand}" />
|
|
<Border Grid.Row="1" BorderBrush="{StaticResource Border}" BorderThickness="1"
|
|
CornerRadius="0,0,12,12" ClipToBounds="True">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<!--
|
|
v5c-4: two rows rather than three. The 60-pixel host header that used to sit above this
|
|
screen is gone; the address and the "Open terminal" button it carried are in the
|
|
sidebar now — see SessionSidebar.axaml — and the pane keeps the height. Its third
|
|
binding, the Transfers.Status line it printed while no host was open, is not moved
|
|
either: TransfersScreen draws that same string itself, both in its own empty state and
|
|
beside the remote pane's DISCONNECT once something is open.
|
|
-->
|
|
<!--
|
|
Three rows since the disconnect strip arrived, and it is the same control the terminal
|
|
surface draws — see SessionEndedBar.axaml, which resolves which surface it is about
|
|
through the shell rather than through a binding of its own. Auto and empty except while
|
|
the file session has been lost, so an ordinary browse pays nothing for it.
|
|
|
|
Note the DataContext: this row is the shell's rather than the transfers screen's, which
|
|
is why the strip sits here and not inside TransfersScreen.axaml.
|
|
-->
|
|
<Grid Grid.Column="0" RowDefinitions="*,Auto,Auto">
|
|
<views:TransfersScreen Grid.Row="0" DataContext="{Binding Transfers}" />
|
|
<views:SessionEndedBar Grid.Row="1" />
|
|
<views:SessionStatusBar Grid.Row="2" />
|
|
</Grid>
|
|
<!--
|
|
Hides when no session is active — see ShowsQuickAccessSidebar — rather than always
|
|
drawn: the SFTP surface reaches this Panel before a host is chosen, and a sidebar with
|
|
an empty QUICK ACCESS and no host name to print would be furniture with nothing to say.
|
|
Collapsing frees the "Auto" column it sits in, so the pane column takes the width back.
|
|
-->
|
|
<views:SessionSidebar Grid.Column="1"
|
|
IsVisible="{Binding ShowsQuickAccessSidebar, FallbackValue=False}" />
|
|
</Grid>
|
|
</Border>
|
|
</Grid>
|
|
</Panel>
|
|
|
|
<!-- ============ S3 ============ -->
|
|
<!--
|
|
The same screen as SFTP above, over the same view model, because an object store and an
|
|
SFTP host are both an IRemoteFileStore and everything below the picker was written once.
|
|
What differs is which picker is offered, and that is decided by the destination rather than
|
|
by a toggle inside the screen — see ShowFiles, and the rail's own switcher segment that
|
|
calls it now; see NavRail.axaml.
|
|
|
|
Deliberately not given the full session shell above: a bucket is not a host, has no terminal tab
|
|
to be the other end of a cross-surface button, and pins nothing the sidebar's QUICK ACCESS
|
|
could show — so no tab row, no host header, no status bar and no sidebar. Wave B's own scope
|
|
was the terminal and SFTP surfaces and left this plain; v5c-3 gives it the session shell's own
|
|
LOOK without its machinery — a 26px padded column and the same bordered, radius-12 container —
|
|
since design-notes/v5c-fidelity-notes.md calls for the fidelity pass here too, and there is
|
|
nothing this screen does that needs the parts still withheld.
|
|
-->
|
|
<Panel IsVisible="{Binding IsBucketsScreen}">
|
|
<Border Margin="26" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
|
|
ClipToBounds="True">
|
|
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
|
</Border>
|
|
</Panel>
|
|
|
|
<!-- ============ KEYCHAIN ============ -->
|
|
<!--
|
|
Wrapped rather than bound directly, for the reason the vault column always was: this
|
|
element's visibility is the shell's business and its data context is the vault, and put both
|
|
on one element and IsVisible resolves against the vault as well, where IsKeychainScreen does
|
|
not exist.
|
|
-->
|
|
<Panel IsVisible="{Binding IsKeychainScreen}">
|
|
<views:KeychainScreen x:Name="VaultPane" DataContext="{Binding Vault}" />
|
|
</Panel>
|
|
|
|
<!-- ============ HOST KEYS ============ -->
|
|
<!--
|
|
Wrapped, like the two above and for the same reason: its data context is the screen's own
|
|
view model, where IsKnownHostsScreen does not exist.
|
|
-->
|
|
<Panel IsVisible="{Binding IsKnownHostsScreen}">
|
|
<views:KnownHostsScreen x:Name="PinsPane" DataContext="{Binding KnownHostsScreen}" />
|
|
</Panel>
|
|
|
|
<!-- ============ SNIPPETS ============ -->
|
|
<!-- Wrapped, like the others whose data context is their own view model. -->
|
|
<Panel IsVisible="{Binding IsSnippetsScreen}">
|
|
<views:SnippetsScreen x:Name="SnippetsPane" DataContext="{Binding SnippetsScreen}" />
|
|
</Panel>
|
|
|
|
<!-- ============ LOGS ============ -->
|
|
<!-- Wrapped, like the others whose data context is their own view model. -->
|
|
<Panel IsVisible="{Binding IsLogsScreen}">
|
|
<views:LogsScreen x:Name="LogsPane" DataContext="{Binding LogsScreen}" />
|
|
</Panel>
|
|
|
|
<!--
|
|
v5c-2: the VAULTS panel that used to live here is gone. ShellScreen.Vaults now only ever shows
|
|
through settings mode's own SettingsVaultsPage — see MainWindowViewModel.ShowScreen, which
|
|
redirects it into EnterSettings before Screen is ever set — so IsVaultsScreen being true also
|
|
means IsSettingsMode is true, and this whole page-area Panel (bound to !IsSettingsMode, above)
|
|
is already hidden behind SettingsView by then. A Panel here would never have been drawn.
|
|
|
|
v5c-3: IMPORT left this Panel the same way, and for the same reason. Import.dc.html draws the
|
|
importer inside the settings chrome — SettingsNav lit on Preferences, a "Back to preferences"
|
|
titlebar rather than the ordinary rail and titlebar this Panel belongs to — so ImportScreen now
|
|
lives in SettingsView.axaml behind MainWindowViewModel.IsImportOpen, beside the seven settings
|
|
pages rather than among the screens here. See ShowScreen's own translation of
|
|
ShellScreen.Import for what still reaches it: the Preferences page's own "OPEN IMPORTER" row,
|
|
and anything else that names the same destination.
|
|
-->
|
|
|
|
</Panel>
|
|
|
|
<!-- ============ TERMINAL ============ -->
|
|
<!--
|
|
◆ v5b's session shell for the terminal surface, built the same way SFTP's own wrapper above is —
|
|
around the real terminal control rather than around a page.
|
|
|
|
Gated on IsTerminalSurface rather than on IsShowingPages: the two are always exclusive, because
|
|
Surface is a single ShellSurface value, so this Grid and the pages Panel above it are never both
|
|
visible at once. That is what keeps the occlusion rule intact with the WebView now nested inside
|
|
a padded column, a bordered container and a header row rather than sitting directly beside the
|
|
page area the way it used to.
|
|
|
|
THE RULE ITSELF DID NOT MOVE. NativeWebView's own IsVisible binding, below, is unchanged and is
|
|
not hoisted to this Grid — hoisting it is the one alternative the pin-strip era of this file
|
|
called out as unverified, and nesting the control deeper without touching its own binding is not
|
|
that: IsTerminalShowing already depends on IsTerminalSurface, so this Grid's own visibility and
|
|
the WebView's own visibility flip together on every surface change, driven by the same property
|
|
change rather than one waiting on the other.
|
|
|
|
The tab row's own click selects a terminal tab exactly as the old window-wide strip's did; see
|
|
SelectTabCommand. "+" keeps its current meaning, quick connect, on both this row and SFTP's own.
|
|
-->
|
|
<Grid RowDefinitions="Auto,*" Margin="26" IsVisible="{Binding IsTerminalSurface}">
|
|
<views:SessionTabRow Grid.Row="0" TabCommand="{Binding SelectTabCommand}" />
|
|
<Border Grid.Row="1" BorderBrush="{StaticResource Border}" BorderThickness="1"
|
|
CornerRadius="0,0,12,12" ClipToBounds="True">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<!--
|
|
v5c-4: two rows rather than three, the same as the SFTP wrapper above and for the same
|
|
reason — the host header is gone and the terminal has its 60 pixels. The empty state it
|
|
used to print ("no terminals open · press + or Ctrl+K…") went with it rather than moving:
|
|
this Grid is only drawn on the terminal surface, and the surface with no tab open already
|
|
answers for itself in the tab row's own "+" and in the connecting card below.
|
|
-->
|
|
<!--
|
|
Three rows since the disconnect strip arrived, where v5c-4 left two. The extra one is Auto
|
|
and empty except while the selected tab's shell has stopped, so it costs the terminal
|
|
nothing in the ordinary case — and when it is up it takes its 34 pixels off the pane rather
|
|
than drawing over it, which is the only way Avalonia content can sit near the renderer at
|
|
all. See SessionEndedBar.axaml.
|
|
-->
|
|
<Grid Grid.Column="0" RowDefinitions="*,Auto,Auto">
|
|
|
|
<Panel Grid.Row="0" Background="{StaticResource Pane}">
|
|
|
|
<!--
|
|
The other thing that can be in the terminal's rectangle: a tab whose session does not
|
|
exist yet, or never will. Exclusive with the WebView below by construction — a
|
|
selected tab either has a session or it does not — which is what makes drawing it here
|
|
safe under the occlusion rule, the same way the page area is. See ConnectingCard.axaml.
|
|
-->
|
|
<views:ConnectingCard x:Name="ConnectingPane"
|
|
IsVisible="{Binding IsConnectingShowing, FallbackValue=False}" />
|
|
|
|
<!--
|
|
One WebView hosting every terminal. Not one per tab: each WebView2 is a separate
|
|
browser process tree, so twenty tabs would cost twenty of them.
|
|
|
|
FallbackValue, because a compiled binding with no DataContext yields UnsetValue,
|
|
IsVisible then falls back to its default of true, and the occlusion comes back
|
|
silently. Not reachable at runtime — the DataContext is set before the window is shown
|
|
— but it is what the previewer does.
|
|
-->
|
|
<NativeWebView x:Name="Terminal"
|
|
IsVisible="{Binding IsTerminalShowing, FallbackValue=False}" />
|
|
|
|
</Panel>
|
|
|
|
<views:SessionEndedBar Grid.Row="1" />
|
|
<views:SessionStatusBar Grid.Row="2" ShowsEncoding="True" />
|
|
</Grid>
|
|
<!--
|
|
Hides when no session is active — see ShowsQuickAccessSidebar — rather than always drawn:
|
|
the terminal surface is reachable with no tab open at all (the rail's own SSH segment), and
|
|
a sidebar naming no host would be furniture with nothing to say. Collapsing frees the
|
|
"Auto" column it sits in, so the pane column takes the width back.
|
|
-->
|
|
<views:SessionSidebar Grid.Column="1" ShowsSnips="True" ShowsForwards="True"
|
|
IsVisible="{Binding ShowsQuickAccessSidebar, FallbackValue=False}" />
|
|
</Grid>
|
|
</Border>
|
|
</Grid>
|
|
|
|
</Panel>
|
|
</Grid>
|
|
|
|
<!--
|
|
v5c: the settings mode, which replaces the titlebar above and everything in this Panel below it with
|
|
its own — see SettingsView.axaml and TitleBar's own IsVisible next to it in this file. Later in the
|
|
Panel than the unlocked Grid, on the same reasoning the setup Border below gives for its own
|
|
position: Avalonia z-order draws a later sibling over an earlier one, and SettingsView's own
|
|
Background is opaque, so it covers the rail and the page area completely rather than needing them
|
|
hidden out from under it. The unlocked Grid keeps IsEnabled bound to the negation above so a Tab
|
|
press cannot walk keyboard focus into a control this mode has covered.
|
|
|
|
Earlier than the palette and the host-key decision, both further down this same Panel, so either can
|
|
still interrupt settings mode exactly as it already interrupts every other screen — a host key that
|
|
changes while the sync loop is running does not stop being worth answering because the window
|
|
happens to be showing Settings.
|
|
-->
|
|
<views:SettingsView IsVisible="{Binding IsSettingsMode}" />
|
|
|
|
<!--
|
|
Setup and unlock. Later in the Panel, so it is above the application content in Avalonia's z-order —
|
|
which covers Avalonia-drawn content and nothing else. The terminal is collapsed rather than covered;
|
|
see the occlusion rule above.
|
|
-->
|
|
<Border Background="{StaticResource Canvas}" IsVisible="{Binding !IsUnlocked}">
|
|
<Panel>
|
|
|
|
<Border Classes="card" IsVisible="{Binding IsStarting}">
|
|
<StackPanel Spacing="10">
|
|
<TextBlock Classes="heading" Text="DodoSSH" />
|
|
<TextBlock Classes="hint" Text="{Binding StatusMessage}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<Border Classes="card" IsVisible="{Binding IsNeedingServer}">
|
|
<StackPanel Spacing="12">
|
|
<TextBlock Classes="heading" Text="Connect to your server" />
|
|
<TextBlock Classes="hint"
|
|
Text="One address is all this needs. The identity provider, the client id and the scopes all come from the server itself." />
|
|
<!--
|
|
Named because signing out lands here with the keyboard needing somewhere to go: the vault's
|
|
controls have just been collapsed, and Focus() on a collapsed control is a no-op that is
|
|
not replayed. See MainWindow.axaml.cs.
|
|
-->
|
|
<TextBox x:Name="ServerUrlBox" Text="{Binding ServerUrl}"
|
|
PlaceholderText="https://dodossh.example">
|
|
<!-- Enter signs in, as it unlocks on the unlock card. -->
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding SignInCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<Button Classes="accent" Content="SIGN IN WITH YOUR BROWSER" Command="{Binding SignInCommand}"
|
|
IsEnabled="{Binding !IsBusy}" HorizontalAlignment="Left" />
|
|
<TextBlock Classes="hint" Text="{Binding StatusMessage}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<Border Classes="card" IsVisible="{Binding IsNeedingEnrollment}">
|
|
<StackPanel Spacing="12">
|
|
<TextBlock Classes="heading" Text="Choose a keychain passphrase" />
|
|
<TextBlock Classes="hint"
|
|
Text="This passphrase never leaves this machine, and the server cannot reset it. It is the only thing standing between a stolen copy of the database and every credential in your keychain." />
|
|
<TextBox x:Name="NewPassphraseBox" Text="{Binding Passphrase}" PlaceholderText="passphrase"
|
|
PasswordChar="•" />
|
|
<TextBox Text="{Binding ConfirmPassphrase}" PlaceholderText="again" PasswordChar="•">
|
|
<!-- On the second box only: Enter in the first moves nowhere useful, Enter here is "done". -->
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding EnrollCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<Button Classes="accent" Content="CREATE MY KEYCHAIN" Command="{Binding EnrollCommand}"
|
|
IsEnabled="{Binding !IsBusy}" HorizontalAlignment="Left" />
|
|
<TextBlock Classes="hint" Text="{Binding StatusMessage}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
Shown once and impossible to skip. This is the only moment the code exists, and losing it
|
|
together with the passphrase means the keychain is unrecoverable — there is no server-side reset
|
|
by design.
|
|
-->
|
|
<Border Classes="card" IsVisible="{Binding IsShowingRecoveryCode}">
|
|
<StackPanel Spacing="12">
|
|
<TextBlock Classes="heading" Text="Write this recovery code down" />
|
|
<TextBlock Classes="hint"
|
|
Text="It is shown once and is not stored anywhere. Without it, forgetting your passphrase means losing the keychain: nobody — including whoever runs the server — can recover it for you." />
|
|
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
|
BorderThickness="1" CornerRadius="6" Padding="14">
|
|
<SelectableTextBlock Classes="mono" Text="{Binding RecoveryCode}"
|
|
FontSize="17" Foreground="{StaticResource Accent}"
|
|
TextWrapping="Wrap" />
|
|
</Border>
|
|
<!--
|
|
◆ COPY, beside the tick rather than inside the box. The code is selectable and has been
|
|
since this card existed, on the argument that refusing to let somebody take it only pushes
|
|
them to a screenshot — this is that argument finished, because "select 64 characters of
|
|
letter-spaced monospace without missing one" is a thing people get wrong silently.
|
|
|
|
It is the one secret this application offers to the clipboard on purpose; see
|
|
MainWindowViewModel.CopyRecoveryCode for why that is right here and refused for a private
|
|
key, and for what the status line below says afterwards.
|
|
-->
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<Button Classes="ghost" Content="COPY CODE"
|
|
Command="{Binding CopyRecoveryCodeCommand}"
|
|
ToolTip.Tip="Puts the code on the clipboard. Paste it into your password manager now — it is shown once and the next thing you copy replaces it." />
|
|
<CheckBox IsChecked="{Binding RecoveryCodeWrittenDown}"
|
|
Content="I have written it down somewhere safe"
|
|
VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
<Button Classes="accent" Content="CONTINUE" Command="{Binding ConfirmRecoveryCodeCommand}"
|
|
HorizontalAlignment="Left" />
|
|
<TextBlock Classes="hint" Text="{Binding StatusMessage}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
The unlock card, and the sign-out confirmation that replaces it. Both live in their own files:
|
|
nothing inside this window can be laid out by a test — WebView2's adapter refuses the headless
|
|
session's thread — so markup that stays here is markup nobody can measure. See UnlockCard.axaml.
|
|
-->
|
|
<Border Classes="card" IsVisible="{Binding IsAskingForThePassphrase}">
|
|
<views:UnlockCard x:Name="UnlockPane" />
|
|
</Border>
|
|
|
|
<!--
|
|
The confirmation, in place of the unlock card rather than under it: the card is already close
|
|
to the height the window guarantees at its minimum, and a screen a user cannot read all of is
|
|
worse than one that shows one question at a time.
|
|
-->
|
|
<Border Classes="card" IsVisible="{Binding IsConfirmingSignOut}">
|
|
<views:SignOutCard />
|
|
</Border>
|
|
|
|
</Panel>
|
|
</Border>
|
|
|
|
<!--
|
|
Last, so it is over everything Avalonia draws. It is over the terminal too, because opening it
|
|
collapses the terminal — see IsTerminalShowing.
|
|
-->
|
|
<views:QuickConnect x:Name="Palette" IsVisible="{Binding IsSearching}" />
|
|
|
|
<!--
|
|
◆ Later still, and the order between these two is a ranking rather than an accident: the palette is
|
|
something the user opened and can close, and a host-key decision is a connection waiting on them. It
|
|
used to be two banners at the top of the hosts screen, which is why the shell navigated there before
|
|
the question could be asked — see HostKeyCard.axaml, and MainWindowViewModel.OnVaultConnectionFailed
|
|
for what that cost every other screen.
|
|
|
|
Wrapped, like every other child here whose data context is the vault: the visibility is the shell's
|
|
business — it is the flag that also collapses the WebView — and IsHostKeyDecisionShowing does not
|
|
exist on a VaultViewModel, which with compiled bindings is a build error rather than a card that
|
|
silently never appears.
|
|
-->
|
|
<Panel IsVisible="{Binding IsHostKeyDecisionShowing, FallbackValue=False}">
|
|
<views:HostKeyCard x:Name="HostKeyPane" DataContext="{Binding Vault}" />
|
|
</Panel>
|
|
|
|
</Panel>
|
|
|
|
<!--
|
|
A fourth row, and a row rather than an overlay for the reason the occlusion rule above gives: this
|
|
appears while a terminal may be open, and anything drawn in the WebView's rectangle is sliced. Taking
|
|
height from the row above moves the native control's bounds instead of covering it, which is the one
|
|
arrangement that works — the same one TitleBar and StatusBar already rely on.
|
|
|
|
It is a separate control because nothing in this file can be measured by a test, and a strip with two
|
|
buttons and a version string of unknown length is exactly the shape that arranges one of them off the
|
|
edge. See UpdateBanner.axaml.
|
|
|
|
◆ THE DATA CONTEXT IS SET HERE, and the banner did nothing at all until it was.
|
|
|
|
Unlike the titlebar and the status bar, which are typed to this window's own view model and inherit its
|
|
context, the banner is typed to UpdateViewModel — it is one screen's control and its layout suite hosts
|
|
it over that view model alone. Inheriting the shell's context instead left every compiled binding
|
|
inside it resolving against the wrong type and failing silently: no headline, and both Commands null,
|
|
so the strip appeared, hovered and pressed like a real banner and neither button did anything.
|
|
|
|
IsVisible is unqualified because the context is set on this same element, which resolves it against
|
|
UpdateViewModel too — the rule the page area's wrappers above are wrapped for. It needs no wrapper: the
|
|
flag it binds is the banner's own, unlike IsHostsScreen and its siblings, which belong to the shell.
|
|
|
|
FallbackValue, for the reason the WebView and the connecting card carry one: a compiled binding with
|
|
no DataContext yields UnsetValue, IsVisible falls back to true, and the previewer would show a banner
|
|
announcing an update that does not exist.
|
|
-->
|
|
<!--
|
|
v5c: wrapped rather than given a third condition of its own — UpdateBanner sets its own DataContext to
|
|
UpdateViewModel on this same element, so a binding against IsSettingsMode has to live on an ancestor
|
|
whose context is still the shell. The design's settings titlebar has no room for this strip; hiding it
|
|
here rather than teaching the banner about a mode it otherwise has no reason to know about.
|
|
-->
|
|
<Panel Grid.Row="2" IsVisible="{Binding !IsSettingsMode}">
|
|
<views:UpdateBanner DataContext="{Binding Updates}"
|
|
IsVisible="{Binding IsBannerShowing, FallbackValue=False}" />
|
|
</Panel>
|
|
|
|
<views:StatusBar Grid.Row="3" IsVisible="{Binding !IsSettingsMode}" />
|
|
|
|
</Grid>
|
|
|
|
</Window>
|