Public Access
311 lines
18 KiB
XML
311 lines
18 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.PhoneShell"
|
|
x:DataType="vm:MainWindowViewModel"
|
|
Background="{StaticResource Canvas}">
|
|
|
|
<!--
|
|
The phone's single view, and the counterpart of the desktop head's MainWindow — except that this one has
|
|
no window, no nav rail, no titlebar and no status bar.
|
|
|
|
It switches on ShellState, which is the same six states the desktop has because both heads share one
|
|
state machine. What differs is only what each one draws.
|
|
|
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
The desktop's eight rail destinations become four in a bottom bar, and the rest live one tap deeper
|
|
behind MORE: snippets, SFTP, S3, logs, preferences — and teams, which v2 did not draw and which is
|
|
argued for on the screen itself. That is the v2 design's own arrangement, and it replaces the first
|
|
design's four, which had nothing behind them at all. Five characters was a desktop constraint and the
|
|
phone uses words.
|
|
|
|
The order is the design's rather than the rail's. Terminal sits second, beside Hosts, because those two
|
|
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
|
|
|
|
── a terminal gets the screen ─────────────────────────────────────────────────────────────────────────
|
|
Three of the four rows below stand down while a shell is showing: the header, the shells strip and the
|
|
bottom bar itself. All three are bound on IsShowingPages, which is the same question asked once — the
|
|
surface is either a page or a terminal, and these are the chrome a page has.
|
|
|
|
The arithmetic is why. Header 56, strip 46, bar 64, and the terminal's own two rows on top of that: at
|
|
360dp the shell was framed by about a third of the display, all of it about somewhere the user was not.
|
|
What takes their place is one 35-pixel bar drawn by the surface itself, carrying back on the left and
|
|
the sessions and a + across from it. See TerminalScreen.axaml.
|
|
-->
|
|
|
|
<!--
|
|
Named, and the name is load-bearing: everything the phone draws is inside this one element, so its
|
|
bottom margin is the single place the software keyboard can be kept off the box being typed into,
|
|
whichever of the eleven screens is showing. See PhoneShell.axaml.cs.
|
|
-->
|
|
<Panel x:Name="Body">
|
|
|
|
<!-- ============ getting in ============ -->
|
|
<views:PendingScreen IsVisible="{Binding IsStarting}"
|
|
Heading="OPENING THE KEYCHAIN"
|
|
Detail="Reading this phone's local cache to find out whether it is enrolled." />
|
|
|
|
<views:ServerScreen IsVisible="{Binding IsNeedingServer}" />
|
|
<views:EnrollScreen IsVisible="{Binding IsNeedingEnrollment}" />
|
|
<views:RecoveryCodeScreen IsVisible="{Binding IsShowingRecoveryCode}" />
|
|
<views:LockedScreen IsVisible="{Binding IsLocked}" />
|
|
|
|
<!-- ============ unlocked ============ -->
|
|
<Grid IsVisible="{Binding IsUnlocked}" RowDefinitions="Auto,*,Auto,Auto">
|
|
|
|
<!--
|
|
The header: which vault, and whether it is synced.
|
|
|
|
Hidden behind MORE, and that is the design's arrangement rather than a saving. v2 gives every screen
|
|
one header carrying that screen's own name and its own actions — a back arrow, an add, a refresh —
|
|
so the hub's screens draw their own and this one stands down rather than stacking a second row of
|
|
chrome above theirs. It stays on the two destinations that are the product's top level, where the
|
|
vault's name and the sync light are the most useful thing a header could say.
|
|
|
|
Wrapped rather than given a second condition, because Avalonia's bindings have no "and": the wrapper
|
|
collapses it over a terminal, where the surface draws its own bar and the vault's name is not what
|
|
the user is looking at. That is one of three rows this Grid stands down while a shell is showing —
|
|
see the strip and the bottom bar below.
|
|
-->
|
|
<Panel Grid.Row="0" IsVisible="{Binding IsShowingPages}">
|
|
<Border Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
|
BorderThickness="0,0,0,1" Padding="14,0" Height="56"
|
|
IsVisible="{Binding !IsMoreSurface}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
|
|
|
<!--
|
|
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
|
|
fact about the vault, and the design gives it the accent as a solid tile — which is also what
|
|
the launcher icon draws, so the two agree.
|
|
-->
|
|
<Border Grid.Column="0" Width="26" Height="26" CornerRadius="8"
|
|
Background="{StaticResource Accent}" VerticalAlignment="Center">
|
|
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}"
|
|
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="Bold"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
|
|
<TextBlock Grid.Column="1" Classes="heading" Margin="10,0,8,0" FontSize="16"
|
|
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
|
|
|
<!--
|
|
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws
|
|
a permanently green "Synced" here, which is the one claim on that mock-up this application
|
|
will not make.
|
|
-->
|
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
|
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="6" Height="6"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
|
|
</StackPanel>
|
|
|
|
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
|
ToolTip.Tip="Lock the keychain">
|
|
<TextBlock Text="LOCK" Classes="label" FontSize="8.5"
|
|
Foreground="{StaticResource TextDim}" />
|
|
</Button>
|
|
</Grid>
|
|
</Border>
|
|
</Panel>
|
|
|
|
<!-- The screens. Only one draws; which one is the shell's business. -->
|
|
<Panel Grid.Row="1">
|
|
<!--
|
|
Wrapped rather than bound directly, and the desktop head's MainWindow carries the same note for
|
|
the same reason: this element's visibility is the shell's business and its data context is the
|
|
vault, and putting both on one element resolves IsVisible against the vault — where
|
|
IsHostsShowing does not exist. It is a compile error here rather than a silent blank screen only
|
|
because compiled bindings are on.
|
|
-->
|
|
<Panel IsVisible="{Binding IsHostsShowing}">
|
|
<views:HostsScreen DataContext="{Binding Vault}" />
|
|
</Panel>
|
|
<Panel IsVisible="{Binding IsVaultShowing}">
|
|
<views:KeychainScreen DataContext="{Binding Vault}" />
|
|
</Panel>
|
|
<!--
|
|
IsTerminalSurface rather than IsTerminalShowing: a tab exists from the moment connecting starts and
|
|
its session does not, so this screen has to be on show while there is nothing to render — it is
|
|
what says which machine is being connected to. Which of the two things inside it draws is decided
|
|
in the screen itself. See TerminalTabViewModel.
|
|
-->
|
|
<views:TerminalScreen IsVisible="{Binding IsTerminalSurface}" />
|
|
|
|
<!--
|
|
============ under MORE ============
|
|
|
|
The hub itself takes the shell as its data context, because every row on it is a navigation
|
|
command; the destinations behind it each take the view model they are about, so each one is
|
|
wrapped. SnippetsScreen and LogsScreen are nullable on the shell — they are rebuilt on every
|
|
unlock and nulled on lock — and it is the collapsed wrapper that keeps a template from binding
|
|
against nothing.
|
|
-->
|
|
<views:MoreScreen IsVisible="{Binding IsMoreShowing}" />
|
|
|
|
<Panel IsVisible="{Binding IsSnippetsShowing}">
|
|
<views:SnippetsScreen DataContext="{Binding SnippetsScreen}" />
|
|
</Panel>
|
|
|
|
<Panel IsVisible="{Binding IsLogsShowing}">
|
|
<views:LogsScreen DataContext="{Binding LogsScreen}" />
|
|
</Panel>
|
|
|
|
<!--
|
|
The sixth destination behind MORE, and the one v2 never drew — see the comment on the screen
|
|
itself. Wrapped like its neighbours even though Teams is not nullable: the reason for the wrapper
|
|
is the data context, not the null. IsTeamShowing is the shell's and Teams is not the shell.
|
|
-->
|
|
<Panel IsVisible="{Binding IsTeamShowing}">
|
|
<views:TeamsScreen DataContext="{Binding Teams}" />
|
|
</Panel>
|
|
|
|
<!--
|
|
One screen for both file destinations. SFTP and S3 differ in which picker they offer and in
|
|
nothing else below it — the panes, the queue and the transfers are the same IRemoteFileStore
|
|
either way — so drawing them twice would be two copies of a screen that has to stay identical.
|
|
Which picker shows is TransfersViewModel.Remote, set on arrival by the shell.
|
|
-->
|
|
<Panel IsVisible="{Binding IsTransfersShowing}">
|
|
<views:FilesScreen DataContext="{Binding Transfers}" />
|
|
</Panel>
|
|
<Panel IsVisible="{Binding IsBucketsShowing}">
|
|
<views:FilesScreen DataContext="{Binding Transfers}" />
|
|
</Panel>
|
|
|
|
<!--
|
|
Preferences keeps PendingScreen for its body — it is still a screen that states what is absent
|
|
rather than one with settings on it — and gains the header its siblings under MORE have, here
|
|
rather than inside PendingScreen: that control is shared with three states of getting in, none of
|
|
which has anywhere to go back to.
|
|
-->
|
|
<Grid IsVisible="{Binding IsPreferencesShowing}" RowDefinitions="Auto,*">
|
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*" Height="56" Margin="8,0">
|
|
<Button Grid.Column="0" Classes="icon" Content="←" Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
|
<TextBlock Grid.Column="1" Classes="heading" Text="Preferences" Margin="4,0" />
|
|
</Grid>
|
|
<views:PendingScreen Grid.Row="1"
|
|
Heading="PREFERENCES"
|
|
Detail="Registering this phone's device key, and signing out, are on the unlock screen for now. Importing an ~/.ssh/config has no meaning here — scoped storage means there is no such file to read." />
|
|
</Grid>
|
|
</Panel>
|
|
|
|
<!--
|
|
◆ The two host-key decisions, over everything. Outside the screen Panel deliberately: file transfer
|
|
makes its own trust decision on the desktop, and the phone now has that screen — so a sheet nested
|
|
inside the hosts screen would be one the files screen could not raise.
|
|
-->
|
|
<Panel Grid.Row="1">
|
|
<views:HostKeySheet DataContext="{Binding Vault}" />
|
|
</Panel>
|
|
|
|
<!--
|
|
The shells strip. The design puts it above the nav on every screen, and that is the single most
|
|
desktop-shaped idea in the product — it is what makes a terminal a surface you switch to rather than
|
|
a screen you navigate away from. Kept, because losing it would make an open shell invisible from the
|
|
host list, which is where you are when you open the next one.
|
|
|
|
v2 draws the sessions as pills rather than as a labelled row, and drops the word SHELLS: with a
|
|
rounded chip carrying a live dot and a name, the label was spending nine characters of a 360dp row
|
|
saying what the row already looks like.
|
|
|
|
On every screen except the one it names. The terminal draws these same sessions in its own bar, and
|
|
two rows of the same pills — one of them 46 pixels of it — is the arrangement this surface exists to
|
|
stop. Wrapped rather than given a second condition, because the strip's own visibility is about
|
|
whether there are any tabs and this one is about which surface is up.
|
|
-->
|
|
<Panel Grid.Row="2" IsVisible="{Binding IsShowingPages}">
|
|
<Border IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
|
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
|
<Button Classes="row" MinHeight="34" Padding="13,0" CornerRadius="9"
|
|
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
|
BorderThickness="1"
|
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
|
CommandParameter="{Binding}">
|
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
|
<!--
|
|
Green only while there is a shell behind the tab. It used to be lit unconditionally,
|
|
which was true when a tab could not exist without a session; one can now —
|
|
connecting opens the tab first — and a dot that was green before anything had
|
|
answered would be the one thing on this strip claiming something untrue.
|
|
-->
|
|
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
|
</StackPanel>
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</ScrollViewer>
|
|
</Border>
|
|
</Panel>
|
|
|
|
<!-- ============ navigation ============ -->
|
|
<!--
|
|
Gone while a terminal is showing, which is the whole of that surface's arrangement: the bar's four
|
|
destinations are replaced by a back arrow and a + that leads to three of them, both in the terminal's
|
|
own bar. See TerminalScreen.axaml.
|
|
|
|
This one is bound directly rather than wrapped — its visibility is a single question and it has no
|
|
second condition of its own to keep separate.
|
|
-->
|
|
<Border Grid.Row="3" IsVisible="{Binding IsShowingPages}"
|
|
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
|
BorderThickness="0,1,0,0" Height="64">
|
|
<Grid ColumnDefinitions="*,*,*,*">
|
|
|
|
<views:NavButton Grid.Column="0" Label="Hosts" Glyph="▤" IsCurrent="{Binding IsHostsShowing}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
|
|
|
|
<!--
|
|
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
|
|
through ShowScreen. Its own command is on the shell.
|
|
|
|
The only entry here that never lights, and deliberately no longer tries: this bar is collapsed
|
|
while the terminal is showing, so IsCurrent could only ever be read as false. Binding it anyway
|
|
would be a rule about a state this control cannot be in. What marks the terminal as current is
|
|
the surface filling the screen.
|
|
-->
|
|
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗"
|
|
Command="{Binding ShowTerminalCommand}" />
|
|
|
|
<!--
|
|
"Keychain", although the design's tab says Vault and the enum member is Vault. The split is a
|
|
standing rule rather than an inconsistency: every sentence a user reads calls the encrypted
|
|
store a keychain, and everything under the surface calls it a vault, because renaming the wire
|
|
route, the tables and the CLR types would put an AAD resource type inside the blast radius of a
|
|
find-and-replace. See docs/design-import-gaps.md. A tab reading Vault would be the one place the
|
|
product surface leaked the implementation's word.
|
|
-->
|
|
<views:NavButton Grid.Column="2" Label="Keychain" Glyph="⚿" IsCurrent="{Binding IsVaultShowing}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Vault}" />
|
|
|
|
<!--
|
|
IsMoreSurface rather than IsMoreShowing: this tab stands for the hub and everything behind it,
|
|
and a bar that went dark the moment you opened one of them would only ever light three of its
|
|
four entries.
|
|
-->
|
|
<views:NavButton Grid.Column="3" Label="More" Glyph="≣" IsCurrent="{Binding IsMoreSurface}"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
|
|
|
</Grid>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</Panel>
|
|
|
|
</UserControl>
|