Public Access
Make Connections the place a connection is made, and put the keychain away
Four changes to the phone, and the last one needed the connect path taking apart. **The bottom bar is three entries.** The keychain moved onto the hub, which is now SETTINGS with a gear rather than MORE with a hamburger. A bottom bar is for the places a session moves between, and keys, credentials and tags are managed occasionally and then left alone — which is the shape of everything already behind that hub. With the keychain on it, "more" stopped being a description of what is there. `ShellScreen.Vault` joining `IsMoreSurface` is the whole of the change: the tab that lights, the header that stands down and the back gesture's first case all read that one property, which is why the switch mirrors it by construction rather than by a second list. The keychain screen grew the header every hub screen has, because the shell's own is not above it any more and without one there would be no back arrow and nothing saying what the list is. The desktop keeps its Keychain rail entry. A rail with nine slots has room, so this is the second thing the two heads arrange deliberately differently, after the hub itself. **Terminal became Connections**, and the word does more work than a rename usually does — see below. The enum member stays `ShellSurface.Terminal`, for the reason the tab was never called Vault: the surface is a terminal, and the word a user reads is the product's. **The + puts the software keyboard away.** It sits above a terminal somebody is typing into, so the sheet it raises was arriving underneath a keyboard covering the half of the screen the sheet is on — and worse, laid out into the strip left above it, since the keyboard's inset shortens everything this head draws. Avalonia cannot do this and it is worth knowing why: `TopLevel.InputPane` reports the keyboard and offers nothing that closes one, because the framework's model is that it belongs to whatever has focus — and this keyboard was raised by the `WebView`'s own text input, by a native view Avalonia's focus manager never owned. Clearing Avalonia's focus leaves it exactly where it is. So `Platform/SoftKeyboard.cs` asks `InputMethodManager`, off the decor view's window token, and every step of it is allowed to be absent. **With nothing open, Connections is a connect screen rather than an empty state.** A box taking `user@host` or `user@host:port`, a password, and the machines most recently connected to underneath. The box is the only path in this product to a machine the keychain has never heard of, which is a real case it had no answer for: an address somebody was handed five minutes ago. A typed password and nothing else — offering the keychain's keys would be a second binding resolution beside `TryBuildAuthentication`, and the argument against a second one is written there at length. Nothing typed is saved, and the screen says so: a machine worth keeping belongs on HOSTS, where it can carry a key, a group's defaults and a name. The recents come out of the vault's own connection log rather than a list kept in this process, so they survive a restart and arrive on a new phone with the keychain. Deduplicated by address, because this is a list of places and not of events, and capped at six so the box stays above the keyboard. Emptied when the vault is — they are decrypted entries naming where somebody works, and a lock that left them on screen would be a list still readable after every key that decrypted it was zeroed. Tapping one leads to whichever of two things it is: a keychain host goes to that host's connect bar, where its key, its password box and its refusals already live, and an address goes back into the box, without the password, whose absence is the point of that path rather than a gap in it. **The connect path was shaped like `HostRowViewModel` all the way down.** The log entry, the identification, the failure record and the retry all took a row. They take a four-field `ConnectionTarget` now, so a connection to an address shares the ladder of refusals, the host-key question and the tab's lifecycle rather than growing a second copy of them. `ConnectionRecorder.Record` and `Identify` have always taken a nullable host id, so the log could already hold a connection with no item behind it. One behavioural change falls out of that and it is the one to know about: **trusting a host key now retries the attempt that raised the question** instead of re-running whichever host is selected. That was correct while a selected host was the only way to connect; with a manual target it would dial a different machine, or refuse with "choose a host first" over a key the user has just agreed to trust. The test selects a host first, so a regression cannot pass by connecting to the wrong thing successfully. `LogsViewModel.ReloadAsync` split so the connections half can be read alone. Reading the keychain's activity for a screen that offers neither would double the decryption on the list that was already the expensive one. Twelve tests: the parse grammar as a theory over seven refusals, the dialled request, the retry, and both branches of tapping a recent row. The recents rows are built by hand rather than connected-and-closed — what those tests are about is which branch a row takes, and driving it through the recorder's queue would test the recorder, which `DodoSSH.Client.Session.Tests` already does. What needs a device is phases 11.6 to 11.9 of `docs/manual-checks.md`.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:views="using:DodoSSH.Client.Android.Views"
|
||||
x:Class="DodoSSH.Client.Android.Views.KeychainScreen"
|
||||
x:DataType="vm:VaultViewModel"
|
||||
Background="{StaticResource Canvas}">
|
||||
@@ -26,12 +27,30 @@
|
||||
|
||||
Mode=OneWay on the chips is load-bearing: a two-way selection binding moves the chip before the command
|
||||
beside it has had the chance to refuse, which would leave the row lit for a section the list is not on.
|
||||
|
||||
── v3 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
This screen moved behind SETTINGS, so it draws its own header — the one every hub screen has. It is not
|
||||
a decoration: the shell's header stands down behind the hub, and without a row of its own there would be
|
||||
no back arrow and nothing on screen saying what this list is.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<!-- ============ header ============ -->
|
||||
<!--
|
||||
The same shape as the one over PREFERENCES in PhoneShell, and pointed at the same place. Back goes to
|
||||
the hub rather than to HOSTS, because the hub is where this screen is reached from and back that
|
||||
skipped it would strand the four rows underneath it.
|
||||
-->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*" Height="56" Margin="8,0">
|
||||
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Text="Keychain" Margin="4,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- ============ categories ============ -->
|
||||
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
Margin="0,12,0,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="16,0">
|
||||
|
||||
@@ -91,7 +110,7 @@
|
||||
them are still waiting to be pushed — which is the honest version of the design's unconditional
|
||||
"synced".
|
||||
-->
|
||||
<TextBlock Grid.Row="1" Classes="detail" Margin="18,6,18,2" Text="{Binding SectionSummary}" />
|
||||
<TextBlock Grid.Row="2" Classes="detail" Margin="18,6,18,2" Text="{Binding SectionSummary}" />
|
||||
|
||||
<!-- ============ ◆ the counted confirmation ============ -->
|
||||
<!--
|
||||
@@ -99,7 +118,7 @@
|
||||
"three hosts authenticate with this key and will refuse to connect" — and the count is the whole
|
||||
difference between a sentence somebody reads and one they click past.
|
||||
-->
|
||||
<Border Grid.Row="2" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
|
||||
<Border Grid.Row="3" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
BorderThickness="1" CornerRadius="12" Padding="14,12">
|
||||
<StackPanel Spacing="5">
|
||||
@@ -120,7 +139,7 @@
|
||||
</Border>
|
||||
|
||||
<!-- ============ the items ============ -->
|
||||
<Panel Grid.Row="3">
|
||||
<Panel Grid.Row="4">
|
||||
|
||||
<!-- Written per section by the view model, and it says what the section is for. -->
|
||||
<TextBlock Classes="body" IsVisible="{Binding !HasVaultItems}" Margin="24,20"
|
||||
|
||||
@@ -6,26 +6,60 @@
|
||||
Background="{StaticResource Canvas}">
|
||||
|
||||
<!--
|
||||
Design v2 — MORE: the hub for everything the bottom bar has no room for.
|
||||
Design v2 — SETTINGS: the hub for everything the bottom bar has no room for.
|
||||
|
||||
Four slots and nine destinations is the arithmetic the design solves by putting five of them one tap
|
||||
Three slots and nine destinations is the arithmetic the design solves by putting the rest one tap
|
||||
deeper. This screen is that tap. It takes the shell as its data context rather than the vault, because
|
||||
every row on it is a navigation command and nothing here reads an item.
|
||||
|
||||
The rows are the design's list rows rather than cards: a card is one thing you act on, and a destination
|
||||
is not a thing — it is a place. Each carries a sentence saying what is behind it, because a hub whose
|
||||
entries are one word each is a menu you have to open to read.
|
||||
|
||||
── from MORE to SETTINGS ──────────────────────────────────────────────────────────────────────────────
|
||||
The hub was called More because that was the honest name for five things that did not fit. With the
|
||||
keychain on it as well it is the place everything that is not connecting to a machine lives, which has a
|
||||
name of its own and a glyph everyone already knows.
|
||||
|
||||
The keychain is first, and above the rule, because it is the only row here that holds the user's own
|
||||
material rather than a screen about the application. Everything below it is somewhere to look at what
|
||||
has happened or how this phone behaves.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<Border Grid.Row="0" Height="56" Padding="16,0">
|
||||
<TextBlock Classes="heading" Text="More" />
|
||||
<TextBlock Classes="heading" Text="Settings" />
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel Margin="10,0,10,20" Spacing="2">
|
||||
|
||||
<!--
|
||||
"Keychain", although the design's tab says Vault and the enum member is Vault. The split is a
|
||||
standing rule rather than an inconsistency: every sentence a user reads calls the encrypted store a
|
||||
keychain, and everything under the surface calls it a vault, because renaming the wire route, the
|
||||
tables and the CLR types would put an AAD resource type inside the blast radius of a
|
||||
find-and-replace. See docs/design-import-gaps.md. A row reading Vault would be the one place the
|
||||
product surface leaked the implementation's word.
|
||||
-->
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vault}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="⚿" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||
Width="22" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Keychain" />
|
||||
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||
Text="Keys, passwords, buckets and tags — everything in the vault that is not a host." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Border Height="1" Background="{StaticResource Border}" Margin="16,8" />
|
||||
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Snippets}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
@@ -106,10 +140,10 @@
|
||||
<!--
|
||||
Locking, here as well as in the header.
|
||||
|
||||
The header carries it on the three top-level destinations and stands down behind MORE, so without
|
||||
this row the five screens under here would each be two taps from locking the keychain. That is the
|
||||
wrong direction for the one action whose whole value is being quick, and it is why this is a row
|
||||
on the hub rather than something you go back for.
|
||||
The header carries it on HOSTS and stands down behind SETTINGS, so without this row every screen
|
||||
under here would be two taps from locking the keychain. That is the wrong direction for the one
|
||||
action whose whole value is being quick, and it is why this is a row on the hub rather than
|
||||
something you go back for.
|
||||
-->
|
||||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding LockCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
|
||||
@@ -56,11 +56,11 @@
|
||||
<!--
|
||||
The header: which vault, and whether it is synced.
|
||||
|
||||
Hidden behind MORE, and that is the design's arrangement rather than a saving. v2 gives every screen
|
||||
one header carrying that screen's own name and its own actions — a back arrow, an add, a refresh —
|
||||
so the five hub screens draw their own and this one stands down rather than stacking a second row of
|
||||
chrome above theirs. It stays on the two destinations that are the product's top level, where the
|
||||
vault's name and the sync light are the most useful thing a header could say.
|
||||
Hidden behind SETTINGS, and that is the design's arrangement rather than a saving. v2 gives every
|
||||
screen one header carrying that screen's own name and its own actions — a back arrow, an add, a
|
||||
refresh — so the hub screens draw their own and this one stands down rather than stacking a second
|
||||
row of chrome above theirs. What is left is HOSTS, which is the screen the application opens on and
|
||||
the one where the vault's name and the sync light are the most useful thing a header could say.
|
||||
|
||||
Wrapped rather than given a second condition, because Avalonia's bindings have no "and": the wrapper
|
||||
collapses it over a terminal, where the surface draws its own bar and the vault's name is not what
|
||||
@@ -241,17 +241,23 @@
|
||||
|
||||
<!-- ============ navigation ============ -->
|
||||
<!--
|
||||
Gone while a terminal is showing, which is the whole of that surface's arrangement: the bar's four
|
||||
destinations are replaced by a back arrow and a + that leads to three of them, both in the terminal's
|
||||
Gone while a terminal is showing, which is the whole of that surface's arrangement: the bar's three
|
||||
destinations are replaced by a back arrow and a + that leads to all of them, both in the terminal's
|
||||
own bar. See TerminalScreen.axaml.
|
||||
|
||||
This one is bound directly rather than wrapped — its visibility is a single question and it has no
|
||||
second condition of its own to keep separate.
|
||||
|
||||
── three rather than four ─────────────────────────────────────────────────────────────────────────
|
||||
The keychain moved behind SETTINGS. A bottom bar is for the places a session moves between, and keys,
|
||||
credentials and tags are not that — they are managed occasionally and then left alone, which is the
|
||||
shape of everything else already behind the hub. What is left is the two halves of using this
|
||||
application, and the drawer holding the rest.
|
||||
-->
|
||||
<Border Grid.Row="3" IsVisible="{Binding IsShowingPages}"
|
||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" Height="64">
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
|
||||
<views:NavButton Grid.Column="0" Label="Hosts" Glyph="▤" IsCurrent="{Binding IsHostsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
@@ -261,31 +267,30 @@
|
||||
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
|
||||
through ShowScreen. Its own command is on the shell.
|
||||
|
||||
"Connections" rather than "Terminal", and the word does more work than a rename usually does:
|
||||
with nothing open this surface is where a connection is *made* — a machine typed in by hand, and
|
||||
the ones connected to before — so a tab labelled Terminal would name the thing behind the door
|
||||
rather than the door. The enum member stays ShellSurface.Terminal, for the reason the keychain's
|
||||
tab is not called Vault: the surface is a terminal, and the word a user reads is the product's.
|
||||
|
||||
The only entry here that never lights, and deliberately no longer tries: this bar is collapsed
|
||||
while the terminal is showing, so IsCurrent could only ever be read as false. Binding it anyway
|
||||
would be a rule about a state this control cannot be in. What marks the terminal as current is
|
||||
the surface filling the screen.
|
||||
would be a rule about a state this control cannot be in. What marks it as current is the surface
|
||||
filling the screen.
|
||||
-->
|
||||
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗"
|
||||
<views:NavButton Grid.Column="1" Label="Connections" Glyph="⌗"
|
||||
Command="{Binding ShowTerminalCommand}" />
|
||||
|
||||
<!--
|
||||
"Keychain", although the design's tab says Vault and the enum member is Vault. The split is a
|
||||
standing rule rather than an inconsistency: every sentence a user reads calls the encrypted
|
||||
store a keychain, and everything under the surface calls it a vault, because renaming the wire
|
||||
route, the tables and the CLR types would put an AAD resource type inside the blast radius of a
|
||||
find-and-replace. See docs/design-import-gaps.md. A tab reading Vault would be the one place the
|
||||
product surface leaked the implementation's word.
|
||||
-->
|
||||
<views:NavButton Grid.Column="2" Label="Keychain" Glyph="⚿" IsCurrent="{Binding IsVaultShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vault}" />
|
||||
IsMoreSurface rather than IsMoreShowing: this tab stands for seven screens, and a bar that went
|
||||
dark the moment you opened one of them would only ever light two of its three entries.
|
||||
|
||||
<!--
|
||||
IsMoreSurface rather than IsMoreShowing: this tab stands for six screens, and a bar that went
|
||||
dark the moment you opened one of them would only ever light three of its four entries.
|
||||
"Settings" and a gear, where it was "More" and a hamburger. The hub held five destinations and a
|
||||
paragraph and could not be called anything better than More; with the keychain and preferences
|
||||
both on it, it is the place everything that is not connecting to something lives — which is what
|
||||
a gear has meant on a phone since before this application existed.
|
||||
-->
|
||||
<views:NavButton Grid.Column="3" Label="More" Glyph="≣" IsCurrent="{Binding IsMoreSurface}"
|
||||
<views:NavButton Grid.Column="2" Label="Settings" Glyph="⚙" IsCurrent="{Binding IsMoreSurface}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||
|
||||
|
||||
@@ -347,12 +347,12 @@ internal sealed partial class PhoneShell : UserControl
|
||||
switch (current.Screen)
|
||||
{
|
||||
case ShellScreen.Snippets or ShellScreen.Logs or ShellScreen.Transfers
|
||||
or ShellScreen.Buckets or ShellScreen.Preferences:
|
||||
or ShellScreen.Buckets or ShellScreen.Preferences or ShellScreen.Vault:
|
||||
current.ShowScreenCommand.Execute(ShellScreen.More);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case ShellScreen.More or ShellScreen.Vault:
|
||||
case ShellScreen.More:
|
||||
current.ShowScreenCommand.Execute(ShellScreen.Hosts);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
@@ -144,8 +144,12 @@
|
||||
are a matched pair at either end of one bar — one leaves this surface, one adds to it — and an
|
||||
accented + would rank itself above the way out. The accent fill belongs to the floating + on
|
||||
HOSTS, which is the only action on its screen; this one is not.
|
||||
|
||||
Click as well as Command, and the handler does one thing the command cannot: put the software
|
||||
keyboard away. See OnConnectMenuRequested.
|
||||
-->
|
||||
<Button Grid.Column="2" Classes="icon" Content="+" Height="34" Margin="0,0,4,0"
|
||||
Click="OnConnectMenuRequested"
|
||||
Command="{Binding OpenConnectSheetCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -154,14 +158,107 @@
|
||||
<Panel Grid.Row="1">
|
||||
|
||||
<!--
|
||||
The empty state, and it says what the surface is for rather than that it is empty. A phone opens
|
||||
here on a cold start, so this is the first thing a new user reads.
|
||||
============ nothing open ============
|
||||
|
||||
Not an empty state any more, and that is what the bottom bar's word change is about: with no shell
|
||||
running this surface is where a connection is made rather than a page apologising for having
|
||||
nothing on it. Two things are offered, in the order somebody reaches for them.
|
||||
|
||||
A box, because the case this application had no answer for is the address somebody has just been
|
||||
given. Everything else here starts from a keychain item, which is right for a machine used twice and
|
||||
is several screens of work for one used once.
|
||||
|
||||
And the machines already connected to, under it, because that is the commoner half by far: the
|
||||
thing most likely to be wanted is the thing that was wanted yesterday. They come out of the vault's
|
||||
own connection log — see MainWindowViewModel.RecentConnections — so they survive a restart and
|
||||
arrive on a new phone with the keychain, which a list kept in this process would not.
|
||||
-->
|
||||
<StackPanel IsVisible="{Binding !HasTabs}" VerticalAlignment="Center" Margin="24" Spacing="10">
|
||||
<TextBlock Classes="title" FontSize="13" Text="NO SHELL OPEN" />
|
||||
<TextBlock Classes="body"
|
||||
Text="Press + above, or choose a host and press CONNECT. A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
||||
</StackPanel>
|
||||
<ScrollViewer IsVisible="{Binding !HasTabs}">
|
||||
<StackPanel Margin="20,18,20,24" Spacing="10">
|
||||
|
||||
<TextBlock Classes="title" FontSize="13" Text="CONNECT TO A MACHINE" />
|
||||
|
||||
<!--
|
||||
The manual target. One box in the grammar people already have — user@host, or user@host:port —
|
||||
rather than three, because three boxes on a phone is three taps between three keyboards for
|
||||
something that arrives as one string.
|
||||
-->
|
||||
<TextBox Classes="field" Text="{Binding Vault.ManualTarget}"
|
||||
PlaceholderText="user@host or user@host:port">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding Vault.ConnectManuallyCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
|
||||
<!--
|
||||
A typed password and nothing else. Offering the keychain's keys here would be a second binding
|
||||
resolution beside the one the connect path already has, and a key is the reason to save the
|
||||
machine as a host rather than dial it from a box.
|
||||
|
||||
Classes="secret" and not merely PasswordChar: the class is what also tells the software keyboard
|
||||
this is not prose, which is the difference between dots on screen and dots on screen with the
|
||||
password in the IME's dictionary. See Theme/Phone.axaml.
|
||||
-->
|
||||
<TextBox Classes="field secret" Text="{Binding Vault.ManualPassword}"
|
||||
PlaceholderText="password">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding Vault.ConnectManuallyCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
|
||||
<Button Classes="primary" Content="CONNECT" Command="{Binding Vault.ConnectManuallyCommand}" />
|
||||
|
||||
<!--
|
||||
Beside the box rather than on the shell's status line. What fails here is about what was typed —
|
||||
a missing account, a port that is not a number — and a sentence about a text box belongs under
|
||||
the text box.
|
||||
-->
|
||||
<TextBlock Classes="detail" TextWrapping="Wrap" Foreground="{StaticResource WarnText}"
|
||||
Text="{Binding Vault.ManualStatus}"
|
||||
IsVisible="{Binding Vault.ManualStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<TextBlock Classes="body"
|
||||
Text="Nothing typed here is saved. A machine worth keeping belongs on HOSTS, where it can carry a key, a group's defaults and a name." />
|
||||
|
||||
<!-- ============ where you have been ============ -->
|
||||
<StackPanel Spacing="2" Margin="0,14,0,0" IsVisible="{Binding HasRecentConnections}">
|
||||
|
||||
<TextBlock Classes="label" Text="RECENT" Margin="0,0,0,4" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding RecentConnections}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
|
||||
<!--
|
||||
Tapping one does not connect, and that is deliberate rather than a missing step. A row
|
||||
that names a keychain host goes to that host with its connect bar — which is where its
|
||||
key, or its password box, or its refusal lives — and one that names only an address goes
|
||||
back into the box above, without the password, which was never stored. Either way what
|
||||
happens next is visible before anything is dialled. See ConnectToRecentCommand.
|
||||
-->
|
||||
<Button Classes="row" MinHeight="52" Padding="12,8" CornerRadius="10"
|
||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).ConnectToRecentCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||||
Text="{Binding HostLabel}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Classes="detail" FontSize="10"
|
||||
VerticalAlignment="Center" Text="{Binding Started}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="body" Margin="0,10,0,0"
|
||||
Text="A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
A tab with no pane behind it: one still connecting, or one that was refused. The desktop head draws
|
||||
|
||||
@@ -2,11 +2,13 @@ using System.Text;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
|
||||
using DodoSSH.Client.Android.Platform;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
// Avalonia's Button, not Android.Widget's. .NET for Android puts Android.Widget in this project's
|
||||
@@ -85,6 +87,30 @@ internal sealed partial class TerminalScreen : UserControl
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the keyboard away on the way to the connect menu.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A handler beside the button's command rather than something the command does, because the command is
|
||||
/// on the shared view model and this is a platform call: the desktop head has no software keyboard to
|
||||
/// put away, and a shell view model that knew about Android's input-method manager would be the first
|
||||
/// thing in that layer that did.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The keyboard is up in the ordinary case.</b> This button sits above a terminal somebody is typing
|
||||
/// into, so the sheet it raises would otherwise arrive underneath a keyboard covering half the display —
|
||||
/// and the three rows on it are the bottom half. Worse, the keyboard's inset shortens everything the
|
||||
/// phone draws (see <c>PhoneShell.ApplyKeyboardInset</c>), so the sheet would be laid out into the
|
||||
/// remaining strip rather than merely hidden behind it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Click and Command both fire for one press and their order is not something to rely on. Neither reads
|
||||
/// what the other writes: this one talks to the platform and the command sets a flag, so they commute.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnConnectMenuRequested(object? sender, RoutedEventArgs e) => SoftKeyboard.Hide();
|
||||
|
||||
/// <summary>
|
||||
/// Takes the browser gestures back off a surface that is not a web page.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user