Public Access
Six defects, all found by running this head on an emulator against a local API rather than by reading it. They are in one commit because two of the files carry several of them and splitting those would mean hand-editing patches; each is argued at its own site. ── the keypad, which is most of this ─────────────────────────────────────────────────────────────────── It was eleven 30-by-44 keys in a horizontal ScrollViewer, and it was wrong twice over. A pan and a press begin with the same few pixels of travel, so a thumb coming down at an angle — which is how a thumb comes down — was read as the start of a pan and the press was never delivered; the row slid slightly instead. And 30 tall is well under the 48 Android asks for, so the presses that did arrive were hard to aim. The arrows, the keys pressed most and in sequence, were the ones parked past the right-hand edge. Termux, Termius, JuiceSSH, ConnectBot and Blink all ship this row and not one of them scrolls it: they divide the width evenly and add a second row when the set does not fit. That is not convention for its own sake — a key whose position is fixed can be hit without looking, which is the whole point of a modifier row used while reading the output above it. So: two UniformGrid rows, every key an equal share of the width, nothing scrollable, 40 tall. Every key kept. Arrows on the lower row where the thumb rests, reordered to ← ↓ ↑ → so the pairs that undo each other are neighbours. It costs about 51 pixels of terminal, four or five lines, and that is the right trade: the row that was cheaper did not work. The keys also gain a pressed state, which they had none of. They are Focusable="False" so that using one does not take the keyboard off the terminal, and a button that never takes focus never shows the adorner Fluent would otherwise give it — leaving no feedback at all on a control whose only job is to be pressed. Their looks move from assignments in the code-behind to Button.hotkey, which is what lets a style say :pressed, and the Ctrl latch becomes a class rather than two brushes written by hand. ── and five smaller ones ──────────────────────────────────────────────────────────────────────────────── The keyboard's inset was never cleared when the app was resumed without the keyboard it was applied for. Android takes the keyboard away as the activity stops and Avalonia's input pane raises no StateChanged, the pane going with the window rather than closing in front of one — so the interface came back holding a whole keyboard's height of empty canvas. 838 of 2340 pixels on a Pixel 5, on every screen, until something opened and closed the keyboard. MainActivity now raises PhoneEnvironment.Resumed and PhoneShell re-asks the pane. It asks rather than assuming zero: a permission prompt or the biometric sheet can hand the activity back with the keyboard genuinely still up, and flattening the inset there would strand the focused box under a real one. TextBlock.detail never wrapped, where .body beside it always has. A block that neither wraps nor trims lays out at its full single-line width and the parent clips it, with no ellipsis to say anything is missing — so the add sheet read "A machine to connect to, filed under a group if you" and stopped. Which of the fifty-odd .detail blocks overflows is a function of width and font scale rather than of the string, so this is a style rather than an attribute on the handful visibly wrong on one phone. The nine that genuinely want one line now say NoWrap beside their TextTrimming, which is the pairing that was always meant. The two port boxes are NumericUpDowns and were the only unthemed controls on this head — Fluent's border and fill, next to a TextBox.field sharing their row. App.axaml has styled NumericUpDown for the desktop all along; this head never had one. Three buttons classed "icon accent" were painting Fluent's own Button.accent, #6275AC, a blue in no palette here. The desktop overrides that style and this head did not, so the same class read correctly there and not here. The host key sheet said "First contact with 10.0.2.2 · 2222 . Nothing in this keychain…" — Avalonia puts a space between adjacent Runs and the last one began with the full stop. A gap before a full stop, on the one dialogue whose whole job is to be read carefully before it is answered. ── checks ────────────────────────────────────────────────────────────────────────────────────────────── manual-checks gains 10.4a and 11.10f. 10.4 already covered dismissing the keyboard with back or the chevron but not leaving the app with it up, which is the path the inset bug shipped through; 10.4a covers both directions, since a fix that flattened the inset on every resume would pass one half and fail the other. 11.10f guards the keypad against a ScrollViewer being put back around it.
305 lines
17 KiB
XML
305 lines
17 KiB
XML
<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}">
|
||
|
||
<!--
|
||
Design 04 — KEYCHAIN: everything in the vault that is not a host.
|
||
|
||
Two things from the design are structural rather than decorative and both are kept.
|
||
|
||
The category chips carry counts, because "SSH KEYS 8" answers a question the word alone does not.
|
||
And ◆ the delete confirmation replaces the row in place rather than stacking a dialogue over it — the
|
||
design is explicit about that, and on a phone it matters more than on the desktop: a modal over a list
|
||
hides which row it is talking about, which is exactly the information the confirmation exists to give.
|
||
|
||
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||
Rows became cards with a glyph tile, which is the design's own shape. Three things drawn on those cards
|
||
are not here, and all three for the same reason: there is nothing behind them. An `ED25519` badge needs
|
||
an algorithm field, and `SshKeySecret` has none — the type stores the armour verbatim rather than
|
||
parsing it. A `SHA256:…` line needs a fingerprint, computed by the same parsing. And an `agent` chip
|
||
needs an agent, of which this application has none at all — the connect path decrypts the key and hands
|
||
the bytes to the SSH library. What the card carries instead is `Detail`, which is what is genuinely
|
||
known *about* an item and never the secret. See docs/design-import-gaps.md.
|
||
|
||
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,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">
|
||
<!--
|
||
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
|
||
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
|
||
somewhere the layout has removed. See PhoneShell.IsWide.
|
||
-->
|
||
<Button Grid.Column="0" Classes="icon" Content="←"
|
||
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
|
||
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="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||
Margin="0,12,0,4">
|
||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="16,0">
|
||
|
||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsAll, Mode=OneWay}"
|
||
Command="{Binding ShowSectionCommand}" CommandParameter="{x:Static vm:VaultSection.All}">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Text="ALL" />
|
||
<TextBlock Text="{Binding TotalItemCount}" />
|
||
</StackPanel>
|
||
</RadioButton>
|
||
|
||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsKeys, Mode=OneWay}"
|
||
Command="{Binding ShowSectionCommand}" CommandParameter="{x:Static vm:VaultSection.Keys}">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Text="SSH KEYS" />
|
||
<TextBlock Text="{Binding Keys.Count}" />
|
||
</StackPanel>
|
||
</RadioButton>
|
||
|
||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsCredentials, Mode=OneWay}"
|
||
Command="{Binding ShowSectionCommand}"
|
||
CommandParameter="{x:Static vm:VaultSection.Credentials}">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Text="PASSWORDS" />
|
||
<TextBlock Text="{Binding Credentials.Count}" />
|
||
</StackPanel>
|
||
</RadioButton>
|
||
|
||
<!--
|
||
Tags. The one category holding nothing secret — a tag is a name — and here because a host's editor
|
||
is the only place on this head that makes one, which leaves nowhere else to see the whole set or
|
||
what wears it. Deleting one happens here, with every other item kind; renaming is desktop work,
|
||
because nothing on this screen opens an editor.
|
||
-->
|
||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsTags, Mode=OneWay}"
|
||
Command="{Binding ShowSectionCommand}"
|
||
CommandParameter="{x:Static vm:VaultSection.Tags}">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Text="TAGS" />
|
||
<TextBlock Text="{Binding Tags.Count}" />
|
||
</StackPanel>
|
||
</RadioButton>
|
||
|
||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsBuckets, Mode=OneWay}"
|
||
Command="{Binding ShowSectionCommand}"
|
||
CommandParameter="{x:Static vm:VaultSection.Buckets}">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Text="BUCKETS" />
|
||
<TextBlock Text="{Binding ObjectStores.Count}" />
|
||
</StackPanel>
|
||
</RadioButton>
|
||
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
<!--
|
||
The design's "4 keys · synced". SectionSummary counts the rows actually on screen and says how many of
|
||
them are still waiting to be pushed — which is the honest version of the design's unconditional
|
||
"synced".
|
||
-->
|
||
<TextBlock Grid.Row="2" Classes="detail" Margin="18,6,18,2" Text="{Binding SectionSummary}" />
|
||
|
||
<!-- ============ what to do with the chosen item ============ -->
|
||
<!--
|
||
DELETE and nothing beside it. The desktop's detail pane offers EDIT as well, and there is nothing here
|
||
for it to open — so this is the one action this head has for a keychain item, and until it existed the
|
||
confirmation below could not be reached at all.
|
||
|
||
It shares row 2 with that confirmation rather than being a bottom bar like the one on HOSTS or FILES,
|
||
because the question is what this button turns into: ShowsItemActions is false while a deletion is
|
||
armed, so the two swap in place and DELETE cannot be pressed again underneath its own question. A
|
||
button at the foot of the screen and its question at the top would be one exchange drawn twice.
|
||
|
||
And it names the item, because a keychain row is a card with a background of its own — see the selected
|
||
style below, which had nothing to colour until now. DELETE is the last button that should be pressed on
|
||
a guess about which row it means.
|
||
-->
|
||
<Border Grid.Row="2" IsVisible="{Binding ShowsItemActions}" Margin="12,4"
|
||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||
BorderThickness="1" CornerRadius="12" Padding="14,10">
|
||
<Grid ColumnDefinitions="*,10,Auto">
|
||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="label" Text="SELECTED" />
|
||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||
Text="{Binding SelectedVaultItem.Name}" TextTrimming="CharacterEllipsis" />
|
||
<Border Classes="tag outline">
|
||
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
|
||
</Border>
|
||
</StackPanel>
|
||
</StackPanel>
|
||
|
||
<!--
|
||
MOVE beside it, and only where there is somewhere to move to — the rule the host's MOVE follows on
|
||
this head, for the reason a phone has: there is no room to draw a button that answers with a
|
||
refusal. It is the ghost of the pair rather than the danger one, because a move is undone by
|
||
moving it back.
|
||
-->
|
||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
||
<Button Classes="secondary" Height="44" Width="86" Content="MOVE"
|
||
IsVisible="{Binding CanMoveSelectedItem}"
|
||
Command="{Binding MoveSelectedItemCommand}" />
|
||
<Button Classes="danger" Height="44" Width="104" Content="DELETE"
|
||
Command="{Binding DeleteSelectedItemCommand}" />
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!-- ============ ◆ the counted confirmation ============ -->
|
||
<!--
|
||
In place, above the list it refers to and over the button that asked it. The text comes from the view
|
||
model, which counts what breaks — "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="3" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
|
||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||
BorderThickness="1" CornerRadius="12" Padding="14,12">
|
||
<StackPanel Spacing="5">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding PendingDeletion.Question}" TextWrapping="Wrap" />
|
||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||
Text="{Binding PendingDeletion.Consequence}" />
|
||
<!-- The line worth reading twice: what depends on the item, counted. Absent when nothing does. -->
|
||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||
IsVisible="{Binding PendingDeletion.HasUsage}"
|
||
Text="{Binding PendingDeletion.Usage}" />
|
||
<Grid ColumnDefinitions="*,8,*" Margin="0,6,0,0">
|
||
<Button Grid.Column="0" Classes="danger" Content="DELETE"
|
||
Command="{Binding ConfirmDeleteCommand}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||
Command="{Binding CancelDeleteCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!-- ============ ◆ moving it to another vault ============ -->
|
||
<!--
|
||
The desktop's panel, in the place the deletion question uses and never at the same time as it: MOVE
|
||
disarms a pending deletion on the way in, and the buttons that ask either question are hidden while
|
||
one is up.
|
||
|
||
Both sentences are here rather than only in the status line afterwards, which on a phone is one line at
|
||
the bottom of a screen somebody has already navigated away from. The second one is the count of what
|
||
points at this key — every one of them is re-aimed at the vault it moves to, and that is the part
|
||
nobody can see from a keychain row.
|
||
-->
|
||
<Border Grid.Row="3" IsVisible="{Binding IsMovingItem}" Margin="12,4"
|
||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||
BorderThickness="1" CornerRadius="12" Padding="14,12">
|
||
<StackPanel Spacing="8">
|
||
<TextBlock Classes="label" Text="MOVE TO VAULT" />
|
||
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||
Text="{Binding MovingItemLabel}" />
|
||
<ComboBox HorizontalAlignment="Stretch" MinHeight="44"
|
||
ItemsSource="{Binding MoveItemVaultChoices}"
|
||
SelectedItem="{Binding SelectedMoveItemVault}">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Display}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
<TextBlock Classes="body" TextWrapping="Wrap"
|
||
Text="It is re-encrypted with the other vault's key, so everybody who holds that key can read it and nobody in the vault it leaves can." />
|
||
<TextBlock Classes="body" TextWrapping="Wrap"
|
||
IsVisible="{Binding HasMovingItemUsage}"
|
||
Text="{Binding MovingItemUsage}" />
|
||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||
<Button Grid.Column="0" Classes="primary" Height="44" Content="MOVE"
|
||
Command="{Binding ConfirmMoveItemCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelMoveItemCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!-- ============ the items ============ -->
|
||
<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"
|
||
VerticalAlignment="Top" Text="{Binding EmptySectionMessage}" />
|
||
|
||
<ListBox ItemsSource="{Binding VaultItems}" SelectedItem="{Binding SelectedVaultItem}"
|
||
Background="Transparent" BorderThickness="0" IsVisible="{Binding HasVaultItems}">
|
||
<ListBox.Styles>
|
||
<Style Selector="ListBoxItem">
|
||
<Setter Property="Padding" Value="0" />
|
||
<Setter Property="MinHeight" Value="0" />
|
||
</Style>
|
||
<!--
|
||
The fill goes on the card and not on the ContentPresenter behind it, which is where HOSTS and
|
||
FILES put theirs. Those rows are bare Borders; a keychain row is Border.card, which paints an
|
||
opaque Sidebar over anything drawn underneath — so the selector has to reach the card itself or
|
||
the chosen row looks exactly like the others. It has to look different now that a button above
|
||
the list acts on it.
|
||
-->
|
||
<Style Selector="ListBoxItem:selected Border.card">
|
||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||
</Style>
|
||
</ListBox.Styles>
|
||
<ListBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
||
<Border Classes="card" Margin="12,4">
|
||
<Grid ColumnDefinitions="Auto,*,Auto" MinHeight="64">
|
||
|
||
<!--
|
||
The glyph tile. One mark for all three kinds rather than three: a key, a password and a
|
||
bucket are already named by the badge beside them, and three near-identical monochrome
|
||
glyphs at 14px would be decoration that reads as a distinction.
|
||
-->
|
||
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="10"
|
||
Background="{StaticResource Active}" VerticalAlignment="Center">
|
||
<TextBlock Text="⚿" Foreground="{StaticResource AccentText}" FontSize="14"
|
||
FontWeight="SemiBold" HorizontalAlignment="Center"
|
||
VerticalAlignment="Center" />
|
||
</Border>
|
||
|
||
<StackPanel Grid.Column="1" Margin="12,0" Spacing="3" VerticalAlignment="Center">
|
||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||
TextTrimming="CharacterEllipsis" />
|
||
<Border Classes="tag outline">
|
||
<TextBlock Text="{Binding Type}" />
|
||
</Border>
|
||
<Border Classes="tag outline" BorderBrush="{StaticResource WarnSoft}"
|
||
IsVisible="{Binding HasBadge}">
|
||
<TextBlock Foreground="{StaticResource Warn}" Text="{Binding Badge}" />
|
||
</Border>
|
||
</StackPanel>
|
||
<!-- What is known *about* an item, never the secret. -->
|
||
<TextBlock Classes="detail" Text="{Binding Detail}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||
</StackPanel>
|
||
|
||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||
VerticalAlignment="Center" />
|
||
</Grid>
|
||
</Border>
|
||
</DataTemplate>
|
||
</ListBox.ItemTemplate>
|
||
</ListBox>
|
||
</Panel>
|
||
|
||
</Grid>
|
||
|
||
</UserControl>
|