Public Access
Give the phone the rest of its screens, and a way in
All seven screens of the design, plus the two it does not draw because it starts at an enrolled phone: naming a server, and choosing a passphrase. The five states docs/android-port.md worried about losing at 360dp are all here and none of them softened. The changed-key refusal is a full-screen panel rather than a bottom sheet, because a sheet is swipe-to-dismiss by convention and that screen must have no way forward. The recovery code raises FLAG_SECURE for its own state and lowers it afterwards, so the sentence about screenshots is true rather than decorative. The delete confirmations keep their counts and replace the row in place. Signing in works, and the seam it needed is worth more than the implementation: IAuthorizationCallback now sits between OidcClient and the loopback listener, so the two heads differ in where the response arrives and in nothing else. PKCE, the state check, discovery, the token exchange and the key binding stay one implementation — a second OIDC client would be a second place for a security bug to live. The phone registers a private-use scheme with the system rather than binding a loopback port, which on a shared device any other app can do first. The accessory key row needed TerminalWorkspace.SendInputAsync: ordinary typing goes from the renderer straight down the socket, and there was no way in for the keys a software keyboard does not have. Ctrl latches, because one thumb cannot chord, and the latch is drawn — a modifier that is on and does not look on is how somebody sends ^L to a database prompt believing they typed an l. 597 client tests green, including two new ones for the input path and one for the terminal surface command. Nothing has run on a device.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<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.TerminalScreen"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Background="{StaticResource Canvas}">
|
||||
|
||||
<!--
|
||||
Design 03 — TERMINAL.
|
||||
|
||||
One WebView for every session, exactly as on the desktop and for the same reason: each session is a pane
|
||||
inside the one page, so switching tabs is a single frame rather than a second browser. On a phone the
|
||||
argument is stronger — a WebView per tab would be several hundred megabytes on a device that will
|
||||
reclaim them.
|
||||
|
||||
The tab strip is horizontal-scrolling rather than wrapping. Wrapping would reflow the terminal every
|
||||
time a tab opened, which is the one thing a terminal must not do while output is arriving.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<!-- ============ tabs ============ -->
|
||||
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel Orientation="Horizontal" Height="44">
|
||||
<ItemsControl ItemsSource="{Binding Tabs}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||
<Grid ColumnDefinitions="Auto,Auto">
|
||||
<Button Grid.Column="0" Classes="row" MinHeight="44" Padding="12,0"
|
||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<Ellipse Classes="dot live" Width="5" Height="5" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<!--
|
||||
The close cross is inside the tab, which the plan calls out: a strip-level close would
|
||||
act on whichever tab happened to be selected, and on a phone that is a mis-tap away from
|
||||
killing the wrong shell.
|
||||
-->
|
||||
<Button Grid.Column="1" Classes="row" MinHeight="44" Width="34" Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="×" Foreground="{StaticResource TextFaint}" FontSize="14" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ============ the connection line ============ -->
|
||||
<Border Grid.Row="1" Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderSubtle}"
|
||||
BorderThickness="0,0,0,1" Padding="14,5"
|
||||
IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<TextBlock Classes="detail" FontSize="9.5" TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding SelectedTab.Address}" />
|
||||
</Border>
|
||||
|
||||
<!-- ============ the renderer ============ -->
|
||||
<Panel Grid.Row="2">
|
||||
|
||||
<!--
|
||||
The empty state, and it says what the surface is for rather than that it is empty. A phone opens
|
||||
here on a cold start, so this is the first thing a new user reads.
|
||||
-->
|
||||
<StackPanel IsVisible="{Binding !HasTabs}" VerticalAlignment="Center" Margin="24" Spacing="10">
|
||||
<TextBlock Classes="title" FontSize="13" Text="NO SHELL OPEN" />
|
||||
<TextBlock Classes="body"
|
||||
Text="Choose a host and press CONNECT. A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Collapsed rather than merely covered when there are no tabs. On Windows this control is a native
|
||||
child window that composites above everything Avalonia draws, which is why the desktop head hides
|
||||
it explicitly; whether Android's WebView does the same is recorded as unverified in
|
||||
docs/android-port.md. Hiding it either way costs nothing and is correct under both answers.
|
||||
-->
|
||||
<NativeWebView x:Name="Renderer" IsVisible="{Binding HasTabs, FallbackValue=False}" />
|
||||
</Panel>
|
||||
|
||||
<!-- ============ accessory keys ============ -->
|
||||
<!--
|
||||
The row every Android SSH client ships, and the reason is not preference: a software keyboard has no
|
||||
Ctrl, no Esc, no Tab and no arrows, and without them a phone cannot interrupt a process, complete a
|
||||
path, or reach the previous command. Ctrl and Alt latch — pressed once they apply to the next key and
|
||||
then release, because holding a modifier while typing is not possible one-thumbed.
|
||||
-->
|
||||
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" IsVisible="{Binding HasTabs}">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Height="44" Spacing="4"
|
||||
Margin="6,5" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
Reference in New Issue
Block a user