Merge branch 'main' into the Android head

Main grew the screens the host-management plan called for — hosts, pins, snippets, logs,
import, teams — plus the ObjectStore and Import projects behind two of them, and moved
WindowsDeviceKeyStore into the desktop head's Platform folder.

Five of those view models landed in a directory this branch had already moved, so they
join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the
namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android
head then gets transitively and will use neither of at first — scoped storage means there
is no ~/.ssh/config to import, and file transfer is out of its first scope.

Desktop suites green at 155 and 64.
This commit is contained in:
2026-07-31 21:03:22 +02:00
199 changed files with 31294 additions and 775 deletions
@@ -0,0 +1,154 @@
<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.App.Views.LogsScreen"
x:DataType="vm:LogsViewModel">
<!--
What has been connected to, and what has been changed.
Two logs behind one screen, chosen by two buttons rather than by a selector's selection — the same idiom
the keychain screen's categories use, and for the same reason: a selection binding moves before a command
can refuse it.
Both are ordinary synced keychain items, encrypted like everything else. The server holds them and cannot
read a single field; what it does learn is that rows exist and when they were written, which ADR 0001
records as the metadata this design cannot hide.
The connections list shows anything still open at the top, marked "still open" rather than with a dash. A
dash would read as a missing recording, and the two are opposite facts — an entry is written once, when a
connection closes, so a live session is deliberately not in the vault yet.
-->
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="14,0" Height="44"
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0" Classes="mono" Text="LOGS" FontSize="11" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
Margin="0,0,14,0" />
<Button Grid.Column="1" Classes="flat cat" Content="CONNECTIONS"
Classes.active="{Binding ShowsConnections}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Connections}" />
<Button Grid.Column="2" Classes="flat cat" Content="KEYCHAIN"
Classes.active="{Binding ShowsActivity}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Activity}" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Status}" FontSize="9.5"
Foreground="{StaticResource TextFaint}" Margin="14,0,0,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<Button Grid.Column="4" Classes="ghost" Content="REFRESH" Command="{Binding RefreshCommand}"
IsEnabled="{Binding !IsBusy}" />
</Grid>
</Border>
<!-- ============ Connections ============ -->
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding ShowsConnections}">
<Grid Grid.Row="0" ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="14,6,14,6"
IsVisible="{Binding HasConnections}">
<TextBlock Grid.Column="0" Classes="label" Text="HOST" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="1" Classes="label" Text="ADDRESS" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="2" Classes="label" Text="LASTED" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="3" Classes="label" Text="KIND" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="4" Classes="label" Text="STARTED" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="5" Classes="label" Text="FROM" FontSize="8.5" LetterSpacing="1" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConnectionList" Focusable="True"
ItemsSource="{Binding Connections}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
<Grid ColumnDefinitions="1.2*,1.6*,88,72,90,*" Margin="0,6,14,6">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" Margin="14,0,8,0">
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" VerticalAlignment="Center" />
<TextBlock Classes="mono" Text="{Binding HostLabel}" FontSize="11" FontWeight="Medium"
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Address}" FontSize="9.5"
Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding Duration}" FontSize="9.5"
Foreground="{StaticResource TextDim}" />
</StackPanel>
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Kind}" FontSize="9"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Started}" FontSize="9"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding DeviceName}" FontSize="9"
Foreground="{StaticResource TextFaint}"
TextTrimming="CharacterEllipsis" />
<!--
Only when there is something to say. A connection that opened and closed says nothing
here; one that was refused says so, and that is the row worth finding in a long list.
-->
<Border Classes="chip warn" Padding="4,0" IsVisible="{Binding HasOutcome}">
<TextBlock Text="{Binding Outcome}" FontSize="8.5" />
</Border>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="11"
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="420"
IsVisible="{Binding !HasConnections}" />
</Grid>
<!-- ============ Keychain changes ============ -->
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding ShowsActivity}">
<Grid Grid.Row="0" ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6"
IsVisible="{Binding HasActivity}">
<TextBlock Grid.Column="0" Classes="label" Text="ITEM" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="1" Classes="label" Text="TYPE" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="2" Classes="label" Text="WHAT" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="3" Classes="label" Text="FIELDS" FontSize="8.5" LetterSpacing="1" />
<TextBlock Grid.Column="4" Classes="label" Text="WHEN" FontSize="8.5" LetterSpacing="1" />
</Grid>
<ListBox Grid.Row="1" x:Name="ActivityList" Focusable="True" ItemsSource="{Binding Activity}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
<Grid ColumnDefinitions="1.2*,90,96,*,90" Margin="14,6,14,6">
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding ItemLabel}" FontSize="11"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ItemKind}" FontSize="9"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Operation}" FontSize="9.5"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<!--
The names of the fields that changed, and never what they changed to. A log that recorded
an old password would be a plaintext credential store with a vault drawn around it.
-->
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding ChangedFields}" FontSize="9.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
IsVisible="{Binding HasChangedFields}" />
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding At}" FontSize="9"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="11"
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="420"
IsVisible="{Binding !HasActivity}" />
</Grid>
</Grid>
</UserControl>