Files
DodoSSH/src/DodoSSH.Client.Android/Views/KeychainScreen.axaml
T
jaap-jan 5800a4bbd0 Give the keychain the DELETE its confirmation was already written for
The counted confirmation has been on this screen since the phone got its
screens, and nothing could raise it. PendingDeletion is armed by
DeleteSelectedItemCommand and its siblings, and the only Delete bindings
anywhere on this head were the files screen's remote delete and the snippets
one — so the card, and the header comment calling it structural, described a
question the phone could not ask. The button is what was missing rather than
anything behind it: the command, the counting and the wording were all already
shared with the desktop.

It sits in row 2 beside that confirmation rather than in a bar at the foot of
the screen, which is where HOSTS and FILES put what to do with the chosen thing.
ShowsItemActions is SelectedItemIsEditable and not-already-asking, so sharing
the row makes the question take the place of the button that asked it, and
DELETE cannot be pressed a second time underneath its own confirmation. A button
at the bottom and its question at the top would be one exchange drawn twice.

DELETE alone, without the EDIT the desktop's detail pane offers beside it,
because there is nothing on this head for EDIT to open. That is also why the
bar names the item rather than trusting the highlight: it is the only thing on
screen saying which row is about to go.

And the highlight had to be made to exist. HOSTS and FILES fill the selected
ContentPresenter, which works because their rows are bare Borders; a keychain
row is Border.card, and the card paints an opaque Sidebar over it. The selector
reaches the card itself instead, in the same Active the chosen host wears, so
the row a button now acts on looks different from the ones it does not.

The TAGS comment said renaming and deleting happened here. Half of that is true
as of this commit and the other half is not — a tag is still renamed on the
desktop, because no kind of item opens an editor on this screen.

The README's paragraph about what the phone is missing said there was no host
editor, which stopped being true when the floating + and both editors landed. It
now names what is actually absent: no local pane for file transfer, no keychain
editor, and no screen at all for pins, teams or import.
2026-08-03 14:54:19 +02:00

228 lines
13 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
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.
-->
<Grid RowDefinitions="Auto,Auto,Auto,*">
<!-- ============ categories ============ -->
<ScrollViewer Grid.Row="0" 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="1" 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>
<Button Grid.Column="2" Classes="danger" Height="44" Width="104" Content="DELETE"
Command="{Binding DeleteSelectedItemCommand}" />
</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="2" 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>
<!-- ============ the items ============ -->
<Panel Grid.Row="3">
<!-- 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}" TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Grid.Column="2" Text="" Foreground="{StaticResource TextGhost}" FontSize="15"
VerticalAlignment="Center" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Panel>
</Grid>
</UserControl>