Files
DodoSSH/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml
T
jaap-jan 5b32c76c95
ci / build and test (push) Successful in 3m49s
ci / android head (push) Successful in 3m55s
ci / api image (push) Successful in 6s
ci / desktop nightly (push) Successful in 1m22s
Android: the redesign — a Vaults tab with hosts, keychain and snippets, card lists, a keychain that can add, and files that act on what is chosen
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.
2026-10-04 15:50:09 +02:00

177 lines
10 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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.PreferencesScreen"
x:DataType="vm:MainWindowViewModel"
Background="{StaticResource Canvas}">
<!--
Design v2 — PREFERENCES, and the phone's counterpart of the desktop head's screen of the same name.
It exists for one setting: whether this phone may open the keychain with a fingerprint. That is the
setting the whole Android device key store was built for — AndroidDeviceKeyStore and BiometricGate have
been in this head since it was written — and until this screen there was nowhere to turn it on. The
lock screen offers UNLOCK WITH FINGERPRINT only when a device key exists, and nothing on the phone could
make one, so on a phone that button could never appear. This is the missing half.
Registering has to happen from an unlocked keychain and cannot happen from the lock screen, which is why
it is here rather than beside the button it enables: sealing this phone's device key needs the vault
open, and the wrap has to reach the server or it could never be revoked from a phone that was lost. See
MainWindowViewModel.RegisterDeviceAsync.
Everything the desktop's screen carries and this one does not is stated at the foot rather than left
out, exactly as the desktop states its own gaps. A preferences screen that is silent about what it has
not got reads as a product with one preference.
-->
<ScrollViewer>
<StackPanel Margin="16,0,16,24" Spacing="0">
<TextBlock Classes="section" Text="THIS PHONE" Margin="6,4,0,0" />
<!--
The offer and the withdrawal share this card and never show together: one is "there is no device key
here", the other is "there is". Neither appears on a phone that cannot hold one at all — a handset
with no screen lock — and that third case is the paragraph under them, because a card with nothing
in it would read as a setting that failed to load.
-->
<Border Classes="card" Margin="0,10,0,0">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="9">
<!-- The lock screen draws the same ring beside its own fingerprint button. -->
<Ellipse Width="16" Height="16" Stroke="{StaticResource Accent}" StrokeThickness="1.5"
VerticalAlignment="Center" />
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Unlock with a fingerprint" />
</StackPanel>
<TextBlock Classes="body"
Text="Registers this phone so a later launch can open the keychain with your fingerprint — or this phone's PIN — instead of your passphrase. The key is held by the phone's secure hardware and never leaves it, and your passphrase keeps working." />
<Button Classes="primary" Content="REGISTER THIS PHONE"
Command="{Binding RegisterDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanRegisterDevice}" />
<!--
No confirmation, deliberately, and the desktop's button says the same in a tooltip this head has
no room for — so the sentence above it carries it instead. Somebody pressing this has realised a
phone is in the wrong hands, and the cost of a wrong press is one passphrase.
-->
<StackPanel Spacing="8" IsVisible="{Binding CanForgetDevice}">
<TextBlock Classes="detail" TextWrapping="Wrap"
Text="registered · this phone can open the keychain without your passphrase" />
<Button Classes="danger" Content="STOP UNLOCKING HERE"
Command="{Binding ForgetDeviceCommand}"
IsEnabled="{Binding !IsBusy}" />
</StackPanel>
<!--
Neither flag is set on a phone with no screen lock, and that is worth saying: Android will
generate a key that requires authentication on such a phone and then no gesture can ever release
it, which is why AndroidDeviceKeyStore reports itself unavailable rather than offering one.
-->
<TextBlock Classes="body" IsVisible="{Binding HasNoDeviceKeyOption}"
Text="This phone has nowhere to keep a device key, so the keychain will keep asking for your passphrase. That needs a screen lock with a fingerprint or a PIN enrolled on the phone itself." />
</StackPanel>
</Border>
<!-- Where registering, withdrawing and a declined prompt all answer. -->
<TextBlock Classes="body" Margin="6,12,6,0" TextAlignment="Center"
Text="{Binding StatusMessage}" />
<!-- ============ ◆ updates ============ -->
<!--
The phone's copy of the desktop's UPDATES section, over the same view model. What differs is the
last step and only the last step: pressing INSTALL hands the package to Android's own installer
rather than swapping files, so the platform draws a confirmation this application does not control
and the answer may be no. Everything before that — the timer, the quiet background pass, the loud
pressed one, the fetch — is shared. See AndroidUpdateChannel and ADR 0014.
The version is a fact, printed, in the shape ACCOUNT uses on the desktop. It comes from the
assembly's own informational version rather than from the updater, so it answers on a build run
from a checkout too; see ClientVersion.Current.
The sentence about where builds come from is ADR 0011 rule 2 in plain words, and it is on the head
the rule was written for: no DodoSSH server will ever offer you the client, and one that does is not
one to trust.
-->
<TextBlock Classes="section" Text="UPDATES" Margin="6,26,0,0" />
<Border Classes="card" Margin="0,10,0,0">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Classes="label" Text="VERSION" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" FontSize="13" Margin="10,0,0,0"
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis"
Text="{Binding Updates.CurrentVersion}" />
</Grid>
<StackPanel Spacing="10" IsVisible="{Binding Updates.IsSupported}">
<CheckBox MinHeight="44" IsChecked="{Binding Updates.IsAutomatic}">
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
Text="Look for newer builds on their own" />
</CheckBox>
<TextBlock Classes="body"
Text="Checks come from the project's own release page and never from the server you sign in to. Nothing is installed without you saying so, and Android asks again in its own words before anything is replaced." />
<!--
The bar, copying the desktop's and the transfers screen's. Shown while fetching and not
before: a bar at zero beside a button nobody has pressed is a screen that looks busy.
-->
<ProgressBar IsVisible="{Binding Updates.IsDownloading}" Height="4"
Minimum="0" Maximum="100" Value="{Binding Updates.DownloadPercent}" />
<Button Classes="secondary" Height="44" Content="CHECK NOW"
Command="{Binding Updates.CheckNowCommand}"
IsEnabled="{Binding Updates.CanCheckNow}" />
<!--
INSTALL rather than the desktop's RESTART NOW, and the word is the honest one: this does not
restart anything. It asks Android to install, and Android asks the user. What follows is the
system's screen, not this one.
-->
<Button Classes="primary" Height="44" Content="INSTALL"
IsVisible="{Binding Updates.IsReady}"
Command="{Binding Updates.RestartNowCommand}" />
<TextBlock Classes="body" IsVisible="{Binding Updates.IsReady}"
Text="{Binding Updates.RestartWarning}" />
<TextBlock Classes="detail" TextWrapping="Wrap" Text="{Binding Updates.Status}"
IsVisible="{Binding Updates.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
</StackPanel>
<!--
The build that cannot replace itself, which on this head is one an IDE deployed or one built
from a checkout — see AndroidUpdateChannels.ForThisPhone. Said rather than shown as dead
controls, following HasNoDeviceKeyOption above.
-->
<TextBlock Classes="body" IsVisible="{Binding Updates.IsUnsupported}"
Text="This build cannot replace itself — it was not installed from the project's release page. A newer DodoSSH is an APK from there, installed the way a released one would be. No DodoSSH server will ever offer you one, whatever it says; a server that does is not a server to trust." />
</StackPanel>
</Border>
<TextBlock Classes="section" Text="NOT HERE" Margin="6,26,0,0" />
<Border Classes="card" Margin="0,10,0,0">
<StackPanel Spacing="9">
<TextBlock Classes="body"
Text="Text size is on the terminal itself, where the text is — the phone puts A− and A+ in the shell's own bar rather than a step away from what they resize." />
<TextBlock Classes="body"
Text="Importing an ~/.ssh/config has no meaning on Android: scoped storage means there is no such file for this application to read." />
<TextBlock Classes="body"
Text="Importing a Hetzner Cloud project would work here — it is a token and a network request, not a file — and the wizard behind it is already shared. What is missing is a phone layout for it: the desktop draws a five-column table, and the screen it would become is not the desktop's with narrower columns. Until then it is a thing to do on a desktop; the hosts it makes sync here like any other." />
<TextBlock Classes="body"
Text="Signing out is on the lock screen, where somebody who has forgotten their passphrase can reach it without unlocking anything first." />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>