Files
DodoSSH/src/DodoSSH.Client.Android/Views/LogsScreen.axaml
T
jaap-jan 5b32c76c95
ci / build and test (push) Successful in 3m49s
ci / android head (push) Successful in 3m55s
ci / api image (push) Successful in 6s
ci / desktop nightly (push) Successful in 1m22s
Android: the redesign — a Vaults tab with hosts, keychain and snippets, card lists, a keychain that can add, and files that act on what is chosen
The bottom bar is Vaults · Connections · Settings, drawn as Material 3 pills with icons from the embedded
Material font and a count of open sessions on Connections, which replaces the strip of shells above the
bar. Vaults is the open vault: its header carries the name as a switch to the vaults screen, the sync
light and a lock icon, and a Hosts · Keychain · Snippets segmented control sits under it. The keychain
and snippets move there from Settings (IsVaultSurface lights the tab; IsMoreSurface no longer lists
them), and back from either goes to the hosts. Settings becomes an account card and grouped sections:
security, vaults and history, files, this app.

Host rows are cards with a terminal tile and the live dot on its corner. The connect sheet names the
host and its address, labels its box, focuses it as it opens, reveals the password on request, and puts
CANCEL left and CONNECT right; every two-button row on the head now confirms on the right. While a
hosts-screen sheet is up the header and bar are dimmed too and a tap on them lowers it, and the bar
stands down while the software keyboard is up.

The keychain gains a floating + and a sheet of the desktop's five adds (generate a key, paste a key, a
password, a bucket, a tag) over the editors VaultViewModel already had, drawn as a full-screen page with
labels above the boxes; a chosen item gets EDIT and COPY PUBLIC KEY beside MOVE and DELETE. Snippets get
a floating + and cards that say RUNS or INSERTS. Files trade the seven-button block for a selection bar
(save, and a ⋮ sheet with rename, move, permissions and a red delete), folder and file tiles, an upload
button, and header actions for a terminal on the same host, refresh and close; a terminal's + sheet
opens SFTP on that terminal's own host.

