Public Access
Three destinations in a bar and everything else behind SETTINGS is the right shape at 360dp, where a fourth entry costs the width of the three that are there. On a tablet, an unfolded foldable or a landscape phone it is the wrong one: there is room for every destination at once, and the hub becomes an extra tap between somebody and a screen they can already see space for. So at 600dp — Android's own boundary between a compact window and a medium one, in the density-independent units Avalonia lays out in — the bar stands down and PhoneRail takes the left edge with all nine on it. It is the desktop's NavRail arrangement rather than its file: the two heads cannot share a view, and this one draws the phone's destination set with the phone's palette and touch targets. The flags are computed in code rather than assembled in the markup because none of them is a single question any more, and Avalonia's bindings have no "and" — and the header's condition is an "or", which not even a wrapper can express. That header is the one worth reading twice: narrow it stands down behind SETTINGS, so the hub's screens can draw their own; wide there is no hub to be behind, so it stays up everywhere. Losing it on the keychain would be losing the only LOCK button on the surface. Removing the hub means removing the routes into it, and there were four kinds. The rail has no SETTINGS entry, because that screen is a menu of the rail. The five back arrows in the screens under it are hidden, since an arrow to a screen the layout removed is the one control on a header that leads nowhere. The system back gesture goes to Hosts instead. And unfolding while sitting on the hub moves to Hosts, rather than leaving somebody on a list of things now visible beside it. One bug fixed on the way: OnBodyResized returned early unless the keyboard was open, so a foldable would have opened to a phone layout until somebody typed something. The chrome is refreshed first and unconditionally; the early return belongs to the older job below it. What this does not do is use the width *inside* a screen — the host list is one column at any size. Two columns needs the row model to change, because that list is headings and hosts in one sequence and a heading has to span, and that model is shared with the desktop. Check 8.1 walks the rail; nothing here is covered by a test, for the reason 8.0 exists.
180 lines
10 KiB
XML
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" 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" FontSize="10" Text="{Binding HostLabel}"
|
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" />
|
|
</StackPanel>
|
|
|
|
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
|
Text="{Binding Address}" 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}" 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" 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}" 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>
|