Public Access
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.
241 lines
14 KiB
XML
241 lines
14 KiB
XML
<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.Android.Views.PhoneRail"
|
||
x:DataType="vm:MainWindowViewModel">
|
||
|
||
<!--
|
||
◆ The destinations down the left edge, on a surface too wide to be a phone.
|
||
|
||
This is the desktop head's NavRail arrangement rather than a second design: a column of destinations at
|
||
the left, each a glyph and a word, the current one lit. What it is not is that head's *file* — the two
|
||
heads cannot share a view, and this one draws the phone's destination set with the phone's palette and
|
||
the phone's touch targets.
|
||
|
||
── WHY IT EXISTS AT ALL ────────────────────────────────────────────────────────────────────────────────
|
||
The bottom bar carries three entries and everything else lives one tap deeper behind SETTINGS. That is
|
||
the right shape at 360dp, where a fourth entry costs the width of the three that are there. It is the
|
||
wrong shape on a tablet or an unfolded foldable, where there is room for every destination at once and
|
||
the hub becomes an extra tap between the user and a screen they can already see space for.
|
||
|
||
So above the breakpoint the bar stands down and this takes its place, with all nine destinations that
|
||
were split between the bar and the hub. SETTINGS is not among them: it is a list of the things on this
|
||
rail, and a rail entry leading to a menu of the rail would be furniture.
|
||
|
||
── WHAT IT SHOWS THAT THE HUB DID NOT ──────────────────────────────────────────────────────────────────
|
||
A count beside Hosts and Keychain, which is what the desktop rail's extra width buys there too. Only
|
||
where a count is real: Logs has no total until it is read and Preferences is not a quantity, so those
|
||
show nothing rather than a zero — a number the application never computed is worse than a blank.
|
||
|
||
── SFTP AND S3 ARE HERE, WHERE THE DESKTOP PUTS THEM IN ITS TAB STRIP ──────────────────────────────────
|
||
That head has fixed tabs for the two destinations that are not about the keychain; this one has no tab
|
||
strip, and its hub already listed them beside the rest. Putting them anywhere else would be inventing a
|
||
third arrangement for one screen's worth of difference.
|
||
|
||
── STYLES ARE LOCAL, COLOURS ARE NOT ───────────────────────────────────────────────────────────────────
|
||
The .railentry styles live in this file because they are this control's own shape and nothing else wears
|
||
them; every colour in them is resolved from Theme/Palette.axaml by key, which is the rule that matters —
|
||
a rail that named its own blues is how a fifth surface colour ends up in the application.
|
||
|
||
── DEEP CHROME, SINCE v5 ───────────────────────────────────────────────────────────────────────────────
|
||
This rail's own surface moves from Sidebar to DeepChrome in this pass, matching the desktop's NavRail —
|
||
which is exactly what this control is standing in for on a wide phone, and which took the same v5b move
|
||
itself: Palette.axaml's own remark on DeepChrome names "the rail down the side" as one of the five things
|
||
that colour is for. Sidebar stays what a card sits on; this is the frame the cards sit inside, the same
|
||
distinction PhoneShell's header, strip and bottom bar are drawing at the same time.
|
||
|
||
The hover fill moves too, from Panel to Track — and Track rather than Hover, because Palette.axaml's own
|
||
remark on Track names "the rail row a pointer sits over" as one of the three things that colour is for,
|
||
literally. Panel was never named for this; it read close enough that nobody had gone looking for the
|
||
seam, which is exactly the kind of gap Track exists to close.
|
||
-->
|
||
|
||
<UserControl.Styles>
|
||
<Style Selector="Button.railentry">
|
||
<Setter Property="Background" Value="Transparent" />
|
||
<Setter Property="BorderThickness" Value="0" />
|
||
<Setter Property="CornerRadius" Value="10" />
|
||
<Setter Property="Padding" Value="12,0" />
|
||
|
||
<!-- 48, not the bar's 64: a rail entry is a row rather than a target at the screen's edge, and nine
|
||
of them at 64 would not fit the shortest surface this layout is drawn on. Still above the 44
|
||
every other touch target here is held to. -->
|
||
<Setter Property="MinHeight" Value="48" />
|
||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||
</Style>
|
||
|
||
<Style Selector="Button.railentry:pointerover /template/ ContentPresenter">
|
||
<Setter Property="Background" Value="{StaticResource Track}" />
|
||
</Style>
|
||
|
||
<Style Selector="Button.railentry.active /template/ ContentPresenter">
|
||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||
</Style>
|
||
|
||
<!--
|
||
◆ Material icons rather than Unicode glyphs, as the desktop's own rail draws them. The glyphs came from
|
||
whatever symbol font the phone happened to have, at whatever weight and optical size it drew them — the
|
||
heavy ❯ beside the hairline ◳ the 2026-10-02 audit pointed at (M7) — and two of them meant two things:
|
||
⚙ was both the Settings tab and Preferences, ⚿ both the keychain and locking it. The icon font is the
|
||
one the desktop embeds, so one destination now has one mark on both heads.
|
||
-->
|
||
<Style Selector="Button.railentry TextBlock.railglyph">
|
||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||
<Setter Property="FontFamily" Value="{StaticResource IconFont}" />
|
||
<Setter Property="FontSize" Value="19" />
|
||
<!-- A ligature codepoint is noise to a screen reader; the entry's name is on the button. -->
|
||
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
|
||
<Setter Property="Width" Value="24" />
|
||
<Setter Property="VerticalAlignment" Value="Center" />
|
||
</Style>
|
||
|
||
<!-- A destination's name is a word rather than an identifier, so it takes the sans face — see the remark
|
||
on TextBlock.body in Theme/Phone.axaml. -->
|
||
<Style Selector="Button.railentry TextBlock.raillabel">
|
||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||
<Setter Property="FontSize" Value="13.5" />
|
||
<Setter Property="VerticalAlignment" Value="Center" />
|
||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||
</Style>
|
||
|
||
<Style Selector="Button.railentry TextBlock.railcount">
|
||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||
<Setter Property="FontSize" Value="10.5" />
|
||
<Setter Property="VerticalAlignment" Value="Center" />
|
||
</Style>
|
||
|
||
<!-- Three signals for the current entry rather than one, which is the rule NavButton already states for
|
||
the bar it replaces: colour alone at this size is what a user with a colour deficiency is left
|
||
with. Here it is the filled row, the accent, and the weight. -->
|
||
<Style Selector="Button.railentry.active TextBlock.railglyph">
|
||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||
</Style>
|
||
|
||
<Style Selector="Button.railentry.active TextBlock.raillabel">
|
||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||
<Setter Property="FontWeight" Value="SemiBold" />
|
||
</Style>
|
||
</UserControl.Styles>
|
||
|
||
<Border Width="188" Background="{StaticResource DeepChrome}"
|
||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
||
|
||
<!--
|
||
DockPanel with LastChildFill false, exactly as the desktop rail does it: Preferences is pinned to the
|
||
foot because it is the one entry about the machine rather than about the keychain, and everything else
|
||
flows from the top. A ScrollViewer over the top group so that a short surface — a foldable's cover
|
||
display, a small window in Android's freeform mode — scrolls the list rather than clipping it.
|
||
-->
|
||
<DockPanel LastChildFill="False">
|
||
|
||
<StackPanel DockPanel.Dock="Bottom" Margin="8,0,8,10">
|
||
<Button Classes="railentry" AutomationProperties.Name="Preferences" Classes.active="{Binding IsPreferencesShowing}"
|
||
Command="{Binding ShowScreenCommand}"
|
||
CommandParameter="{x:Static vm:ShellScreen.Preferences}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Preferences" />
|
||
</Grid>
|
||
</Button>
|
||
</StackPanel>
|
||
|
||
<ScrollViewer DockPanel.Dock="Top" VerticalScrollBarVisibility="Auto"
|
||
HorizontalScrollBarVisibility="Disabled">
|
||
<StackPanel Margin="8,10,8,0" Spacing="2">
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="Hosts" Classes.active="{Binding IsHostsShowing}"
|
||
Command="{Binding ShowScreenCommand}"
|
||
CommandParameter="{x:Static vm:ShellScreen.Hosts}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Hosts" />
|
||
<TextBlock Grid.Column="2" Classes="railcount" Text="{Binding Vault.Hosts.Count}" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<!--
|
||
The terminal is a surface rather than a page, so it goes through its own command — the same
|
||
reason the bottom bar's middle entry does. It lights on IsTerminalSurface, which on this control
|
||
can only mean one thing: the rail stands down for a shell, so the one moment it is drawn beside
|
||
this surface is Connections with nothing running. That screen is a page in everything but which
|
||
enum it is in, and a rail sitting beside it with no entry lit would be pointing nowhere.
|
||
|
||
No count, unlike the two above it. The shells strip sits above the bottom of every page on this
|
||
surface and is that count, drawn as the sessions themselves — a number beside this word would be
|
||
the same fact said twice, three inches apart.
|
||
-->
|
||
<Button Classes="railentry" AutomationProperties.Name="Connections" Classes.active="{Binding IsTerminalSurface}"
|
||
Command="{Binding ShowTerminalCommand}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Connections" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<Border Height="1" Background="{StaticResource Border}" Margin="4,8" />
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="Keychain" Classes.active="{Binding IsKeychainShowing}"
|
||
Command="{Binding ShowScreenCommand}"
|
||
CommandParameter="{x:Static vm:ShellScreen.Keychain}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Keychain" />
|
||
<TextBlock Grid.Column="2" Classes="railcount" Text="{Binding Vault.Keys.Count}" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="Snippets" Classes.active="{Binding IsSnippetsShowing}"
|
||
Command="{Binding ShowScreenCommand}"
|
||
CommandParameter="{x:Static vm:ShellScreen.Snippets}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Snippets" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="SFTP" Classes.active="{Binding IsTransfersShowing}"
|
||
Command="{Binding ShowFilesCommand}"
|
||
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="SFTP" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="S3" Classes.active="{Binding IsBucketsShowing}"
|
||
Command="{Binding ShowFilesCommand}"
|
||
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="S3" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="Logs" Classes.active="{Binding IsLogsShowing}"
|
||
Command="{Binding ShowScreenCommand}"
|
||
CommandParameter="{x:Static vm:ShellScreen.Logs}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Logs" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
<Button Classes="railentry" AutomationProperties.Name="Vaults" Classes.active="{Binding IsVaultsShowing}"
|
||
Command="{Binding ShowScreenCommand}"
|
||
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Vaults" />
|
||
</Grid>
|
||
</Button>
|
||
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
</DockPanel>
|
||
</Border>
|
||
|
||
</UserControl>
|