The theme takes the audit's visual findings: Fluent's ClipToBounds is cleared on glowing buttons so the
glow is no longer cut to a square (the desktop's Button.accent too), prose is Montserrat with mono kept
for addresses and code, disabled secondary buttons are quieter than enabled ones, Fluent's dropdown,
checkbox, switch, focus and selection brushes come from the palette, touch no longer leaves hover fills
behind, and lists, nav entries and icon buttons carry names for TalkBack.

Builds clean; checked on the emulator end to end: unlock, the three vault views, adding and deleting a
password, connecting through the new sheet, files on the terminal's host with the selection bar and
its sheet. App tests 727 of 733 pass with 5 skipped; the one failure, TheCountsAPhoneShows_
FollowEveryForwardsState, passes three runs of three alone and is untouched here. Layout tests pass.
2026-10-04 15:50:09 +02:00

180 lines
10 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.LogsScreen"
x:DataType="vm:LogsViewModel"
Background="{StaticResource Canvas}">
<!--
Design 07 — LOGS, under MORE.
Three things on the mock-up are not drawn, and each is the same kind of omission.
◆ **FOLLOW.** The design's header carries a blinking green "FOLLOW" pill and the screen is described as
a live tail over ssh. Nothing here tails anything: these are the keychain's own synced audit records,
written once at close, and they are read when the screen is opened. A pill that blinked would be
animation standing in for a feature. It is a REFRESH button, which is what actually happens.
**The severity filter** — ALL / INFO / WARN / ERROR with counts — has nothing to count. A connection
record is not a syslog line and carries no level. What this screen genuinely has two of is *logs*, so
the design's chip row is spent on the real choice: what was connected to, and what was changed.
**The host chips** are gone for the same reason: nothing filters either log by host.
What is kept is the design's row shape — a quiet timestamp, a coloured word, a source, and the fact
underneath — because that shape is why a log is readable at 10pt on a phone.
-->
<Grid RowDefinitions="Auto,Auto,Auto,*">
<!-- ============ header ============ -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,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="Logs" Margin="4,0" />
<Button Grid.Column="2" Classes="icon" Content="↻" Command="{Binding RefreshCommand}"
IsEnabled="{Binding !IsBusy}" />
</Grid>
<!-- ============ which log ============ -->
<!--
Mode=OneWay is load-bearing on both of these, not a tidiness: a two-way selection binding moves the
chip before the command it is paired with has had a chance to refuse, so the screen would show a
section it is not on. The keychain's categories carry the same note.
-->
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="6" Margin="14,0,14,4">
<RadioButton GroupName="log" Classes="chip" IsChecked="{Binding ShowsConnections, Mode=OneWay}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Connections}"
Content="CONNECTIONS" />
<RadioButton GroupName="log" Classes="chip" IsChecked="{Binding ShowsActivity, Mode=OneWay}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Activity}"
Content="KEYCHAIN" />
</StackPanel>
<TextBlock Grid.Row="2" Classes="detail" Margin="18,4,18,6" TextWrapping="Wrap"
Text="{Binding Status}" />
<!-- ============ the log ============ -->
<Border Grid.Row="3" Classes="output" Margin="12,0,12,12">
<Panel>
<!-- ============ what was connected to ============ -->
<!--
One wrapper per section, each holding its own empty state, because "this section is empty" is a
pair of facts — which section, and whether it has rows — and a binding cannot say `ShowsConnections
&& !HasConnections` without a converter. Two Panels are cheaper than a converter and they keep
EmptyMessage, which is already written per section, pointing at the section it describes.
-->
<Panel IsVisible="{Binding ShowsConnections}">
<TextBlock Classes="body" VerticalAlignment="Top" Text="{Binding EmptyMessage}"
IsVisible="{Binding !HasConnections}" />
<ScrollViewer IsVisible="{Binding HasConnections}">
<ItemsControl ItemsSource="{Binding Connections}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
<StackPanel Spacing="1" Margin="0,5">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Classes="detail code" FontSize="10" Text="{Binding Started}"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<!--
The kind sits where the design puts a severity, because it is the word that tells you
what the row is: a shell or a file transfer. Green while the session is still open —
the same green the host list uses, and the one thing on this screen that is about now
rather than about the past.
-->
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold" Text="{Binding Kind}"
Foreground="{StaticResource Info}" VerticalAlignment="Center"
IsVisible="{Binding !IsLive}" />
<StackPanel Orientation="Horizontal" Spacing="5" IsVisible="{Binding IsLive}"
VerticalAlignment="Center">
<Ellipse Classes="dot live" Width="6" Height="6" VerticalAlignment="Center" />
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
Text="{Binding Kind}" Foreground="{StaticResource Live}" />
</StackPanel>
<TextBlock Classes="detail code" FontSize="10" Text="{Binding HostLabel}"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
Text="{Binding Address}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
<!--
TextDim rather than the `detail` class's own faint step. This block is drawn on
TerminalSurface, which is lighter than the Canvas that step was chosen against, so the
same colour that reads as quiet on a screen background reads as barely there here.
-->
<StackPanel Orientation="Horizontal" Spacing="7">
<!-- "still open" rather than a dash, which the plan calls out by name. -->
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
Text="{Binding Duration}" />
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
Text="·" />
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
Text="{Binding DeviceName}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- A refused host key is the one outcome worth a colour of its own. -->
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasOutcome}"
Foreground="{StaticResource DangerText}" Text="{Binding Outcome}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Panel>
<!-- ============ what was changed ============ -->
<Panel IsVisible="{Binding ShowsActivity}">
<TextBlock Classes="body" VerticalAlignment="Top" Text="{Binding EmptyMessage}"
IsVisible="{Binding !HasActivity}" />
<ScrollViewer IsVisible="{Binding HasActivity}">
<ItemsControl ItemsSource="{Binding Activity}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
<StackPanel Spacing="1" Margin="0,5">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Classes="detail code" FontSize="10" Text="{Binding At}"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
Text="{Binding Operation}" Foreground="{StaticResource Info}"
VerticalAlignment="Center" />
<TextBlock Classes="detail" FontSize="10" Text="{Binding ItemKind}"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
Text="{Binding ItemLabel}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
<!-- The names of the fields that changed, never their values. -->
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasChangedFields}"
Foreground="{StaticResource TextDim}"
Text="{Binding ChangedFields}" TextWrapping="Wrap" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Panel>
</Panel>
</Border>
</Grid>
</UserControl>