05c56f20a42099a361298ff1eb6b9881d51a6579
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b931a06998 | Repaint the phone's chrome, radii and accent to the v5 vocabulary | ||
|
|
7343c717da |
Bring the last three outlined marks over to the filled tile
ServerScreen, LockedScreen and PendingScreen were the only places left drawing v1: a 44px square outlined in the accent with >_ in the accent inside it. Every other place the mark appears — both headers, the desktop titlebar, the launcher icon and the .ico — draws it filled. The radius is derived rather than chosen. The mark is 6 at 20 and 8 at 26, a third of a unit per unit of tile, which puts 44 at exactly 14 — and 14 is already on Phone.axaml's radius ladder as the one for a block of monospaced output, which is a fair description of a box with >_ in it. PendingScreen takes the shape and not the colour. It is the screen that says a surface is not built yet, and the accent is the shell's one "do this" colour; a tile in Raised with the glyph left at TextFaint keeps the mark recognisable while the screen goes on looking as inert as it is. |
||
|
|
416f233657 |
Let the lock screen wear the theme instead of its own copy of it
Four controls on this screen carried a full set of metrics each, and three of them were a shared class rewritten by hand — UNLOCK is Button.primary down to the font weight, the fingerprint button is Button.secondary, the passphrase box is TextBox.field. Copies drift, and every difference between these and the originals was drift rather than intent: corner radii of 6 from the first design where v2's ladder says 9 for a button and 11 for a field, a label half a point small, and the missing content alignment that put UNLOCK's word against the top of its box and started this. Two things fall out that are worth naming, because neither was the reported fault. UNLOCK now has the pressed and disabled states the class carries, and before it had neither — pressing it ran Fluent's default, which replaces the accent fill with a theme grey, so the one button this application most wants to look deliberate flashed the wrong colour under a thumb. And the passphrase box is 44 rather than 48, which is what every other box on the phone is; the four pixels bought it nothing that the button under it does not already give the thumb. RESET keeps two properties of its own against Button.danger, and they are the two that carry meaning. That class is a DELETE beside a KEEP, where the red outline says which of two offered actions costs something. Here there is no pair, and a bordered red button under UNLOCK reads as the next thing to try rather than as the last resort the screen means it to be. The live-sessions disclosure is not a class anywhere — warn cards are plain borders on all three screens that draw one — but its radius was 6 against the 12 the other two use, so it joins the ladder without joining a class. |
||
|
|
6fb82a111e |
Sit UNLOCK's word in the middle of the button it is drawn on
A button's content alignment defaults to Stretch and the Fluent template binds it straight through, so a text block filling a 48-pixel button draws its line along the top edge of it rather than across the middle. That is why all seven button classes in Phone.axaml set VerticalContentAlignment, and it is why this one did not: the lock screen is the only place on the phone whose buttons carry their own metrics rather than a class, and it was written with the horizontal half of the pair and not the vertical. Its two neighbours are unaffected and not for the same reason, which is worth knowing before this is read as a pattern: RESET has no fixed height and a symmetric vertical padding, and the fingerprint button's content is a panel that stretches and centres its own children. Only the one with a bare string in a fixed-height box shows it. Left as its own set of metrics rather than folded into Button.primary, which it otherwise duplicates down to the font weight. The two differ in corner radius — 6 here against the 9 the v2 ladder gives a button — and changing that is a design decision rather than a fix to the thing that was reported. |
||
|
|
35387b1c9d |
Tell the phone's keyboard these are secrets, and get it off the box
Five boxes on this head take a secret and every one of them was drawing dots and saying nothing. `PasswordChar` is a screen property: Windows has no opinion about what is being typed into a text box, so the desktop head needs nothing more. Android's software keyboard has an opinion, and left at its default it read a vault passphrase as prose — completions offered in the suggestion strip above the box, and the passphrase itself learned into the IME's dictionary. Dots on screen with a word bar over them is the worst of both: hidden from the person typing it and offered to the room. `TextInputOptions.ContentType` is the property the Android backend maps onto `InputType`, and it is what turns both off. Both attributes now live in one `TextBox.secret` class rather than being repeated per box, because they are two halves of one fact and the next box added would have got one of them. The keyboard also went on covering whichever box had raised it. That is in `PhoneShell` rather than on each screen: everything the phone draws is inside its one root panel, so a bottom margin shortens all eleven screens at once, and a screen added later cannot forget to handle something it never had to know about. Two mechanisms, and it matters that neither is a backstop for the other. Before Android 15 the activity now declares `AdjustResize` and the platform shortens the window itself; left unspecified Android chooses, and what it chooses for a window whose entire content is one native view — which is what an Avalonia surface is — is to pan, sliding the window by however much it thinks the focused native view needs and leaving the box exactly where it was. That was the bug. From Android 15 the attribute is ignored, edge-to-edge being enforced and the window no longer resized for the keyboard at all, and the reported inset is what there is. Each is dead where the other applies — where the window resizes, the inset arrives already consumed and measures zero — which is why the margin comes from the inset alone. Both added together would strand the interface an entire keyboard above the keyboard. Scrolling the box back into view keys off the size change rather than off either mechanism. `ScrollViewer` already brings a newly focused child into view; what it cannot know is that the visible region shrank after the focus, and both ways of losing that region end in the same resize. None of it is reachable by a test. The software keyboard is an inset the platform reports and a headless top level reports none, so phase 10 of `docs/manual-checks.md` is the whole of the verification — including the note to run it on one device each side of Android 15, since a build exercised on only one of the two will look correct and be half broken. |
||
|
|
5593f337b6 |
Give the phone the second design, and both heads the palette it arrives with
The Android v2 design is what this head draws now: four destinations in a bottom bar — Hosts, Terminal, Keychain, More — with snippets, SFTP, S3, logs and preferences one tap deeper behind the last. The first design's four had nothing behind them, which is what made a hub worth building. The palette moved from green-black to blue-black, and it moved in the shared project because that is where it lives and the desktop v2 specifies the same seventeen tokens. One colour changed meaning rather than value, and it is the only semantic change in the file. Green used to *be* the accent, so Ellipse.dot.live filled with Accent and "the thing to press" and "a shell is open on this host" were the same colour by construction. v2 makes the accent blue and keeps a green for status alone, which finally separates them: Live is that green and nothing merely interactive may use it. The accent is also two colours now — Accent fills, AccentText writes — because a row of chips in the fill colour is a row of things that all look like the primary action. A palette is not one file, which is the part worth knowing before the next one. Nine hex literals lived outside it: the nav bar's own label colours, the accessory keys and their Ctrl-latched state, two scrims, the window background Android paints before Avalonia has a frame, and the launcher vector. The two C# sites now resolve from the dictionary by name rather than restating it. The renderer's page cannot — it is served to a WebView over a loopback socket — so terminal.css and terminal.js keep hand-copied values and say so at both sites. ShellScreen gained More and Buckets, appended rather than slotted in. SFTP and S3 are one screen over one TransfersViewModel differing only in which picker they offer, and the kind is set by the button that navigates rather than on arrival — doing it in OnScreenChanged made every arrival at Transfers force the picker back to hosts, including the desktop's own rail arriving at a screen with a bucket already open. It refuses to change kind while a session is live, because there is one session behind both destinations and switching under it would title a screen S3 while it listed an SFTP host. What the design draws and this does not, on the usual grounds. The FORWARDING screen: nothing here forwards anything, so every toggle would be a control with no effect — it is a paragraph on the hub naming the absence, for the reason the desktop keeps TEAMS in its rail. The terminal's `23 ms · fwd 5432`. An ED25519 badge and a SHA256 line on keychain cards, which need an algorithm field and a fingerprint the item type does not have. An `agent` chip, for an agent that does not exist. Snippet run history and exit codes. The Logs FOLLOW pill, which claims a live tail over records that are written once at close and read when the screen opens, and the severity filter, which has nothing to count — that chip row is spent on the real choice, which of the two logs. S3 bucket totals and lifecycle. And the + on HOSTS, which would open a host editor this head has not got. SFTP is browse, open and delete. Both transfer commands work, and what they work against is the local pane: QueueDownloads writes to Path.Combine(LocalPath, name), and LocalPath starts at SpecialFolder.UserProfile, which on Android is the application's own private directory. A download would have reported success and left the file where the person who asked for it cannot open it, which is worse than not offering it — a refusal is visible and a file in /data/user/0/ is not. The queue is not drawn either, since nothing here can put anything in it. Both return with the document picker. The foreground service still counts zero transfers, and the reason moved rather than went away. Four defects worth naming, because three of them are the kind that compile. A Button as a ListBox ItemTemplate swallows the pointer press before the list sees it, so the files listing selected nothing and every command reading the selection did nothing — the row is a Border now and the phone-only single-tap-to-open is a Tapped handler, which also keeps a desktop single click from walking into directories. Avalonia type selectors are exact, so TextBlock.fingerprint never matched SelectableTextBlock and every fingerprint on this head rendered proportional and unwrapped: that was breaking the never-truncated rule on the host-key sheet already. The new two-level hierarchy had no handler for the system back gesture, so back left the application from a log screen. And the tab's close cross had shrunk to a 30x32 target flush against the select target, which is the one control here that ends a shell with no confirmation and no undo. Fingerprint unlock is raised on arriving at the lock screen rather than waiting for its button, which is still there. Only at launch: a lock the user asked for is not answered with an immediate request to unlock, which makes LOCK look inert and trains the reflex of authenticating at a prompt nobody asked for. And once, because a declined gesture leaves the passphrase box exactly where it was and a prompt that came back after being dismissed would be a modal you cannot get out of to type into it. Two fixes fall on the desktop. Its file listing coloured directories with Info and executables with Accent, which was blue against green and is now two steps of one blue; an executable is Live now. And a bucket's folders were drawn with a 0001-01-01 timestamp, because a prefix has no modification time — blank now, for the reason a directory's size is blank. Verified by the whole suite: 1309 tests over nineteen projects, none failing, including the layout suite that stands up real Avalonia and parses every desktop screen. Both heads build. Not verified on a device — nothing in this head ever has been; see docs/android-port.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZE3u99BNt6LzgTC5jhbz2 |
||
|
|
81e7e6d939 |
Write down what the phone found, and stop it rotting
docs/android-port.md was an audit of work not started; it now says what is built. Three of its statements needed correcting rather than extending, and they are marked where they sit: the Android version question is settled and was never as open as it looked, because Avalonia.Controls.WebView ships only a net10.0-android36.0 assembly and nothing lower can resolve it; cleartext to loopback has to be permitted explicitly, which the audit missed entirely; and the spike produced a structural change it did not anticipate, in DodoSSH.Client.Shell. A CI job of its own, because the head is deliberately not in DodoSSH.slnx and a project outside the solution is a project nobody notices breaking. It packages as well as builds: a native library with no Android ABI and an assembly that will not dex are both invisible to a compile, and both are exactly what this head is exposed to. The README says plainly that signing in is not built, that a fingerprint re-enrolment destroys the device key, that a notification appears while a shell is open, and that none of it has run on a device. |
||
|
|
fe9d7fc289 |
Give DodoSSH a phone, and a shared shell for both heads to drive
The Android head from docs/android-port.md, taken as far as its step 6. Step 3, the spike, is answered and its throwaway screen is gone: libsodium.so and libe_sqlite3.so are both in the arm64 APK, so NSec resolves its native half on Android despite shipping no Android build, and the local cache opens. Two findings the audit could not have had: Avalonia.Controls.WebView only ships net10.0-android36.0, which settles the open "which Android versions" question at targetSdk 36; and Android has blocked cleartext HTTP since API 28, so the terminal renderer needs a network security config scoped to 127.0.0.1 or the WebView loads nothing. DodoSSH.Client.Shell is new and is why the phone can exist: the view models, the terminal renderer files and the palette moved there so both heads drive one state machine and draw from one set of tokens. The desktop head is otherwise untouched and its 144 tests still pass. The platform pieces behind interfaces that already existed: the profile directory from filesDir, a device key wrapped by a StrongBox-backed key that a fingerprint releases, and a foreground service so a shell outliving a vault lock stays true on a platform that stops backgrounded processes. Sign-in is deliberately absent rather than approximated. It needs an app link, because reusing the desktop loopback listener is the attack RFC 8252 section 8.3 names. |