Public Access
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
This commit is contained in:
@@ -268,10 +268,21 @@ the reasoning behind it. Sign in, unlock, browse hosts, open a shell, and read t
|
|||||||
host-key decisions and the counted delete confirmations are there too, and none of them were softened to
|
host-key decisions and the counted delete confirmations are there too, and none of them were softened to
|
||||||
fit 360dp.
|
fit 360dp.
|
||||||
|
|
||||||
What it does **not** have is file transfer — deliberately, since scoped storage means there is no local
|
Its interface is the **v2 design**: four destinations in a bottom bar — Hosts, Terminal, Vault and More —
|
||||||
pane to put beside the remote one — and the four list screens the desktop grew last (pins, snippets, logs,
|
with snippets, SFTP, S3 buckets, logs and preferences one tap deeper behind the last. That is also where
|
||||||
teams), whose view models are already shared and which are additive rather than structural. Importing an
|
the product's palette now comes from, and both heads share it, so the desktop is blue too.
|
||||||
`~/.ssh/config` has no meaning on a phone at all.
|
|
||||||
|
File transfer **is** here now, in the shape scoped storage allows: one remote pane and the queue, over
|
||||||
|
either an SFTP host or a bucket. There is no local pane, because there is no browsable local filesystem to
|
||||||
|
put in one — moving a file *in* from the phone needs the system document picker and is the next piece of
|
||||||
|
work rather than a thing the screen pretends to do. What is still absent is a host editor and a keychain
|
||||||
|
item editor, so hosts and keys are created on the desktop and sync down; pins, teams and import have no
|
||||||
|
phone screen either. Importing an `~/.ssh/config` has no meaning on a phone at all.
|
||||||
|
|
||||||
|
**Port forwarding is not built anywhere**, and the phone's More screen says so in a paragraph rather than
|
||||||
|
leaving a gap. The v2 design draws a whole screen for it; nothing in the SSH layer forwards anything, so
|
||||||
|
every control on that screen would have had no effect. See
|
||||||
|
[`docs/design-import-gaps.md`](docs/design-import-gaps.md).
|
||||||
|
|
||||||
It is deliberately **not** in `DodoSSH.slnx`. Putting it there would make the `android` workload and a full
|
It is deliberately **not** in `DodoSSH.slnx`. Putting it there would make the `android` workload and a full
|
||||||
Android SDK a prerequisite of `dotnet build DodoSSH.slnx` for everybody; it has its own CI job instead, which
|
Android SDK a prerequisite of `dotnet build DodoSSH.slnx` for everybody; it has its own CI job instead, which
|
||||||
|
|||||||
+30
-3
@@ -439,6 +439,15 @@ go at 360dp:
|
|||||||
`setInvalidatedByBiometricEnrollment` is on, which is what stops somebody who can add a fingerprint to an
|
`setInvalidatedByBiometricEnrollment` is on, which is what stops somebody who can add a fingerprint to an
|
||||||
unlocked phone from inheriting the vault — the cost being that re-enrolment permanently destroys the key,
|
unlocked phone from inheriting the vault — the cost being that re-enrolment permanently destroys the key,
|
||||||
which the load path treats as ordinary rather than exceptional.
|
which the load path treats as ordinary rather than exceptional.
|
||||||
|
|
||||||
|
**The prompt is raised on arrival at the lock screen** rather than waiting for the button, which is still
|
||||||
|
there and still says what it does. Two rules shape it, both in `PhoneShell.TryOfferDeviceUnlock`. It
|
||||||
|
happens **only at launch**: a lock the user asked for is not answered with a request to unlock, because
|
||||||
|
that makes LOCK look inert and trains the reflex of authenticating at a prompt nobody asked for. And it
|
||||||
|
happens **once**: a declined gesture leaves the passphrase box exactly where it was, which is the whole
|
||||||
|
fallback, and a prompt that came back after being dismissed would be a modal you cannot get out of to
|
||||||
|
type into it. It watches three properties rather than one because startup sets the state to `Locked`
|
||||||
|
before it has asked the keystore whether there is a key to offer, and does both inside the busy wrapper.
|
||||||
5. ~~**Android sign-in.**~~ **Done**, and the seam it needed turned out to be worth more than the
|
5. ~~**Android sign-in.**~~ **Done**, and the seam it needed turned out to be worth more than the
|
||||||
implementation. `IAuthorizationCallback` now sits between `OidcClient` and the loopback listener, so 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,
|
two heads differ in *where the response arrives* and in nothing else — PKCE, the state check, discovery,
|
||||||
@@ -477,6 +486,14 @@ go at 360dp:
|
|||||||
The nav rail's eight destinations became four. Pins, snippets, logs, import and teams are not built here:
|
The nav rail's eight destinations became four. Pins, snippets, logs, import and teams are not built here:
|
||||||
import has no meaning under scoped storage, and the other four are list screens whose view models already
|
import has no meaning under scoped storage, and the other four are list screens whose view models already
|
||||||
exist — they are additive rather than structural.
|
exist — they are additive rather than structural.
|
||||||
|
|
||||||
|
**Superseded by v2.** A second design — *DodoSSH Android v2* — is what this head now draws, and it took
|
||||||
|
the "additive rather than structural" claim at its word: snippets, logs, SFTP and S3 are built, over the
|
||||||
|
view models that were already shared, behind a MORE hub that holds the five destinations the bottom bar
|
||||||
|
has no room for. `ShellScreen` gained `More` and `Buckets`. The palette went blue and the two heads moved
|
||||||
|
together, because it is shared. What v2 asked for and did not get is in
|
||||||
|
[`docs/design-import-gaps.md`](design-import-gaps.md) — the short version being that port forwarding is
|
||||||
|
named as absent rather than drawn, because nothing in the SSH layer forwards anything.
|
||||||
8. ~~**The terminal.**~~ **Done** — one `NativeWebView` for every session, as on the desktop and for the
|
8. ~~**The terminal.**~~ **Done** — one `NativeWebView` for every session, as on the desktop and for the
|
||||||
same reason, with the tab strip scrolling horizontally rather than wrapping so that opening a tab never
|
same reason, with the tab strip scrolling horizontally rather than wrapping so that opening a tab never
|
||||||
reflows a terminal while output is arriving.
|
reflows a terminal while output is arriving.
|
||||||
@@ -487,9 +504,19 @@ go at 360dp:
|
|||||||
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
||||||
believing they typed an `l`.
|
believing they typed an `l`.
|
||||||
|
|
||||||
All eight are done for the decided scope. What is left is not on this list: the four destinations the
|
All eight are done for the decided scope, and v2 has since gone past it — see the note under step 7.
|
||||||
phone does not draw, file transfer (deliberately deferred, and see the decision above for the shape it
|
|
||||||
takes), the App Link upgrade, and — the one that is not optional — running any of it on a device.
|
What is left, in the order it matters:
|
||||||
|
|
||||||
|
- **Running any of it on a device.** Still the one that is not optional, and still true: nothing here has
|
||||||
|
ever been launched on hardware or an emulator. Everything below is reasoning from the code.
|
||||||
|
- **The document picker.** The files screen browses a remote, downloads from it and runs the queue; putting
|
||||||
|
a file *on* a host from the phone needs `ACTION_OPEN_DOCUMENT` behind a third `IRemoteFileStore`-shaped
|
||||||
|
source, which is the decision recorded above and the obvious next piece of work.
|
||||||
|
- **Editors.** There is no host editor and no keychain item editor on the phone, so both are create-on-
|
||||||
|
desktop-and-sync. That is why the v2 design's `+` buttons on HOSTS and on the keychain are not drawn.
|
||||||
|
- **Pins, teams and import**, which v2 does not draw either.
|
||||||
|
- **The App Link upgrade**, unchanged from step 5.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,41 @@
|
|||||||
The Avalonia client's interface was rebuilt from a Claude Design project — a five-screen spec covering
|
The Avalonia client's interface was rebuilt from a Claude Design project — a five-screen spec covering
|
||||||
the chrome, hosts and terminals, file transfer, the vault, teams, and preferences.
|
the chrome, hosts and terminals, file transfer, the vault, teams, and preferences.
|
||||||
|
|
||||||
|
> ## The second import — v2, phone
|
||||||
|
>
|
||||||
|
> A later design in the same project, **DodoSSH Android v2**, is what the phone head now draws: eight
|
||||||
|
> screens, a bottom bar of four with the rest behind MORE, and a new palette. It has been imported, and
|
||||||
|
> this section is the same accounting for it. The desktop's own v2 has **not** been imported — only its
|
||||||
|
> palette, because the palette is shared and the two heads may not disagree about a colour.
|
||||||
|
>
|
||||||
|
> **The palette went from green to blue, and one colour changed meaning rather than value.** Green used to
|
||||||
|
> be the accent, so `Ellipse.dot.live` filled with `Accent` and "the thing to press" and "a shell is open
|
||||||
|
> here" were the same colour by construction. v2 makes the accent `#5B8CFF` and keeps a green, `#3DDC97`,
|
||||||
|
> for status alone. That is now `Live`, and nothing merely interactive may use it. The accent is also two
|
||||||
|
> colours — `Accent` fills, `AccentText` writes — which is what stops a row of chips reading as a row of
|
||||||
|
> primary actions.
|
||||||
|
>
|
||||||
|
> **Five destinations moved under MORE and four of them were built**: snippets, SFTP, S3 and logs, each
|
||||||
|
> over a view model that already existed, plus preferences. `ShellScreen` gained `More` and `Buckets`;
|
||||||
|
> SFTP and S3 are one screen over one `TransfersViewModel`, differing only in which picker they offer.
|
||||||
|
>
|
||||||
|
> | v2 element | What ships instead |
|
||||||
|
> | --- | --- |
|
||||||
|
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
||||||
|
> | `23 ms · fwd 5432` on the terminal's connection line | The account and endpoint actually dialled. There is no RTT measurement in SSH.NET and nothing forwards anything. |
|
||||||
|
> | `ED25519` badge and `SHA256:kQ9f…Zw2M` on every keychain card | `Detail`, which is what is genuinely known *about* an item. Unchanged from the first import: no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim. |
|
||||||
|
> | An `agent` chip on a key | Omitted. There is no agent of any kind — see the first import's Vault section. |
|
||||||
|
> | Snippet cards footed with `edge-eu-1 · today 10:58 · exit 0` | The command and its badge. Nothing records where a snippet ran, when, or what it returned; the shell it is typed into never reports back. |
|
||||||
|
> | Logs: a blinking **FOLLOW** pill over a live `tail` | A REFRESH button. These are the keychain's own synced audit records, written once at close and read when the screen opens — there is nothing to tail. |
|
||||||
|
> | Logs: `ALL 8 / INFO 5 / WARN 2 / ERROR 1` and per-host chips | The real two-way choice: connections, or keychain activity. A connection record is not a syslog line and carries no level, and neither log filters by host. |
|
||||||
|
> | SFTP: the `LOCAL / REMOTE` toggle | One remote pane. Scoped storage means there is no browsable local filesystem, which `docs/android-port.md` decided before any of this was built. |
|
||||||
|
> | SFTP: the transfer card, and moving files at all | ◆ **Browsing, opening and deleting only, and the screen says so.** Both transfer commands exist and both work — against 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 report success and leave the file somewhere the person who asked for it cannot open. That is worse than not offering it, because a refusal is visible and a file in `/data/user/0/…` is not. The queue is not drawn either, since nothing on this head can put anything in it. Both return with the document picker. |
|
||||||
|
> | S3: `96 objects · 412 GB` and `lifecycle: IA after 7d` | Omitted. Neither a bucket total nor a lifecycle policy is fetched; the listing is what `IRemoteFileStore` returns. |
|
||||||
|
> | A `+` floating button on **HOSTS** | Omitted. It would open a host editor, and this head has none — hosts are created on the desktop and sync down. |
|
||||||
|
> | A 14px rounded inset around the terminal | Not drawn. The renderer is a native child view composited above everything Avalonia draws, so a rounded frame behind it clips nothing. What the design was after comes from the page's own background, which is the design's `#171A26`. |
|
||||||
|
> | Instrument Sans / Fira Code | Inter and the system monospace stack, as before and for the same reason — see the note on `MonoFont` in `Palette.axaml`. |
|
||||||
|
> | A theme toggle (`☀`) in the header, and the desktop v2's light theme | Omitted. There is one theme by decision, and a switch with one position is a control that does nothing. |
|
||||||
|
|
||||||
Most of it landed. This file is the rest: every element of that design with nothing behind it, which
|
Most of it landed. This file is the rest: every element of that design with nothing behind it, which
|
||||||
project each piece would have to land in, and **what the shipped interface does instead**. That last
|
project each piece would have to land in, and **what the shipped interface does instead**. That last
|
||||||
column is the important one. This codebase states its limits rather than implying them — the README does
|
column is the important one. This codebase states its limits rather than implying them — the README does
|
||||||
|
|||||||
@@ -76,8 +76,13 @@ public sealed partial class DodoSshApp : Avalonia.Application
|
|||||||
|
|
||||||
// Difference 2: the foreground service, which is what makes TerminalWorkspace's promise — that a
|
// Difference 2: the foreground service, which is what makes TerminalWorkspace's promise — that a
|
||||||
// shell outlives a vault lock — true on a platform that stops backgrounded processes.
|
// shell outlives a vault lock — true on a platform that stops backgrounded processes.
|
||||||
// Zero transfers for now: file transfer is out of this head's first scope by decision, and this is
|
//
|
||||||
// the seam it arrives through rather than a dependency taken before there is anything behind it.
|
// Still zero transfers, and the reason moved rather than went away. v2 built the files screen, so
|
||||||
|
// this head can now browse a remote — but it cannot start a transfer, because both directions need
|
||||||
|
// the system document picker that scoped storage forces and that is not built (see FilesScreen).
|
||||||
|
// So the count is zero because the queue provably cannot have anything in it, not because nothing
|
||||||
|
// was wired. This is still the seam it arrives through: when the picker lands, this reads the
|
||||||
|
// queue and Refresh() gets called as transfers start and finish.
|
||||||
// A local rather than a field, matching the desktop head: an Avalonia Application has no disposal
|
// A local rather than a field, matching the desktop head: an Avalonia Application has no disposal
|
||||||
// hook, so a field holding a disposable would have nowhere honest to release it. It stays alive
|
// hook, so a field holding a disposable would have nowhere honest to release it. It stays alive
|
||||||
// because it is subscribed to the workspace, which lives as long as the process.
|
// because it is subscribed to the workspace, which lives as long as the process.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
bitmap so there is one geometry to change and no set of five PNG densities to forget one
|
bitmap so there is one geometry to change and no set of five PNG densities to forget one
|
||||||
of.
|
of.
|
||||||
|
|
||||||
#3CE88F is AccentColor from DodoSSH.Client.Shell's Theme/Palette.axaml, written out
|
#5B8CFF is AccentColor from DodoSSH.Client.Shell's Theme/Palette.axaml, written out
|
||||||
because an Android resource cannot reference a XAML dictionary. The same duplication
|
because an Android resource cannot reference a XAML dictionary. The same duplication
|
||||||
colors.xml already carries for the window background, and the same rule applies: if the
|
colors.xml already carries for the window background, and the same rule applies: if the
|
||||||
palette moves, this moves with it.
|
palette moves, this moves with it.
|
||||||
@@ -31,14 +31,14 @@
|
|||||||
<path
|
<path
|
||||||
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
|
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#3CE88F"
|
android:strokeColor="#5B8CFF"
|
||||||
android:strokeWidth="2.2" />
|
android:strokeWidth="2.2" />
|
||||||
|
|
||||||
<!-- The chevron of >_ -->
|
<!-- The chevron of >_ -->
|
||||||
<path
|
<path
|
||||||
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
|
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#3CE88F"
|
android:strokeColor="#5B8CFF"
|
||||||
android:strokeWidth="2.8"
|
android:strokeWidth="2.8"
|
||||||
android:strokeLineCap="round"
|
android:strokeLineCap="round"
|
||||||
android:strokeLineJoin="round" />
|
android:strokeLineJoin="round" />
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<path
|
<path
|
||||||
android:pathData="M54,59.7 L63.2,59.7"
|
android:pathData="M54,59.7 L63.2,59.7"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:strokeColor="#3CE88F"
|
android:strokeColor="#5B8CFF"
|
||||||
android:strokeWidth="2.8"
|
android:strokeWidth="2.8"
|
||||||
android:strokeLineCap="round" />
|
android:strokeLineCap="round" />
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
between an Android resource and a XAML resource dictionary, so the duplication is stated rather than
|
between an Android resource and a XAML resource dictionary, so the duplication is stated rather than
|
||||||
hidden.
|
hidden.
|
||||||
-->
|
-->
|
||||||
<color name="dodo_window">#0A0C0B</color>
|
<color name="dodo_window">#0E1220</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
the sync dot, which a system action bar has nowhere to put.
|
the sync dot, which a system action bar has nowhere to put.
|
||||||
|
|
||||||
windowLightStatusBar is false so the clock and the battery icon are drawn light. The design's status
|
windowLightStatusBar is false so the clock and the battery icon are drawn light. The design's status
|
||||||
strip is #7E8A84 text on near-black, and leaving this at its default renders dark-on-dark — legible
|
strip is #8B93B0 text on near-black, and leaving this at its default renders dark-on-dark — legible
|
||||||
on the mock-up and invisible on a device.
|
on the mock-up and invisible on a device.
|
||||||
-->
|
-->
|
||||||
<style name="DodoTheme" parent="Theme.AppCompat.NoActionBar">
|
<style name="DodoTheme" parent="Theme.AppCompat.NoActionBar">
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
Classes rather than implicit styles for the buttons, because the shell needs three that differ in
|
Classes rather than implicit styles for the buttons, because the shell needs three that differ in
|
||||||
meaning rather than in degree: the accent one is what the screen wants you to do, the outlined one is
|
meaning rather than in degree: the accent one is what the screen wants you to do, the outlined one is
|
||||||
the alternative, and the red one is the one that costs something.
|
the alternative, and the red one is the one that costs something.
|
||||||
|
|
||||||
|
── v2 ──────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
The second design rounds everything. The corner radii below are the design's own — 4 for a tag, 9 for a
|
||||||
|
button or a pill, 10 for a list row, 11 for the search well, 12 for a card, 14 for a block of
|
||||||
|
monospaced output — and they are a ladder rather than a set of preferences: the radius says how big the
|
||||||
|
thing is, so a 12 on a chip or a 4 on a card reads as the wrong size before it reads as the wrong shape.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Style Selector="Button.primary">
|
<Style Selector="Button.primary">
|
||||||
@@ -18,8 +24,8 @@
|
|||||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
<Setter Property="Background" Value="{StaticResource Accent}" />
|
<Setter Property="Background" Value="{StaticResource Accent}" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Canvas}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
|
||||||
<Setter Property="CornerRadius" Value="6" />
|
<Setter Property="CornerRadius" Value="9" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
@@ -35,7 +41,7 @@
|
|||||||
reads the screen as broken rather than as waiting for them.
|
reads the screen as broken rather than as waiting for them.
|
||||||
-->
|
-->
|
||||||
<Style Selector="Button.primary:disabled /template/ ContentPresenter">
|
<Style Selector="Button.primary:disabled /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="#1A1F1D" />
|
<Setter Property="Background" Value="{StaticResource Raised}" />
|
||||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextFaint}" />
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextFaint}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
@@ -48,7 +54,7 @@
|
|||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
<Setter Property="CornerRadius" Value="6" />
|
<Setter Property="CornerRadius" Value="9" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="FontSize" Value="11.5" />
|
<Setter Property="FontSize" Value="11.5" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
@@ -63,7 +69,7 @@
|
|||||||
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
|
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
||||||
<Setter Property="CornerRadius" Value="5" />
|
<Setter Property="CornerRadius" Value="9" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="FontSize" Value="10.5" />
|
<Setter Property="FontSize" Value="10.5" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
@@ -76,7 +82,7 @@
|
|||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderThickness" Value="0" />
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="0" />
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
<Setter Property="Padding" Value="16,6" />
|
<Setter Property="Padding" Value="16,6" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.row:pressed /template/ ContentPresenter">
|
<Style Selector="Button.row:pressed /template/ ContentPresenter">
|
||||||
@@ -84,20 +90,108 @@
|
|||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
A category chip. A RadioButton rather than a ToggleButton because the four are exclusive and a screen
|
The 44-pixel square in a header — a back arrow, an add, an upload. Square rather than stretched because
|
||||||
|
it sits beside a title that must keep the width it has, and 44 rather than the glyph's own size because
|
||||||
|
a 17-pixel arrow is a 17-pixel target.
|
||||||
|
-->
|
||||||
|
<Style Selector="Button.icon">
|
||||||
|
<Setter Property="Width" Value="44" />
|
||||||
|
<Setter Property="Height" Value="44" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
|
<Setter Property="FontSize" Value="17" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon.accent">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
|
<Setter Property="FontSize" Value="20" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.icon:pressed /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
No floating action button. The design puts one on HOSTS and one on S3, and both would open an editor
|
||||||
|
this head has not got — hosts and keychain items are created on the desktop and sync down. The style is
|
||||||
|
not here waiting for them either: an unused style is a claim that the control exists somewhere.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A card: one item, one rule, one thing you can act on. The design uses it for a key, a snippet, a
|
||||||
|
transfer and the panel at the foot of the keychain, and they are all the same shape deliberately —
|
||||||
|
the screens differ in what they hold rather than in how they are built.
|
||||||
|
-->
|
||||||
|
<Style Selector="Border.card">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Sidebar}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
<Setter Property="Padding" Value="14,13" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A block of monospaced output — a snippet's command, a log, the terminal. One surface for all three, and
|
||||||
|
it is not one of the palette's chrome greys: see TerminalSurface in Palette.axaml.
|
||||||
|
-->
|
||||||
|
<Style Selector="Border.output">
|
||||||
|
<Setter Property="Background" Value="{StaticResource TerminalSurface}" />
|
||||||
|
<Setter Property="CornerRadius" Value="14" />
|
||||||
|
<Setter Property="Padding" Value="12" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A small monospaced tag: a vault's name, a sync badge, what is open on the files screen. Not a control —
|
||||||
|
it is never pressed — so a Border rather than a Button, and the class exists so that the four screens
|
||||||
|
drawing one cannot disagree about its padding.
|
||||||
|
-->
|
||||||
|
<Style Selector="Border.tag">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Chip}" />
|
||||||
|
<Setter Property="CornerRadius" Value="4" />
|
||||||
|
<Setter Property="Padding" Value="7,2" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.tag > TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="FontSize" Value="9" />
|
||||||
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- An outlined tag, for a fact about an item rather than a name: a type, an algorithm, a count. -->
|
||||||
|
<Style Selector="Border.tag.outline">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="Padding" Value="5,1" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.tag.outline > TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
|
<Setter Property="FontSize" Value="8.5" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A category chip. A RadioButton rather than a ToggleButton because the options are exclusive and a screen
|
||||||
reader should say so; the bullet its default template draws is removed, since the chip's own fill is
|
reader should say so; the bullet its default template draws is removed, since the chip's own fill is
|
||||||
what carries the state.
|
what carries the state.
|
||||||
|
|
||||||
|
Checked is a filled surface with accent *text*, not an accent fill. v2 makes that distinction
|
||||||
|
everywhere — see the remark on AccentText in Palette.axaml — and a chip is where it matters most: a row
|
||||||
|
of four solid blue lozenges is a row of four things that all look like the primary action.
|
||||||
-->
|
-->
|
||||||
<Style Selector="RadioButton.chip">
|
<Style Selector="RadioButton.chip">
|
||||||
<Setter Property="MinHeight" Value="34" />
|
<Setter Property="MinHeight" Value="34" />
|
||||||
<Setter Property="Padding" Value="12,6" />
|
<Setter Property="Padding" Value="13,6" />
|
||||||
<Setter Property="CornerRadius" Value="17" />
|
<Setter Property="CornerRadius" Value="9" />
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="FontSize" Value="10" />
|
<Setter Property="FontSize" Value="10.5" />
|
||||||
<Setter Property="FontWeight" Value="Medium" />
|
<Setter Property="FontWeight" Value="Medium" />
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
@@ -114,19 +208,27 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="RadioButton.chip:checked">
|
<Style Selector="RadioButton.chip:checked">
|
||||||
<Setter Property="Background" Value="{StaticResource Accent}" />
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Canvas}" />
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
No segmented control either. The design draws one over the file listing for LOCAL / REMOTE, and there is
|
||||||
|
no local side on a phone — see FilesScreen. The two-way choices that are real, the log's two logs and
|
||||||
|
the keychain's categories, are chip rows, which is the same control at the same size without a well
|
||||||
|
around it.
|
||||||
|
-->
|
||||||
|
|
||||||
<Style Selector="TextBox.field">
|
<Style Selector="TextBox.field">
|
||||||
<Setter Property="Height" Value="48" />
|
<Setter Property="Height" Value="44" />
|
||||||
<Setter Property="Padding" Value="14,0" />
|
<Setter Property="Padding" Value="14,0" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
<Setter Property="Background" Value="{StaticResource Field}" />
|
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="6" />
|
<Setter Property="CornerRadius" Value="11" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="FontSize" Value="12" />
|
<Setter Property="FontSize" Value="12" />
|
||||||
@@ -136,6 +238,9 @@
|
|||||||
The live dot, and the class name is the same one the desktop sidebar uses so the two heads cannot
|
The live dot, and the class name is the same one the desktop sidebar uses so the two heads cannot
|
||||||
drift on what green means: a terminal is open on this host right now. Deliberately not reachability —
|
drift on what green means: a terminal is open on this host right now. Deliberately not reachability —
|
||||||
nothing pings anything, and a dot meaning "up" would be a claim the application never checks.
|
nothing pings anything, and a dot meaning "up" would be a claim the application never checks.
|
||||||
|
|
||||||
|
Live rather than Accent since v2. The accent is blue now and this is not an accent: it is a fact about
|
||||||
|
a host. See the remark on Live in Palette.axaml.
|
||||||
-->
|
-->
|
||||||
<Style Selector="Ellipse.dot">
|
<Style Selector="Ellipse.dot">
|
||||||
<Setter Property="Width" Value="7" />
|
<Setter Property="Width" Value="7" />
|
||||||
@@ -143,7 +248,7 @@
|
|||||||
<Setter Property="Fill" Value="{StaticResource TextGhost}" />
|
<Setter Property="Fill" Value="{StaticResource TextGhost}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Ellipse.dot.live">
|
<Style Selector="Ellipse.dot.live">
|
||||||
<Setter Property="Fill" Value="{StaticResource Accent}" />
|
<Setter Property="Fill" Value="{StaticResource Live}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Every label, count, address and fingerprint in this design is monospace. See Palette.axaml. -->
|
<!-- Every label, count, address and fingerprint in this design is monospace. See Palette.axaml. -->
|
||||||
@@ -177,11 +282,49 @@
|
|||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A screen's own name, in the header. The one place this design is not monospaced: the design sets every
|
||||||
|
heading in its sans face and keeps the mono for what is machine-shaped — an address, a fingerprint, a
|
||||||
|
count, a command. That split is worth honouring even though both resolve to substituted fonts here.
|
||||||
|
-->
|
||||||
|
<Style Selector="TextBlock.heading">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontSize" Value="17" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A group heading over a list: PRODUCTION, STAGING. Letter-spaced, and quieter than the rows under it —
|
||||||
|
but TextDim rather than the design's own #565E7E. In a flat list these headings are the only thing
|
||||||
|
separating one block of hosts from the next, and at 10px with a letter-space the fainter step is under
|
||||||
|
3:1 on this background. Quieter than a host row, still readable, which is what "quieter" has to mean.
|
||||||
|
-->
|
||||||
|
<Style Selector="TextBlock.section">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||||
|
<Setter Property="FontSize" Value="10" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="LetterSpacing" Value="1" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
A fingerprint, and it is a style rather than four repeated setters because the rule it encodes is a
|
A fingerprint, and it is a style rather than four repeated setters because the rule it encodes is a
|
||||||
product rule: never truncated, and always breakable across lines rather than clipped. Four screens show
|
product rule: never truncated, and always breakable across lines rather than clipped. Four screens show
|
||||||
one, and the day one of them clips is the day somebody approves the wrong key.
|
one, and the day one of them clips is the day somebody approves the wrong key.
|
||||||
-->
|
-->
|
||||||
|
<!--
|
||||||
|
◆ Declared twice, and this is the bug the rule above exists to prevent rather than a tidiness problem.
|
||||||
|
|
||||||
|
Avalonia's type selectors are exact: `TextBlock.fingerprint` does not match a SelectableTextBlock, which
|
||||||
|
derives from it. Every fingerprint on this head is selectable — you want to be able to copy one and
|
||||||
|
compare it — so with only the first selector the style applied to none of them, and a SHA256 fingerprint
|
||||||
|
rendered in the proportional font at the inherited size with TextWrapping left at NoWrap. At 360dp that
|
||||||
|
runs off the edge and clips, which is exactly the thing the comment above says must never happen.
|
||||||
|
|
||||||
|
The desktop head already carries the same pair for `mono` (App.axaml), for the same reason. Avalonia has
|
||||||
|
an `:is(TextBlock)` form that would match both in one selector; two explicit lines are used here because
|
||||||
|
the duplication is the thing worth seeing when somebody adds a third fingerprint site.
|
||||||
|
-->
|
||||||
<Style Selector="TextBlock.fingerprint">
|
<Style Selector="TextBlock.fingerprint">
|
||||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
@@ -189,5 +332,12 @@
|
|||||||
<Setter Property="LineHeight" Value="18" />
|
<Setter Property="LineHeight" Value="18" />
|
||||||
<Setter Property="TextWrapping" Value="Wrap" />
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="SelectableTextBlock.fingerprint">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontSize" Value="11" />
|
||||||
|
<Setter Property="LineHeight" Value="18" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Styles>
|
</Styles>
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<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.FilesScreen"
|
||||||
|
x:DataType="vm:TransfersViewModel"
|
||||||
|
Background="{StaticResource Canvas}">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Designs 06 and 08 — SFTP and S3, which are one screen.
|
||||||
|
|
||||||
|
Below the picker the two are the same thing: a bucket and an SFTP host are both an IRemoteFileStore, and
|
||||||
|
the pane, the queue and every transfer were written once. What differs is which picker is offered and
|
||||||
|
what the button says — OPEN for a bucket, because S3 is request-per-operation and nothing stays
|
||||||
|
connected. Drawing them twice would be two copies of a screen obliged to stay identical.
|
||||||
|
|
||||||
|
**One pane, not two.** The design's LOCAL / REMOTE toggle is not drawn, and this is the decision
|
||||||
|
docs/android-port.md took before any of this was built: Android has scoped storage, so there is no
|
||||||
|
browsable local filesystem to put in the other half. TransfersViewModel's local pane — LocalPath,
|
||||||
|
LocalRoots, LocalEntries — is desktop-only and is left alone here rather than shown empty.
|
||||||
|
|
||||||
|
◆ **And that is why neither DOWNLOAD nor UPLOAD is on this screen.** Both commands exist and both work;
|
||||||
|
what they work *against* is the local pane. `QueueDownloads` writes to `Path.Combine(LocalPath, name)`,
|
||||||
|
and `LocalPath` starts at `LocalDirectory.Home` — `SpecialFolder.UserProfile`, which on Android is the
|
||||||
|
application's own private directory. A download would report success and put the file somewhere 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 way in and out is the system document picker, which is the shape
|
||||||
|
docs/android-port.md decided on and is the next piece of work.
|
||||||
|
|
||||||
|
So what ships is browsing a remote, and the two remote-side operations that need nothing local —
|
||||||
|
opening a directory and deleting. The queue is drawn because a transfer can still be running when this
|
||||||
|
screen is opened; it is simply not something this head can start yet.
|
||||||
|
|
||||||
|
◆ **The host key prompts are here too.** File transfer is a second, separate authenticated connection
|
||||||
|
and it makes its own trust decision — the host records a second login. So this screen carries its own
|
||||||
|
unknown-key prompt and its own changed-key refusal, over TransfersViewModel's own PendingHostKey, and
|
||||||
|
the refusal has no continue button here either.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*,Auto">
|
||||||
|
|
||||||
|
<!-- ============ header ============ -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Height="56" Margin="8,0">
|
||||||
|
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||||
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="1" Classes="heading" Text="SFTP" Margin="4,0"
|
||||||
|
IsVisible="{Binding ShowsHostPicker}" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="heading" Text="S3" Margin="4,0"
|
||||||
|
IsVisible="{Binding ShowsBucketPicker}" />
|
||||||
|
|
||||||
|
<!-- What is actually open, named. On a bucket this reads s3://name. -->
|
||||||
|
<Border Grid.Column="2" Classes="tag" Margin="8,0" HorizontalAlignment="Left"
|
||||||
|
IsVisible="{Binding IsConnected}">
|
||||||
|
<TextBlock Text="{Binding ConnectedTo}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button Grid.Column="3" Classes="icon" Content="↻" Command="{Binding RefreshRemoteCommand}"
|
||||||
|
IsVisible="{Binding IsConnected}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ============ opening one ============ -->
|
||||||
|
<Border Grid.Row="1" Classes="card" Margin="12,0,12,8" IsVisible="{Binding !IsConnected}">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
|
||||||
|
<ComboBox HorizontalAlignment="Stretch" MinHeight="44" IsVisible="{Binding ShowsHostPicker}"
|
||||||
|
ItemsSource="{Binding Hosts}" SelectedItem="{Binding SelectedHost}"
|
||||||
|
PlaceholderText="choose a host">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||||||
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<ComboBox HorizontalAlignment="Stretch" MinHeight="44" IsVisible="{Binding ShowsBucketPicker}"
|
||||||
|
ItemsSource="{Binding Buckets}" SelectedItem="{Binding SelectedBucket}"
|
||||||
|
PlaceholderText="choose a bucket">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
||||||
|
<StackPanel Spacing="1">
|
||||||
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||||||
|
<TextBlock Classes="detail" Text="{Binding Description}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Its own box, deliberately not the hosts screen's. This is a separate authentication, so a password
|
||||||
|
typed to open a terminal has not been offered here — and quietly reusing it would make a one-time
|
||||||
|
password appear to work twice.
|
||||||
|
-->
|
||||||
|
<TextBox Classes="field" IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||||
|
Text="{Binding TypedPassword}" PasswordChar="•" PlaceholderText="password" />
|
||||||
|
|
||||||
|
<Button Classes="primary" Content="{Binding ConnectLabel}" Command="{Binding ConnectCommand}"
|
||||||
|
IsEnabled="{Binding !IsBusy}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ============ ◆ the host key decisions ============ -->
|
||||||
|
<!--
|
||||||
|
The unknown key: the fingerprint in full, wrapping rather than clipping, and two ways out.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="2" Margin="12,0,12,8" IsVisible="{Binding HasPendingHostKey}"
|
||||||
|
Background="{StaticResource WarnWash}" BorderBrush="{StaticResource WarnSoft}"
|
||||||
|
BorderThickness="1" CornerRadius="12" Padding="14">
|
||||||
|
<StackPanel Spacing="9">
|
||||||
|
<TextBlock Classes="label" Foreground="{StaticResource Warn}" Text="UNKNOWN HOST KEY" />
|
||||||
|
<TextBlock Classes="body">
|
||||||
|
<Run Text="First contact with" />
|
||||||
|
<Run Text="{Binding PendingHostKey.Host}" Foreground="{StaticResource Text}" />
|
||||||
|
<Run Text="·" />
|
||||||
|
<Run Text="{Binding PendingHostKey.Port}" />
|
||||||
|
<Run Text=". Nothing in this keychain has approved this key." />
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Classes="label" Foreground="{StaticResource TextFaint}"
|
||||||
|
Text="{Binding PendingHostKey.Algorithm}" />
|
||||||
|
<!-- ◆ In full, and wrapping rather than clipping. See the .fingerprint style. -->
|
||||||
|
<SelectableTextBlock Classes="fingerprint" Text="{Binding PendingHostKey.Fingerprint}" />
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="TRUST AND OPEN"
|
||||||
|
Command="{Binding TrustHostKeyCommand}" />
|
||||||
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||||
|
Command="{Binding RejectHostKeyCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ The changed key, and it has no continue button. Presenting this as dismissible is the one design
|
||||||
|
mistake that matters here — the same rule the hosts screen's sheet follows.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="2" Margin="12,0,12,8" IsVisible="{Binding HasHostKeyMismatch}"
|
||||||
|
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||||
|
BorderThickness="1" CornerRadius="12" Padding="14">
|
||||||
|
<StackPanel Spacing="9">
|
||||||
|
<TextBlock Classes="label" Foreground="{StaticResource Danger}" Text="HOST KEY CHANGED — REFUSED" />
|
||||||
|
<TextBlock Classes="body" Foreground="{StaticResource DangerText}" Text="{Binding HostKeyMismatch}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ============ where we are ============ -->
|
||||||
|
<ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||||
|
IsVisible="{Binding IsConnected}" Margin="0,0,0,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4" Margin="14,0" VerticalAlignment="Center">
|
||||||
|
<Button Classes="row" MinHeight="36" Padding="9,0" CornerRadius="9"
|
||||||
|
Command="{Binding RemoteUpCommand}">
|
||||||
|
<TextBlock Classes="mono" FontSize="12" Text="↑" Foreground="{StaticResource AccentText}" />
|
||||||
|
</Button>
|
||||||
|
<ItemsControl ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="2" /></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||||||
|
<Button Classes="row" MinHeight="36" Padding="7,0" CornerRadius="9"
|
||||||
|
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
||||||
|
CommandParameter="{Binding Path}">
|
||||||
|
<TextBlock Classes="detail" FontSize="11" Text="{Binding Name}" />
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- ============ the listing ============ -->
|
||||||
|
<Panel Grid.Row="4">
|
||||||
|
|
||||||
|
<TextBlock Classes="body" IsVisible="{Binding !IsConnected}" Margin="24,12"
|
||||||
|
VerticalAlignment="Top" Text="{Binding Status}" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Tapped on the list rather than a Command on the row, and that is not a preference.
|
||||||
|
|
||||||
|
A Button as the ItemTemplate root swallows the press: the ListBoxItem never sees it, SelectedItem
|
||||||
|
stays null, and every command that reads SelectedRemoteEntry — opening a directory, deleting —
|
||||||
|
quietly does nothing. Measured, not assumed. So the row is a Border, selection works as it does on
|
||||||
|
every other list in this head, and the one phone-specific behaviour is here: a single tap on a
|
||||||
|
directory opens it, because a phone has no double-tap-to-open and no Enter.
|
||||||
|
|
||||||
|
Not driven from OnSelectedRemoteEntryChanged, which would have been the tidier-looking place: that
|
||||||
|
setter is in the shared view model, and the desktop pane deliberately opens on DoubleTapped — making
|
||||||
|
selection navigate would send a single desktop click walking into a directory.
|
||||||
|
-->
|
||||||
|
<ListBox ItemsSource="{Binding RemoteEntries}" SelectedItem="{Binding SelectedRemoteEntry}"
|
||||||
|
Background="Transparent" BorderThickness="0" IsVisible="{Binding IsConnected}"
|
||||||
|
Tapped="OnRemoteEntryTapped">
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
<Setter Property="Margin" Value="10,1" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
||||||
|
<Border MinHeight="52" Padding="10,7">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Width="16" HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" FontSize="12" Text="▸"
|
||||||
|
Foreground="{StaticResource AccentText}" IsVisible="{Binding IsNavigable}" />
|
||||||
|
<TextBlock Grid.Column="0" Width="16" HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" FontSize="12" Text="·"
|
||||||
|
Foreground="{StaticResource TextFaint}" IsVisible="{Binding IsFile}" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Margin="11,0,0,0" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="12.5" Text="{Binding Name}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
|
<TextBlock Classes="detail" Text="{Binding Size}"
|
||||||
|
IsVisible="{Binding Size, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||||
|
<!-- Real POSIX bits from the server, not a plausible-looking mode. -->
|
||||||
|
<TextBlock Classes="detail" Text="{Binding Permissions}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Modified}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- ============ what to do with the chosen entry ============ -->
|
||||||
|
<Border Grid.Row="5" IsVisible="{Binding IsConnected}" Background="{StaticResource Chrome}"
|
||||||
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="12,10">
|
||||||
|
<StackPanel Spacing="9">
|
||||||
|
|
||||||
|
<!-- ◆ Counted, and it replaces the actions in place rather than stacking over the row it names. -->
|
||||||
|
<Border IsVisible="{Binding IsConfirmingRemoteDeletion}" Background="{StaticResource DangerWash}"
|
||||||
|
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="12"
|
||||||
|
Padding="13,11">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||||||
|
Text="{Binding PendingRemoteDeletion.Question}" />
|
||||||
|
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||||||
|
Text="{Binding PendingRemoteDeletion.Consequence}" />
|
||||||
|
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||||
|
<Button Grid.Column="0" Classes="danger" Height="44" Content="DELETE"
|
||||||
|
Command="{Binding ConfirmDeleteRemoteCommand}" />
|
||||||
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP"
|
||||||
|
Command="{Binding CancelDeleteRemoteCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsConfirmingRemoteDeletion}">
|
||||||
|
<Button Grid.Column="0" Classes="secondary" Height="44" Content="DELETE"
|
||||||
|
Command="{Binding DeleteRemoteCommand}" IsEnabled="{Binding CanDeleteRemote}" />
|
||||||
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CLOSE"
|
||||||
|
Command="{Binding DisconnectCommand}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Classes="body" IsVisible="{Binding !IsConfirmingRemoteDeletion}"
|
||||||
|
Text="Copying files to and from this phone needs the system document picker, which is not built yet — see the note at the top of this screen. Browsing, opening and deleting work." />
|
||||||
|
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
||||||
|
Text="{Binding Status}" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
There is no queue on this screen, and that follows from the note at the top rather than being a
|
||||||
|
separate decision: nothing here can enqueue a transfer, so a queue would be a region that is
|
||||||
|
empty for every possible state of the application. It comes back with the document picker, along
|
||||||
|
with the two buttons that would fill it.
|
||||||
|
-->
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
using DodoSSH.Client.Shell.ViewModels;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Android.Views;
|
||||||
|
|
||||||
|
/// <summary>Designs 06 and 08 — one remote pane, over SFTP or over a bucket.</summary>
|
||||||
|
internal sealed partial class FilesScreen : UserControl
|
||||||
|
{
|
||||||
|
public FilesScreen() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a directory on a single tap.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The desktop pane opens on a double tap and on Enter. A phone has neither, so the gesture has to be
|
||||||
|
/// the one gesture there is — and that is why this is here rather than in the shared view model: making
|
||||||
|
/// selection navigate would give the desktop a single click that walks into a directory.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="InputElement.Tapped"/> rather than a command on the row, because the row must stay a
|
||||||
|
/// plain <see cref="Border"/>: a button as the item template swallows the press before the list sees it
|
||||||
|
/// — it marks the pointer event handled, and the list's own handler is not registered for handled
|
||||||
|
/// events — leaving nothing selected and every command that reads the selection doing nothing. Tapped
|
||||||
|
/// fires after the list has moved its selection, which is what lets this read it.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// A file is left selected rather than opened. There is nothing this head could do with it — see the
|
||||||
|
/// note about the document picker at the top of the screen — and the actions below act on the selection.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private void OnRemoteEntryTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TransfersViewModel { SelectedRemoteEntry.IsNavigable: true } transfers)
|
||||||
|
{
|
||||||
|
transfers.OpenRemoteCommand.Execute(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,8 +24,12 @@
|
|||||||
<!-- ============ 05 UNKNOWN HOST KEY ============ -->
|
<!-- ============ 05 UNKNOWN HOST KEY ============ -->
|
||||||
<Panel IsVisible="{Binding HasPendingHostKey}">
|
<Panel IsVisible="{Binding HasPendingHostKey}">
|
||||||
|
|
||||||
<!-- Scrim. Non-interactive by itself: tapping outside must not answer the question. -->
|
<!--
|
||||||
<Border Background="#9E040505" />
|
Scrim. Non-interactive by itself: tapping outside must not answer the question. 62% of Canvas,
|
||||||
|
written out because the palette holds no alpha variant of a surface — see QuickConnect on the
|
||||||
|
desktop, which carries the same note.
|
||||||
|
-->
|
||||||
|
<Border Background="#9E0E1220" />
|
||||||
|
|
||||||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
Background="{StaticResource Canvas}">
|
Background="{StaticResource Canvas}">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Design 02 — HOSTS, the phone's home.
|
Design 01 — HOSTS, the phone's home.
|
||||||
|
|
||||||
The desktop's sidebar and its right-hand connect column cannot both be on screen at 360dp, so they are
|
The desktop's sidebar and its right-hand connect column cannot both be on screen at 360dp, so they are
|
||||||
the same column here: the list fills the screen, and choosing a host raises the connect bar over the
|
the same column here: the list fills the screen, and choosing a host raises the connect bar over the
|
||||||
@@ -15,29 +15,35 @@
|
|||||||
|
|
||||||
Groups are rows in the same list rather than a nesting container, which is what SidebarRows already
|
Groups are rows in the same list rather than a nesting container, which is what SidebarRows already
|
||||||
hands both heads. The desktop shows group headings only when groups exist and so does this, for free.
|
hands both heads. The desktop shows group headings only when groups exist and so does this, for free.
|
||||||
|
|
||||||
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
The rows became cards: rounded, inset from the edge, and filled when chosen rather than marked with a
|
||||||
|
strip. What did not come across is the design's floating + button. It would open a host editor, and this
|
||||||
|
head has none — hosts are created on the desktop and sync down. A button that opened nothing would be
|
||||||
|
the one control on this screen that lied about what the phone can do; see docs/design-import-gaps.md.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
|
|
||||||
<!-- ============ filter ============ -->
|
<!-- ============ filter ============ -->
|
||||||
<!--
|
<!--
|
||||||
The design's "quick connect · search hosts" bar. On the desktop this is Ctrl+K over a palette; on a
|
The design's "Search or connect…" well. On the desktop this is Ctrl+K over a palette; on a phone the
|
||||||
phone the list is already the palette, so it filters in place rather than opening a second surface.
|
list is already the palette, so it filters in place rather than opening a second surface.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="0" Margin="16,12,16,4" Background="{StaticResource Field}"
|
<Border Grid.Row="0" Margin="14,10,14,4" Background="{StaticResource Field}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="6">
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="11" Height="44">
|
||||||
<Grid ColumnDefinitions="Auto,*">
|
<Grid ColumnDefinitions="Auto,*">
|
||||||
<TextBlock Grid.Column="0" Text=">" Classes="detail" Margin="12,0,0,0"
|
<TextBlock Grid.Column="0" Text="⌕" Foreground="{StaticResource TextFaint}" FontSize="13"
|
||||||
VerticalAlignment="Center" />
|
Margin="13,0,0,0" VerticalAlignment="Center" />
|
||||||
<TextBox Grid.Column="1" Text="{Binding HostFilter}" PlaceholderText="search hosts"
|
<TextBox Grid.Column="1" Text="{Binding HostFilter}" PlaceholderText="Search hosts"
|
||||||
Height="44" Padding="9,0" VerticalContentAlignment="Center"
|
Padding="9,0" VerticalContentAlignment="Center"
|
||||||
Background="Transparent" BorderThickness="0"
|
Background="Transparent" BorderThickness="0"
|
||||||
Foreground="{StaticResource Text}" FontFamily="{StaticResource MonoFont}" FontSize="11.5" />
|
Foreground="{StaticResource Text}" FontFamily="{StaticResource MonoFont}" FontSize="12" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ============ sync + conflicts ============ -->
|
<!-- ============ sync + conflicts ============ -->
|
||||||
<StackPanel Grid.Row="1" Margin="16,4,16,0" Spacing="6">
|
<StackPanel Grid.Row="1" Margin="16,4,16,0" Spacing="7">
|
||||||
|
|
||||||
<TextBlock Classes="detail" Text="{Binding Status}" TextWrapping="Wrap" />
|
<TextBlock Classes="detail" Text="{Binding Status}" TextWrapping="Wrap" />
|
||||||
|
|
||||||
@@ -46,11 +52,11 @@
|
|||||||
banner the user has to dismiss rather than a screen they might never open.
|
banner the user has to dismiss rather than a screen they might never open.
|
||||||
-->
|
-->
|
||||||
<Border IsVisible="{Binding HasConflicts}" Background="{StaticResource WarnWash}"
|
<Border IsVisible="{Binding HasConflicts}" Background="{StaticResource WarnWash}"
|
||||||
BorderBrush="{StaticResource WarnSoft}" BorderThickness="1" CornerRadius="6" Padding="12,10">
|
BorderBrush="{StaticResource WarnSoft}" BorderThickness="1" CornerRadius="12" Padding="13,11">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="9">
|
||||||
<ItemsControl ItemsSource="{Binding Conflicts}">
|
<ItemsControl ItemsSource="{Binding Conflicts}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate x:DataType="vm:ConflictRowViewModel">
|
||||||
<TextBlock Classes="detail" Foreground="{StaticResource WarnText}" TextWrapping="Wrap"
|
<TextBlock Classes="detail" Foreground="{StaticResource WarnText}" TextWrapping="Wrap"
|
||||||
Text="{Binding Summary}" Margin="0,2" />
|
Text="{Binding Summary}" Margin="0,2" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@@ -62,22 +68,33 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border IsVisible="{Binding HasUnreadableItems}" Background="{StaticResource DangerWash}"
|
<Border IsVisible="{Binding HasUnreadableItems}" Background="{StaticResource DangerWash}"
|
||||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="6" Padding="12,10">
|
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="12" Padding="13,11">
|
||||||
<TextBlock Classes="detail" Foreground="{StaticResource DangerText}" TextWrapping="Wrap"
|
<TextBlock Classes="detail" Foreground="{StaticResource DangerText}" TextWrapping="Wrap"
|
||||||
Text="{Binding UnreadableSummary}" />
|
Text="{Binding UnreadableSummary}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- ============ the list ============ -->
|
<!-- ============ the list ============ -->
|
||||||
<ListBox Grid.Row="2" Margin="0,8,0,0"
|
<ListBox Grid.Row="2" Margin="0,6,0,0"
|
||||||
ItemsSource="{Binding SidebarRows}"
|
ItemsSource="{Binding SidebarRows}"
|
||||||
SelectedItem="{Binding SelectedSidebarRow}"
|
SelectedItem="{Binding SelectedSidebarRow}"
|
||||||
Background="Transparent" BorderThickness="0">
|
Background="Transparent" BorderThickness="0">
|
||||||
|
|
||||||
<ListBox.Styles>
|
<ListBox.Styles>
|
||||||
|
<!--
|
||||||
|
The chosen row is a filled card. The corner radius is on the item rather than on a Border inside
|
||||||
|
it so that the fill the theme draws for selection is the rounded one — a radius set inside would
|
||||||
|
leave a square highlight behind a rounded row.
|
||||||
|
-->
|
||||||
<Style Selector="ListBoxItem">
|
<Style Selector="ListBoxItem">
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="MinHeight" Value="0" />
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
<Setter Property="Margin" Value="10,1" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.Styles>
|
</ListBox.Styles>
|
||||||
|
|
||||||
@@ -89,45 +106,52 @@
|
|||||||
the filter, which is what a heading over a filtered list has to say.
|
the filter, which is what a heading over a filtered list has to say.
|
||||||
-->
|
-->
|
||||||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="16,12,16,6">
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="8,12,8,5">
|
||||||
<TextBlock Grid.Column="0" Classes="detail" Text="{Binding Chevron}" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="0" Classes="detail" Text="{Binding Chevron}" VerticalAlignment="Center" />
|
||||||
<TextBlock Grid.Column="1" Classes="label" Text="{Binding Label}" Margin="7,0,0,0" />
|
<TextBlock Grid.Column="1" Classes="section" Text="{Binding Label}" Margin="7,0,0,0" />
|
||||||
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Count}" FontSize="9" />
|
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Count}" FontSize="9" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<!-- A host. -->
|
<!-- A host. -->
|
||||||
<DataTemplate DataType="vm:HostRowViewModel">
|
<DataTemplate DataType="vm:HostRowViewModel">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" MinHeight="54" Margin="16,6">
|
<Grid ColumnDefinitions="Auto,*,Auto" MinHeight="56" Margin="14,12">
|
||||||
|
|
||||||
<!-- Green means a terminal is open on this host right now. See the .dot style. -->
|
<!-- Green means a terminal is open on this host right now. See the .dot style. -->
|
||||||
<Ellipse Grid.Column="0" Classes="dot" Classes.live="{Binding IsConnected}"
|
<Ellipse Grid.Column="0" Classes="dot" Classes.live="{Binding IsConnected}" Width="8" Height="8"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="11,0,0,0" Spacing="2" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Margin="12,0,0,0" Spacing="3" VerticalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="13" FontWeight="Medium"
|
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="14" FontWeight="SemiBold"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
<Border IsVisible="{Binding HasVaultBadge}" BorderBrush="{StaticResource BorderFaint}"
|
<Border Classes="tag outline" IsVisible="{Binding HasVaultBadge}">
|
||||||
BorderThickness="1" CornerRadius="3" Padding="4,0" VerticalAlignment="Center">
|
<TextBlock Text="{Binding VaultBadge}" />
|
||||||
<TextBlock Classes="detail" FontSize="8.5" Text="{Binding VaultBadge}" />
|
|
||||||
</Border>
|
</Border>
|
||||||
<Border IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
<Border Classes="tag outline" BorderBrush="{StaticResource WarnSoft}"
|
||||||
BorderBrush="{StaticResource WarnSoft}" BorderThickness="1" CornerRadius="3"
|
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
Padding="4,0" VerticalAlignment="Center">
|
<TextBlock Foreground="{StaticResource Warn}" Text="{Binding Badge}" />
|
||||||
<TextBlock Classes="detail" FontSize="8" Foreground="{StaticResource Warn}"
|
|
||||||
Text="{Binding Badge}" />
|
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
<TextBlock Classes="detail" Text="{Binding Address}" TextTrimming="CharacterEllipsis" />
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||||||
<TextBlock Classes="detail" Text="·" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock Classes="detail" Text="{Binding Authentication}" />
|
<TextBlock Classes="detail" FontSize="10.5" Text="·" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Authentication}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The design's "live" tag, on exactly the hosts the dot is green for — and in the same green,
|
||||||
|
overriding the tag style's accent text. A status word in the accent colour beside a green dot
|
||||||
|
meaning the same thing would be one fact drawn in two colours.
|
||||||
|
-->
|
||||||
|
<Border Grid.Column="2" Classes="tag" IsVisible="{Binding IsConnected}"
|
||||||
|
Background="{StaticResource LiveWash}">
|
||||||
|
<TextBlock Text="live" Foreground="{StaticResource Live}" />
|
||||||
|
</Border>
|
||||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" IsVisible="{Binding !IsConnected}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -141,7 +165,7 @@
|
|||||||
-->
|
-->
|
||||||
<Border Grid.Row="3" IsVisible="{Binding SelectedHost, Converter={x:Static ObjectConverters.IsNotNull}}"
|
<Border Grid.Row="3" IsVisible="{Binding SelectedHost, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||||
BorderThickness="0,1,0,0" Padding="16,12">
|
BorderThickness="0,1,0,0" Padding="14,12">
|
||||||
<StackPanel Spacing="10">
|
<StackPanel Spacing="10">
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
@@ -153,7 +177,7 @@
|
|||||||
Shown only for a host that actually asks for one. A password box beside a key-authenticated host
|
Shown only for a host that actually asks for one. A password box beside a key-authenticated host
|
||||||
is an invitation to type a secret nothing will use.
|
is an invitation to type a secret nothing will use.
|
||||||
-->
|
-->
|
||||||
<TextBox Classes="field" Height="44" IsVisible="{Binding SelectedHostAsksForAPassword}"
|
<TextBox Classes="field" IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||||
Text="{Binding ConnectPassword}" PasswordChar="•" PlaceholderText="password">
|
Text="{Binding ConnectPassword}" PasswordChar="•" PlaceholderText="password">
|
||||||
<TextBox.KeyBindings>
|
<TextBox.KeyBindings>
|
||||||
<KeyBinding Gesture="Enter" Command="{Binding ConnectCommand}" />
|
<KeyBinding Gesture="Enter" Command="{Binding ConnectCommand}" />
|
||||||
|
|||||||
@@ -14,9 +14,21 @@
|
|||||||
And ◆ the delete confirmation replaces the row in place rather than stacking a dialogue over it — the
|
And ◆ the delete confirmation replaces the row in place rather than stacking a dialogue over it — the
|
||||||
design is explicit about that, and on a phone it matters more than on the desktop: a modal over a list
|
design is explicit about that, and on a phone it matters more than on the desktop: a modal over a list
|
||||||
hides which row it is talking about, which is exactly the information the confirmation exists to give.
|
hides which row it is talking about, which is exactly the information the confirmation exists to give.
|
||||||
|
|
||||||
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Rows became cards with a glyph tile, which is the design's own shape. Three things drawn on those cards
|
||||||
|
are not here, and all three for the same reason: there is nothing behind them. An `ED25519` badge needs
|
||||||
|
an algorithm field, and `SshKeySecret` has none — the type stores the armour verbatim rather than
|
||||||
|
parsing it. A `SHA256:…` line needs a fingerprint, computed by the same parsing. And an `agent` chip
|
||||||
|
needs an agent, of which this application has none at all — the connect path decrypts the key and hands
|
||||||
|
the bytes to the SSH library. What the card carries instead is `Detail`, which is what is genuinely
|
||||||
|
known *about* an item and never the secret. See docs/design-import-gaps.md.
|
||||||
|
|
||||||
|
Mode=OneWay on the chips is load-bearing: a two-way selection binding moves the chip before the command
|
||||||
|
beside it has had the chance to refuse, which would leave the row lit for a section the list is not on.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*">
|
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||||
|
|
||||||
<!-- ============ categories ============ -->
|
<!-- ============ categories ============ -->
|
||||||
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||||
@@ -60,15 +72,22 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The design's "4 keys · synced". SectionSummary counts the rows actually on screen and says how many of
|
||||||
|
them are still waiting to be pushed — which is the honest version of the design's unconditional
|
||||||
|
"synced".
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Row="1" Classes="detail" Margin="18,6,18,2" Text="{Binding SectionSummary}" />
|
||||||
|
|
||||||
<!-- ============ ◆ the counted confirmation ============ -->
|
<!-- ============ ◆ the counted confirmation ============ -->
|
||||||
<!--
|
<!--
|
||||||
In place, above the list it refers to. The text comes from the view model, which counts what breaks —
|
In place, above the list it refers to. The text comes from the view model, which counts what breaks —
|
||||||
"three hosts authenticate with this key and will refuse to connect" — and the count is the whole
|
"three hosts authenticate with this key and will refuse to connect" — and the count is the whole
|
||||||
difference between a sentence somebody reads and one they click past.
|
difference between a sentence somebody reads and one they click past.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="1" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
|
<Border Grid.Row="2" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
|
||||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
BorderThickness="1" CornerRadius="12" Padding="14,12">
|
||||||
<StackPanel Spacing="5">
|
<StackPanel Spacing="5">
|
||||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding PendingDeletion.Question}" TextWrapping="Wrap" />
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding PendingDeletion.Question}" TextWrapping="Wrap" />
|
||||||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||||||
@@ -87,7 +106,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ============ the items ============ -->
|
<!-- ============ the items ============ -->
|
||||||
<Panel Grid.Row="2">
|
<Panel Grid.Row="3">
|
||||||
|
|
||||||
<!-- Written per section by the view model, and it says what the section is for. -->
|
<!-- Written per section by the view model, and it says what the section is for. -->
|
||||||
<TextBlock Classes="body" IsVisible="{Binding !HasVaultItems}" Margin="24,20"
|
<TextBlock Classes="body" IsVisible="{Binding !HasVaultItems}" Margin="24,20"
|
||||||
@@ -103,27 +122,41 @@
|
|||||||
</ListBox.Styles>
|
</ListBox.Styles>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
||||||
<Grid ColumnDefinitions="*,Auto" MinHeight="56" Margin="16,6">
|
<Border Classes="card" Margin="12,4">
|
||||||
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
<Grid ColumnDefinitions="Auto,*,Auto" MinHeight="64">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
|
||||||
<TextBlock Classes="mono" FontSize="13" FontWeight="Medium" Text="{Binding Name}"
|
<!--
|
||||||
TextTrimming="CharacterEllipsis" />
|
The glyph tile. One mark for all three kinds rather than three: a key, a password and a
|
||||||
<Border BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="3"
|
bucket are already named by the badge beside them, and three near-identical monochrome
|
||||||
Padding="5,1" VerticalAlignment="Center">
|
glyphs at 14px would be decoration that reads as a distinction.
|
||||||
<TextBlock Classes="detail" FontSize="8" Text="{Binding Type}" />
|
-->
|
||||||
|
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="10"
|
||||||
|
Background="{StaticResource Active}" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="⚿" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||||
|
FontWeight="SemiBold" HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
<Border IsVisible="{Binding HasBadge}" BorderBrush="{StaticResource WarnSoft}"
|
|
||||||
BorderThickness="1" CornerRadius="3" Padding="5,1" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Margin="12,0" Spacing="3" VerticalAlignment="Center">
|
||||||
<TextBlock Classes="detail" FontSize="8" Foreground="{StaticResource Warn}"
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
Text="{Binding Badge}" />
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
<Border Classes="tag outline">
|
||||||
|
<TextBlock Text="{Binding Type}" />
|
||||||
|
</Border>
|
||||||
|
<Border Classes="tag outline" BorderBrush="{StaticResource WarnSoft}"
|
||||||
|
IsVisible="{Binding HasBadge}">
|
||||||
|
<TextBlock Foreground="{StaticResource Warn}" Text="{Binding Badge}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<!-- What is known *about* an item, never the secret. -->
|
<!-- What is known *about* an item, never the secret. -->
|
||||||
<TextBlock Classes="detail" Text="{Binding Detail}" TextTrimming="CharacterEllipsis" />
|
<TextBlock Classes="detail" Text="{Binding Detail}" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
|
||||||
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<Button Content="UNLOCK" Command="{Binding UnlockCommand}" IsEnabled="{Binding !IsBusy}"
|
<Button Content="UNLOCK" Command="{Binding UnlockCommand}" IsEnabled="{Binding !IsBusy}"
|
||||||
Height="48" Margin="0,10,0,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
Height="48" Margin="0,10,0,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||||
Background="{StaticResource Accent}" Foreground="{StaticResource Canvas}"
|
Background="{StaticResource Accent}" Foreground="{StaticResource AccentInk}"
|
||||||
CornerRadius="6" FontFamily="{StaticResource MonoFont}" FontSize="12" FontWeight="SemiBold" />
|
CornerRadius="6" FontFamily="{StaticResource MonoFont}" FontSize="12" FontWeight="SemiBold" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -99,7 +99,9 @@
|
|||||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<Ellipse Width="6" Height="6" Fill="{StaticResource Accent}" VerticalAlignment="Center" />
|
<!-- Live, not Accent: this marks shells that are still running, which is a fact and not a
|
||||||
|
thing to press. It read as green until v2 turned the accent blue. -->
|
||||||
|
<Ellipse Width="6" Height="6" Fill="{StaticResource Live}" VerticalAlignment="Center" />
|
||||||
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Warn}"
|
<TextBlock Text="{Binding LiveSessionSummary}" Foreground="{StaticResource Warn}"
|
||||||
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="SemiBold"
|
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="SemiBold"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<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.LogsScreen"
|
||||||
|
x:DataType="vm:LogsViewModel"
|
||||||
|
Background="{StaticResource Canvas}">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Design 07 — LOGS, under MORE.
|
||||||
|
|
||||||
|
Three things on the mock-up are not drawn, and each is the same kind of omission.
|
||||||
|
|
||||||
|
◆ **FOLLOW.** The design's header carries a blinking green "FOLLOW" pill and the screen is described as
|
||||||
|
a live tail over ssh. Nothing here tails anything: these are the keychain's own synced audit records,
|
||||||
|
written once at close, and they are read when the screen is opened. A pill that blinked would be
|
||||||
|
animation standing in for a feature. It is a REFRESH button, which is what actually happens.
|
||||||
|
|
||||||
|
**The severity filter** — ALL / INFO / WARN / ERROR with counts — has nothing to count. A connection
|
||||||
|
record is not a syslog line and carries no level. What this screen genuinely has two of is *logs*, so
|
||||||
|
the design's chip row is spent on the real choice: what was connected to, and what was changed.
|
||||||
|
|
||||||
|
**The host chips** are gone for the same reason: nothing filters either log by host.
|
||||||
|
|
||||||
|
What is kept is the design's row shape — a quiet timestamp, a coloured word, a source, and the fact
|
||||||
|
underneath — because that shape is why a log is readable at 10pt on a phone.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||||
|
|
||||||
|
<!-- ============ header ============ -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="56" Margin="8,0">
|
||||||
|
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||||
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="heading" Text="Logs" Margin="4,0" />
|
||||||
|
<Button Grid.Column="2" Classes="icon" Content="↻" Command="{Binding RefreshCommand}"
|
||||||
|
IsEnabled="{Binding !IsBusy}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ============ which log ============ -->
|
||||||
|
<!--
|
||||||
|
Mode=OneWay is load-bearing on both of these, not a tidiness: a two-way selection binding moves the
|
||||||
|
chip before the command it is paired with has had a chance to refuse, so the screen would show a
|
||||||
|
section it is not on. The keychain's categories carry the same note.
|
||||||
|
-->
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="6" Margin="14,0,14,4">
|
||||||
|
<RadioButton GroupName="log" Classes="chip" IsChecked="{Binding ShowsConnections, Mode=OneWay}"
|
||||||
|
Command="{Binding ShowSectionCommand}"
|
||||||
|
CommandParameter="{x:Static vm:LogSection.Connections}"
|
||||||
|
Content="CONNECTIONS" />
|
||||||
|
<RadioButton GroupName="log" Classes="chip" IsChecked="{Binding ShowsActivity, Mode=OneWay}"
|
||||||
|
Command="{Binding ShowSectionCommand}"
|
||||||
|
CommandParameter="{x:Static vm:LogSection.Activity}"
|
||||||
|
Content="KEYCHAIN" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Classes="detail" Margin="18,4,18,6" TextWrapping="Wrap"
|
||||||
|
Text="{Binding Status}" />
|
||||||
|
|
||||||
|
<!-- ============ the log ============ -->
|
||||||
|
<Border Grid.Row="3" Classes="output" Margin="12,0,12,12">
|
||||||
|
<Panel>
|
||||||
|
|
||||||
|
<!-- ============ what was connected to ============ -->
|
||||||
|
<!--
|
||||||
|
One wrapper per section, each holding its own empty state, because "this section is empty" is a
|
||||||
|
pair of facts — which section, and whether it has rows — and a binding cannot say `ShowsConnections
|
||||||
|
&& !HasConnections` without a converter. Two Panels are cheaper than a converter and they keep
|
||||||
|
EmptyMessage, which is already written per section, pointing at the section it describes.
|
||||||
|
-->
|
||||||
|
<Panel IsVisible="{Binding ShowsConnections}">
|
||||||
|
<TextBlock Classes="body" VerticalAlignment="Top" Text="{Binding EmptyMessage}"
|
||||||
|
IsVisible="{Binding !HasConnections}" />
|
||||||
|
|
||||||
|
<ScrollViewer IsVisible="{Binding HasConnections}">
|
||||||
|
<ItemsControl ItemsSource="{Binding Connections}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
|
||||||
|
<StackPanel Spacing="1" Margin="0,5">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBlock Classes="detail" FontSize="10" Text="{Binding Started}"
|
||||||
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The kind sits where the design puts a severity, because it is the word that tells you
|
||||||
|
what the row is: a shell or a file transfer. Green while the session is still open —
|
||||||
|
the same green the host list uses, and the one thing on this screen that is about now
|
||||||
|
rather than about the past.
|
||||||
|
-->
|
||||||
|
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold" Text="{Binding Kind}"
|
||||||
|
Foreground="{StaticResource Info}" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding !IsLive}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5" IsVisible="{Binding IsLive}"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Ellipse Classes="dot live" Width="6" Height="6" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Text="{Binding Kind}" Foreground="{StaticResource Live}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Classes="detail" FontSize="10" Text="{Binding HostLabel}"
|
||||||
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
||||||
|
Text="{Binding Address}" TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TextDim rather than the `detail` class's own faint step. This block is drawn on
|
||||||
|
TerminalSurface, which is lighter than the Canvas that step was chosen against, so the
|
||||||
|
same colour that reads as quiet on a screen background reads as barely there here.
|
||||||
|
-->
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
|
<!-- "still open" rather than a dash, which the plan calls out by name. -->
|
||||||
|
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||||
|
Text="{Binding Duration}" />
|
||||||
|
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||||
|
Text="·" />
|
||||||
|
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
|
||||||
|
Text="{Binding DeviceName}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- A refused host key is the one outcome worth a colour of its own. -->
|
||||||
|
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasOutcome}"
|
||||||
|
Foreground="{StaticResource DangerText}" Text="{Binding Outcome}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- ============ what was changed ============ -->
|
||||||
|
<Panel IsVisible="{Binding ShowsActivity}">
|
||||||
|
<TextBlock Classes="body" VerticalAlignment="Top" Text="{Binding EmptyMessage}"
|
||||||
|
IsVisible="{Binding !HasActivity}" />
|
||||||
|
|
||||||
|
<ScrollViewer IsVisible="{Binding HasActivity}">
|
||||||
|
<ItemsControl ItemsSource="{Binding Activity}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
|
||||||
|
<StackPanel Spacing="1" Margin="0,5">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<TextBlock Classes="detail" FontSize="10" Text="{Binding At}"
|
||||||
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Text="{Binding Operation}" Foreground="{StaticResource Info}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock Classes="detail" FontSize="10" Text="{Binding ItemKind}"
|
||||||
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
|
||||||
|
Text="{Binding ItemLabel}" TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
|
<!-- The names of the fields that changed, never their values. -->
|
||||||
|
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasChangedFields}"
|
||||||
|
Foreground="{StaticResource TextDim}"
|
||||||
|
Text="{Binding ChangedFields}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Android.Views;
|
||||||
|
|
||||||
|
/// <summary>Design 07 — the two logs: what has been connected to, and what has been changed.</summary>
|
||||||
|
internal sealed partial class LogsScreen : UserControl
|
||||||
|
{
|
||||||
|
public LogsScreen() => AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<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.MoreScreen"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Background="{StaticResource Canvas}">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Design v2 — MORE: the hub for everything the bottom bar has no room for.
|
||||||
|
|
||||||
|
Four slots and nine destinations is the arithmetic the design solves by putting five of them one tap
|
||||||
|
deeper. This screen is that tap. It takes the shell as its data context rather than the vault, because
|
||||||
|
every row on it is a navigation command and nothing here reads an item.
|
||||||
|
|
||||||
|
The rows are the design's list rows rather than cards: a card is one thing you act on, and a destination
|
||||||
|
is not a thing — it is a place. Each carries a sentence saying what is behind it, because a hub whose
|
||||||
|
entries are one word each is a menu you have to open to read.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
|
||||||
|
<Border Grid.Row="0" Height="56" Padding="16,0">
|
||||||
|
<TextBlock Classes="heading" Text="More" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<StackPanel Margin="10,0,10,20" Spacing="2">
|
||||||
|
|
||||||
|
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.Snippets}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="❯" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||||
|
Width="22" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Snippets" />
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" Text="Saved commands, typed into an open shell." />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
These two go through ShowFiles rather than ShowScreen: they are one screen over one view model,
|
||||||
|
and which kind of remote it offers is the thing being chosen. See ShowFilesCommand for why that
|
||||||
|
is set by the button rather than on arrival.
|
||||||
|
-->
|
||||||
|
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||||
|
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="⇅" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||||
|
Width="22" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="SFTP" />
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||||
|
Text="Browse a host's files." />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||||
|
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="◳" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||||
|
Width="22" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="S3" />
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||||
|
Text="Objects in an S3-compatible bucket from the keychain." />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.Logs}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="≡" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||||
|
Width="22" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Logs" />
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" Text="What has been connected to, and what has changed." />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.Preferences}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="⚙" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||||
|
Width="22" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Preferences" />
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" Text="What this phone can and cannot do, and why." />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Locking, here as well as in the header.
|
||||||
|
|
||||||
|
The header carries it on the three top-level destinations and stands down behind MORE, so without
|
||||||
|
this row the five screens under here would each be two taps from locking the keychain. That is the
|
||||||
|
wrong direction for the one action whose whole value is being quick, and it is why this is a row
|
||||||
|
on the hub rather than something you go back for.
|
||||||
|
-->
|
||||||
|
<Button Classes="row" Margin="0,10,0,0" Command="{Binding LockCommand}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*">
|
||||||
|
<TextBlock Grid.Column="0" Text="⚿" Foreground="{StaticResource TextDim}" FontSize="14"
|
||||||
|
Width="22" VerticalAlignment="Center" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Lock the keychain" />
|
||||||
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||||
|
Text="Shells you have open keep running." />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ Port forwarding, stated rather than drawn.
|
||||||
|
|
||||||
|
The v2 design gives this hub a sixth entry and a full screen behind it: local, remote and dynamic
|
||||||
|
rules with toggles and byte counters. Nothing in this application forwards anything — ISshConnection
|
||||||
|
offers OpenShellAsync and nothing else — so every switch on that screen would be a control with no
|
||||||
|
effect and every counter a number with no source.
|
||||||
|
|
||||||
|
It is a paragraph here rather than a missing row for the reason the desktop keeps TEAMS in its rail:
|
||||||
|
a destination that is quietly absent reads as a product that never intended it, and this one is
|
||||||
|
intended. SyncEntityType.PortForward = 9 is reserved on the wire and unused. See
|
||||||
|
docs/design-import-gaps.md.
|
||||||
|
-->
|
||||||
|
<Border Classes="card" Margin="6,16,6,0">
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="9">
|
||||||
|
<TextBlock Text="⇄" Foreground="{StaticResource TextFaint}" FontSize="14"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource TextDim}" Text="Port forwarding" />
|
||||||
|
<Border Classes="tag outline" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="NOT BUILT" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Classes="body"
|
||||||
|
Text="A connection here opens one channel — a shell — and nothing else, so there is no tunnel for a rule to run through. The wire protocol reserves a slot for forwarding rules and no client writes one. Nothing on this screen is hidden by a setting; the feature is absent." />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Android.Views;
|
||||||
|
|
||||||
|
/// <summary>Design v2 — MORE, the hub for the five destinations the bottom bar has no room for.</summary>
|
||||||
|
internal sealed partial class MoreScreen : UserControl
|
||||||
|
{
|
||||||
|
public MoreScreen() => AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
@@ -17,13 +17,21 @@ namespace DodoSSH.Client.Android.Views;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Written in C# rather than as a XAML control because it is a button with a label and a rule about the
|
/// Written in C# rather than as a XAML control because it is a button with a glyph, a label and a rule about
|
||||||
/// accent bar, and a templated control for that would be more machinery than the thing it draws.
|
/// the accent bar, and a templated control for that would be more machinery than the thing it draws.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>The accent bar is on top, not underneath.</b> The design puts it there in the nav rail and it is
|
/// <b>The accent bar is on top, not underneath.</b> The v2 design distinguishes the current destination with
|
||||||
/// worth keeping on a phone for a reason the desktop does not have: the bottom of a phone screen is under
|
/// colour and weight alone and draws no bar at all; the bar is kept, and kept at the top, for a reason the
|
||||||
/// the system's own gesture bar, and an indicator drawn there is one the user cannot see.
|
/// design does not have to think about. The bottom of a phone screen is under the system's own gesture bar,
|
||||||
|
/// so an indicator drawn there is one the user cannot see — and colour alone at 9pt on a dark background is
|
||||||
|
/// the single signal a user with a colour deficiency would be left with. Weight, colour and the bar are
|
||||||
|
/// three, which is the right number for the control that says where you are.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Every colour here is resolved from <c>Theme/Palette.axaml</c> rather than written out. A nav bar that
|
||||||
|
/// named its own blues is exactly how the fourth place a colour appears ends up one shade off — which is the
|
||||||
|
/// argument the desktop head's <c>Ellipse.dot</c> style makes for being a style rather than a converter.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal sealed class NavButton : Button
|
internal sealed class NavButton : Button
|
||||||
@@ -31,12 +39,26 @@ internal sealed class NavButton : Button
|
|||||||
public static readonly StyledProperty<string> LabelProperty =
|
public static readonly StyledProperty<string> LabelProperty =
|
||||||
AvaloniaProperty.Register<NavButton, string>(nameof(Label), string.Empty);
|
AvaloniaProperty.Register<NavButton, string>(nameof(Label), string.Empty);
|
||||||
|
|
||||||
|
/// <summary>The glyph drawn above the label.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A character rather than an icon asset. The four the design uses — ▤ ⌗ ⚿ ≣ — are all in the fonts
|
||||||
|
/// Android already has, so shipping four vectors to draw them would buy nothing but four more files to
|
||||||
|
/// keep in step with the palette.
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly StyledProperty<string> GlyphProperty =
|
||||||
|
AvaloniaProperty.Register<NavButton, string>(nameof(Glyph), string.Empty);
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> IsCurrentProperty =
|
public static readonly StyledProperty<bool> IsCurrentProperty =
|
||||||
AvaloniaProperty.Register<NavButton, bool>(nameof(IsCurrent));
|
AvaloniaProperty.Register<NavButton, bool>(nameof(IsCurrent));
|
||||||
|
|
||||||
private readonly Border indicator;
|
private readonly Border indicator;
|
||||||
|
private readonly TextBlock glyph;
|
||||||
private readonly TextBlock text;
|
private readonly TextBlock text;
|
||||||
|
|
||||||
|
// Resolved from the palette once the control is in the tree; see Repaint.
|
||||||
|
private IBrush? currentBrush;
|
||||||
|
private IBrush? idleBrush;
|
||||||
|
|
||||||
public NavButton()
|
public NavButton()
|
||||||
{
|
{
|
||||||
Background = Brushes.Transparent;
|
Background = Brushes.Transparent;
|
||||||
@@ -55,16 +77,28 @@ internal sealed class NavButton : Button
|
|||||||
Background = Brushes.Transparent,
|
Background = Brushes.Transparent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
glyph = new TextBlock
|
||||||
|
{
|
||||||
|
FontSize = 15,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
text = new TextBlock
|
text = new TextBlock
|
||||||
{
|
{
|
||||||
FontSize = 9,
|
FontSize = 9,
|
||||||
FontWeight = FontWeight.SemiBold,
|
FontWeight = FontWeight.Medium,
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Foreground = new SolidColorBrush(Color.Parse("#566059")),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Content = new Panel { Children = { indicator, text } };
|
var stack = new StackPanel
|
||||||
|
{
|
||||||
|
Spacing = 3,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Children = { glyph, text },
|
||||||
|
};
|
||||||
|
|
||||||
|
Content = new Panel { Children = { indicator, stack } };
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Label
|
public string Label
|
||||||
@@ -73,6 +107,13 @@ internal sealed class NavButton : Button
|
|||||||
set => SetValue(LabelProperty, value);
|
set => SetValue(LabelProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="GlyphProperty" />
|
||||||
|
public string Glyph
|
||||||
|
{
|
||||||
|
get => GetValue(GlyphProperty);
|
||||||
|
set => SetValue(GlyphProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Whether this is the destination currently showing.</summary>
|
/// <summary>Whether this is the destination currently showing.</summary>
|
||||||
public bool IsCurrent
|
public bool IsCurrent
|
||||||
{
|
{
|
||||||
@@ -89,14 +130,13 @@ internal sealed class NavButton : Button
|
|||||||
{
|
{
|
||||||
text.Text = Label;
|
text.Text = Label;
|
||||||
}
|
}
|
||||||
|
else if (change.Property == GlyphProperty)
|
||||||
|
{
|
||||||
|
glyph.Text = Glyph;
|
||||||
|
}
|
||||||
else if (change.Property == IsCurrentProperty)
|
else if (change.Property == IsCurrentProperty)
|
||||||
{
|
{
|
||||||
// Both the label and the bar, not just the bar. Colour alone at 9pt on a dark background is a
|
Repaint();
|
||||||
// weak signal, and it is the only one a user with a colour deficiency would have.
|
|
||||||
text.Foreground = new SolidColorBrush(Color.Parse(IsCurrent ? "#3CE88F" : "#566059"));
|
|
||||||
indicator.Background = IsCurrent
|
|
||||||
? new SolidColorBrush(Color.Parse("#3CE88F"))
|
|
||||||
: Brushes.Transparent;
|
|
||||||
}
|
}
|
||||||
else if (change.Property == FontFamilyProperty)
|
else if (change.Property == FontFamilyProperty)
|
||||||
{
|
{
|
||||||
@@ -115,5 +155,27 @@ internal sealed class NavButton : Button
|
|||||||
{
|
{
|
||||||
text.FontFamily = mono;
|
text.FontFamily = mono;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentBrush = Application.Current?.FindResource("AccentText") as IBrush;
|
||||||
|
idleBrush = Application.Current?.FindResource("TextFaint") as IBrush;
|
||||||
|
|
||||||
|
// IsCurrent is normally bound and so has usually already been set by the time the template applies,
|
||||||
|
// which is why this runs again here rather than only on the property change.
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Repaint()
|
||||||
|
{
|
||||||
|
// Both the label and the bar, not just the bar — and the weight as well. See the remark on the type.
|
||||||
|
var paint = IsCurrent ? currentBrush : idleBrush;
|
||||||
|
|
||||||
|
if (paint is not null)
|
||||||
|
{
|
||||||
|
glyph.Foreground = paint;
|
||||||
|
text.Foreground = paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
text.FontWeight = IsCurrent ? FontWeight.SemiBold : FontWeight.Medium;
|
||||||
|
indicator.Background = IsCurrent && currentBrush is not null ? currentBrush : Brushes.Transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,14 @@
|
|||||||
It switches on ShellState, which is the same six states the desktop has because both heads share one
|
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.
|
state machine. What differs is only what each one draws.
|
||||||
|
|
||||||
The nav rail's eight destinations become four in a bottom bar. That is not a reflow: the plan says five
|
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
characters was a desktop constraint and the phone can use words, and it also says a phone cannot carry
|
The desktop's eight rail destinations become four in a bottom bar, and five more live one tap deeper
|
||||||
eight. What is here is what the decided scope needs — the keychain and a terminal — and the destinations
|
behind MORE: snippets, SFTP, S3, logs and preferences. That is the v2 design's own arrangement, and it
|
||||||
behind the other four are reachable from those two rather than being hidden.
|
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.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Panel>
|
<Panel>
|
||||||
@@ -34,29 +38,47 @@
|
|||||||
<!-- ============ unlocked ============ -->
|
<!-- ============ unlocked ============ -->
|
||||||
<Grid IsVisible="{Binding IsUnlocked}" RowDefinitions="Auto,*,Auto,Auto">
|
<Grid IsVisible="{Binding IsUnlocked}" RowDefinitions="Auto,*,Auto,Auto">
|
||||||
|
|
||||||
<!-- The header: which vault, and whether it is synced. -->
|
<!--
|
||||||
|
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 five hub screens draw their own and this one stands down rather than stacking a second row of
|
||||||
|
chrome above theirs. It stays on the three 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.
|
||||||
|
-->
|
||||||
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||||
BorderThickness="0,0,0,1" Padding="16,0" Height="52">
|
BorderThickness="0,0,0,1" Padding="14,0" Height="56" IsVisible="{Binding !IsMoreSurface}">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
<Border Grid.Column="0" Width="20" Height="20" BorderBrush="{StaticResource Accent}"
|
|
||||||
BorderThickness="1" VerticalAlignment="Center">
|
<!--
|
||||||
<TextBlock Text=">_" Foreground="{StaticResource Accent}"
|
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
|
||||||
FontFamily="{StaticResource MonoFont}" FontSize="9" FontWeight="SemiBold"
|
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" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center" Spacing="1">
|
<TextBlock Grid.Column="1" Classes="heading" Margin="10,0,8,0" FontSize="16"
|
||||||
<TextBlock Classes="mono" FontSize="12" FontWeight="SemiBold"
|
|
||||||
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
|
||||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="5" Height="5"
|
<!--
|
||||||
|
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" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock Classes="label" FontSize="8" Text="{Binding SyncLabel}" />
|
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Column="2" Classes="row" Width="44" MinHeight="44" Padding="0"
|
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
||||||
HorizontalContentAlignment="Center" Command="{Binding LockCommand}">
|
ToolTip.Tip="Lock the keychain">
|
||||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5" Foreground="{StaticResource TextDim}" />
|
<TextBlock Text="LOCK" Classes="label" FontSize="8.5" Foreground="{StaticResource TextDim}" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -85,15 +107,60 @@
|
|||||||
-->
|
-->
|
||||||
<views:TerminalScreen IsVisible="{Binding IsTerminalSurface}" />
|
<views:TerminalScreen IsVisible="{Binding IsTerminalSurface}" />
|
||||||
|
|
||||||
<views:PendingScreen IsVisible="{Binding IsPreferencesShowing}"
|
<!--
|
||||||
|
============ under MORE ============
|
||||||
|
|
||||||
|
The hub itself takes the shell as its data context, because every row on it is a navigation
|
||||||
|
command; the five 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>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
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"
|
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." />
|
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>
|
</Panel>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
◆ The two host-key decisions, over everything. Outside the screen Panel deliberately: file transfer
|
◆ The two host-key decisions, over everything. Outside the screen Panel deliberately: file transfer
|
||||||
makes its own trust decision on the desktop, and when the phone grows that screen this must already
|
makes its own trust decision on the desktop, and the phone now has that screen — so a sheet nested
|
||||||
be somewhere both can reach rather than nested inside one of them.
|
inside the hosts screen would be one the files screen could not raise.
|
||||||
-->
|
-->
|
||||||
<Panel Grid.Row="1">
|
<Panel Grid.Row="1">
|
||||||
<views:HostKeySheet DataContext="{Binding Vault}" />
|
<views:HostKeySheet DataContext="{Binding Vault}" />
|
||||||
@@ -104,64 +171,78 @@
|
|||||||
desktop-shaped idea in the product — it is what makes a terminal a surface you switch to rather than
|
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
|
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.
|
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.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="2" IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
<Border Grid.Row="2" IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="12,0" VerticalAlignment="Center">
|
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
||||||
<TextBlock Classes="label" FontSize="8" Text="SHELLS" VerticalAlignment="Center" />
|
|
||||||
<ItemsControl ItemsSource="{Binding Tabs}">
|
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||||
<Button Classes="row" MinHeight="32" Padding="10,6" CornerRadius="5"
|
<Button Classes="row" MinHeight="34" Padding="13,0" CornerRadius="9"
|
||||||
Background="{StaticResource Field}"
|
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||||
|
BorderThickness="1"
|
||||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||||
<!--
|
<!--
|
||||||
Green only while there is a shell behind the tab. It used to be lit unconditionally,
|
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
|
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
|
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.
|
the one thing on this strip claiming something untrue.
|
||||||
-->
|
-->
|
||||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="5" Height="5"
|
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock Classes="mono" FontSize="10" Text="{Binding Label}" />
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ============ navigation ============ -->
|
<!-- ============ navigation ============ -->
|
||||||
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||||
BorderThickness="0,1,0,0" Height="58">
|
BorderThickness="0,1,0,0" Height="64">
|
||||||
<Grid ColumnDefinitions="*,*,*,*">
|
<Grid ColumnDefinitions="*,*,*,*">
|
||||||
|
|
||||||
<views:NavButton Grid.Column="0" Label="HOSTS" IsCurrent="{Binding IsHostsShowing}"
|
<views:NavButton Grid.Column="0" Label="Hosts" Glyph="▤" IsCurrent="{Binding IsHostsShowing}"
|
||||||
Command="{Binding ShowScreenCommand}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
|
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
|
||||||
|
|
||||||
<views:NavButton Grid.Column="1" Label="KEYCHAIN" IsCurrent="{Binding IsVaultShowing}"
|
|
||||||
Command="{Binding ShowScreenCommand}"
|
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Vault}" />
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
|
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.
|
through ShowScreen. Its own command is on the shell.
|
||||||
-->
|
-->
|
||||||
<views:NavButton Grid.Column="2" Label="TERMINAL" IsCurrent="{Binding IsTerminalSurface}"
|
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗" IsCurrent="{Binding IsTerminalSurface}"
|
||||||
Command="{Binding ShowTerminalCommand}" />
|
Command="{Binding ShowTerminalCommand}" />
|
||||||
|
|
||||||
<views:NavButton Grid.Column="3" Label="MORE" IsCurrent="{Binding IsPreferencesShowing}"
|
<!--
|
||||||
|
"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}"
|
Command="{Binding ShowScreenCommand}"
|
||||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}" />
|
CommandParameter="{x:Static vm:ShellScreen.Vault}" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
IsMoreSurface rather than IsMoreShowing: this tab stands for six screens, 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>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using global::Android.Views;
|
using global::Android.Views;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
using DodoSSH.Client.Android.Platform;
|
using DodoSSH.Client.Android.Platform;
|
||||||
@@ -13,6 +15,19 @@ internal sealed partial class PhoneShell : UserControl
|
|||||||
{
|
{
|
||||||
private MainWindowViewModel? shell;
|
private MainWindowViewModel? shell;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the lock screen currently showing is the one the application launched into.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set once, when the shell arrives, because this control is built once for the process. Cleared the
|
||||||
|
/// moment the state leaves <see cref="ShellState.Locked"/>, and never set again — which is what stops a
|
||||||
|
/// deliberate lock from being answered with an immediate request to unlock. See
|
||||||
|
/// <see cref="TryOfferDeviceUnlock"/>.
|
||||||
|
/// </remarks>
|
||||||
|
private bool thisLockIsTheLaunch;
|
||||||
|
|
||||||
|
private bool offeredDeviceUnlock;
|
||||||
|
|
||||||
public PhoneShell()
|
public PhoneShell()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
@@ -30,15 +45,167 @@ internal sealed partial class PhoneShell : UserControl
|
|||||||
{
|
{
|
||||||
shell.PropertyChanged += OnShellChanged;
|
shell.PropertyChanged += OnShellChanged;
|
||||||
ApplyScreenshotPolicy(shell.State);
|
ApplyScreenshotPolicy(shell.State);
|
||||||
|
|
||||||
|
thisLockIsTheLaunch = true;
|
||||||
|
TryOfferDeviceUnlock();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnShellChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
private void OnShellChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (shell is not null && e.PropertyName is nameof(MainWindowViewModel.State))
|
if (shell is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.PropertyName is nameof(MainWindowViewModel.State))
|
||||||
{
|
{
|
||||||
ApplyScreenshotPolicy(shell.State);
|
ApplyScreenshotPolicy(shell.State);
|
||||||
|
|
||||||
|
if (shell.State is not ShellState.Locked)
|
||||||
|
{
|
||||||
|
thisLockIsTheLaunch = false;
|
||||||
|
offeredDeviceUnlock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Three properties rather than one, because the condition is assembled out of order. Startup sets
|
||||||
|
// State to Locked and only then awaits the keystore to decide CanUnlockWithDevice, and it does all
|
||||||
|
// of it inside the busy wrapper — which refuses a second command outright. So whichever of the
|
||||||
|
// three settles last is the one that has to ask again.
|
||||||
|
if (e.PropertyName is nameof(MainWindowViewModel.State)
|
||||||
|
or nameof(MainWindowViewModel.CanUnlockWithDevice)
|
||||||
|
or nameof(MainWindowViewModel.IsBusy))
|
||||||
|
{
|
||||||
|
TryOfferDeviceUnlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raises the fingerprint prompt on arriving at the lock screen, rather than waiting to be asked.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The button is still there and still says what it does; this only spends the tap for you. On a phone
|
||||||
|
/// that is the difference between opening the application in one gesture and in two, and the second of
|
||||||
|
/// the two was a button whose entire content was "yes, do the thing you already know I want".
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Only at launch.</b> A lock the user asked for is not answered with a request to unlock — that
|
||||||
|
/// turns LOCK into a control that appears to do nothing, and worse, trains the reflex of authenticating
|
||||||
|
/// at a prompt that appeared without being asked for. So the offer belongs to the locked screen the
|
||||||
|
/// process started on and to no other.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Once.</b> A declined gesture leaves the passphrase box exactly where it was, which is the whole
|
||||||
|
/// fallback — and a prompt that reappeared after being dismissed would be a modal the user cannot get
|
||||||
|
/// out of to type into it.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Nothing here needs a failure path. <c>UnlockWithDeviceAsync</c> turns every refusal into a status
|
||||||
|
/// line, and a phone with no enrolled fingerprint never gets here at all, because
|
||||||
|
/// <c>CanUnlockWithDevice</c> already asked the keystore.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private void TryOfferDeviceUnlock()
|
||||||
|
{
|
||||||
|
if (!thisLockIsTheLaunch || offeredDeviceUnlock)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shell is not { State: ShellState.Locked, CanUnlockWithDevice: true, IsBusy: false } current)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
offeredDeviceUnlock = true;
|
||||||
|
current.UnlockWithDeviceCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
|
||||||
|
if (TopLevel.GetTopLevel(this) is { } top)
|
||||||
|
{
|
||||||
|
top.BackRequested += OnBackRequested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
if (TopLevel.GetTopLevel(this) is { } top)
|
||||||
|
{
|
||||||
|
top.BackRequested -= OnBackRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnDetachedFromVisualTree(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Takes the system back gesture up the hierarchy rather than out of the application.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// v2 is the first arrangement here with a second level: five destinations sit behind MORE, each with
|
||||||
|
/// its own back arrow. Android's back is the same gesture as that arrow and users reach for it first,
|
||||||
|
/// and left unhandled it does not go up — it finishes the activity. Ending the application from a log
|
||||||
|
/// screen is not a plausible reading of "back".
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Handled in the order the interface is stacked, not by screen alone: the terminal is a surface over a
|
||||||
|
/// page, so it is dismissed before the page under it is considered. Setting <c>Handled</c> is what stops
|
||||||
|
/// the event being re-dispatched to the activity's own default.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>Two things it deliberately does not do.</b> It does not answer a host-key prompt — those are
|
||||||
|
/// decisions with two named buttons, and a gesture that dismissed one would be the swipe-to-dismiss this
|
||||||
|
/// head refused when it made the changed-key refusal a full-screen panel rather than a sheet. And from
|
||||||
|
/// the host list it does nothing at all, so back still leaves the application from the screen the
|
||||||
|
/// application opens on, which is what every other Android app does.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
private void OnBackRequested(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (shell is not { State: ShellState.Unlocked } current)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A decision is on screen. Leave it alone — see the remark.
|
||||||
|
if (current.Vault is { HasPendingHostKey: true } or { HasHostKeyMismatch: true }
|
||||||
|
|| current.Transfers is { HasPendingHostKey: true } or { HasHostKeyMismatch: true })
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current.IsShowingPages)
|
||||||
|
{
|
||||||
|
current.ShowScreenCommand.Execute(current.Screen);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (current.Screen)
|
||||||
|
{
|
||||||
|
case ShellScreen.Snippets or ShellScreen.Logs or ShellScreen.Transfers
|
||||||
|
or ShellScreen.Buckets or ShellScreen.Preferences:
|
||||||
|
current.ShowScreenCommand.Execute(ShellScreen.More);
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ShellScreen.More or ShellScreen.Vault:
|
||||||
|
current.ShowScreenCommand.Execute(ShellScreen.Hosts);
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Hosts, and anything the phone does not draw. Left unhandled, so back leaves the app.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<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.SnippetsScreen"
|
||||||
|
x:DataType="vm:SnippetsViewModel"
|
||||||
|
Background="{StaticResource Canvas}">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Design 05 — SNIPPETS, under MORE.
|
||||||
|
|
||||||
|
Two departures from the mock-up, both because of what a snippet actually is here.
|
||||||
|
|
||||||
|
The design puts a ▶ Run button on every card. There are two operations rather than one — type the
|
||||||
|
command at the prompt, and type it *and* press Enter — and which of them a snippet allows is a property
|
||||||
|
of the snippet, set by a checkbox in its editor. ◆ That checkbox is the whole safety property: a
|
||||||
|
snippet that does not run on insert leaves the command sitting at the prompt for a human to read before
|
||||||
|
committing to it. Collapsing the two into one button called Run would throw that away, so the actions
|
||||||
|
are a bar over the bottom of the list, they name the terminal they will type into, and RUN only appears
|
||||||
|
for a snippet that is marked as running.
|
||||||
|
|
||||||
|
And the design's per-card footer — the hosts it ran on, the time, `exit 0` — is not drawn. Nothing here
|
||||||
|
records where a snippet was run, when, or what it returned; a snippet is a saved string, and the shell
|
||||||
|
it is typed into never reports back. See docs/design-import-gaps.md.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||||
|
|
||||||
|
<!-- ============ header ============ -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="56" Margin="8,0">
|
||||||
|
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||||
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||||
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||||
|
<TextBlock Grid.Column="1" Classes="heading" Text="Snippets" Margin="4,0" />
|
||||||
|
<Button Grid.Column="2" Classes="icon accent" Content="+" Command="{Binding NewCommand}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ============ filter ============ -->
|
||||||
|
<!-- Matches the command text as well as the name: the workflow is "the one that vacuums", not its label. -->
|
||||||
|
<TextBox Grid.Row="1" Classes="field" Margin="14,2,14,8" Text="{Binding Filter}"
|
||||||
|
PlaceholderText="search snippets" IsVisible="{Binding HasSnippets}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Classes="detail" Margin="18,0,18,6" TextWrapping="Wrap"
|
||||||
|
Text="{Binding Status}" />
|
||||||
|
|
||||||
|
<!-- ============ the editor ============ -->
|
||||||
|
<!--
|
||||||
|
In place, above the list, rather than as a screen of its own — the same decision the keychain's
|
||||||
|
delete confirmation makes and for the same reason: a form stacked over the list hides what it is
|
||||||
|
about.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="3" Classes="card" Margin="12,0" IsVisible="{Binding IsEditing}"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Classes="label" Text="NEW SNIPPET" IsVisible="{Binding IsCreating}" />
|
||||||
|
<TextBlock Classes="label" Text="EDIT SNIPPET" IsVisible="{Binding !IsCreating}" />
|
||||||
|
|
||||||
|
<TextBox Classes="field" Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||||||
|
|
||||||
|
<TextBox Classes="field" Text="{Binding EditorCommand}" PlaceholderText="command"
|
||||||
|
Height="120" AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
VerticalContentAlignment="Top" Padding="14,10" />
|
||||||
|
|
||||||
|
<TextBox Classes="field" Text="{Binding EditorNotes}" PlaceholderText="notes" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ The checkbox the whole screen is arranged around, with the paragraph that says why leaving it
|
||||||
|
off is the safe default. It is not a preference — it is the difference between a command a human
|
||||||
|
reads before committing to it and one that has already run.
|
||||||
|
-->
|
||||||
|
<CheckBox IsChecked="{Binding EditorRunsOnInsert}" MinHeight="44">
|
||||||
|
<TextBlock Classes="mono" FontSize="11.5" Text="Press Enter after inserting this"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</CheckBox>
|
||||||
|
<TextBlock Classes="body"
|
||||||
|
Text="Left off, inserting types the command at the prompt and stops, so it can be read and edited before it runs. Nothing here can tell whether the terminal is even at a prompt, which is why that is the default." />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||||||
|
Command="{Binding SaveCommand}" />
|
||||||
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||||
|
Command="{Binding CancelCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ============ the list ============ -->
|
||||||
|
<Panel Grid.Row="3" IsVisible="{Binding !IsEditing}">
|
||||||
|
|
||||||
|
<!-- Written by the view model, and it says what a snippet is for rather than that there are none. -->
|
||||||
|
<TextBlock Classes="body" IsVisible="{Binding !HasVisible}" Margin="24,20" VerticalAlignment="Top"
|
||||||
|
Text="{Binding EmptyMessage}" />
|
||||||
|
|
||||||
|
<ListBox ItemsSource="{Binding Visible}" SelectedItem="{Binding Selected}"
|
||||||
|
Background="Transparent" BorderThickness="0" IsVisible="{Binding HasVisible}">
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="MinHeight" Value="0" />
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SnippetRowViewModel">
|
||||||
|
<Border Classes="card" Margin="12,5">
|
||||||
|
<StackPanel Spacing="9">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||||
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Label}"
|
||||||
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<!-- ◆ The chip that says this one does not wait to be read. -->
|
||||||
|
<Border Classes="tag" IsVisible="{Binding RunsOnInsert}"
|
||||||
|
Background="{StaticResource WarnWash}">
|
||||||
|
<TextBlock Text="RUNS" Foreground="{StaticResource Warn}" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Classes="tag outline"
|
||||||
|
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock Text="{Binding Badge}" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- The command, on the surface every block of monospace in this design is drawn on. -->
|
||||||
|
<Border Classes="output" CornerRadius="9" Padding="11,9">
|
||||||
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding Preview}"
|
||||||
|
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<!-- ============ what to do with the chosen one ============ -->
|
||||||
|
<!--
|
||||||
|
Raised over the list, as the hosts screen raises its connect bar, and for the same reason: there is no
|
||||||
|
second column to put it in. Both buttons name the terminal — "TYPE INTO pg-primary" — because on a
|
||||||
|
phone the tab strip may be scrolled away and "whatever is in the terminal receives this" is not a
|
||||||
|
sentence anybody should have to guess the subject of.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="4" IsVisible="{Binding HasSelection}" Background="{StaticResource Chrome}"
|
||||||
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="14,12"
|
||||||
|
IsEnabled="{Binding !IsEditing}">
|
||||||
|
<StackPanel Spacing="9">
|
||||||
|
|
||||||
|
<Button Classes="primary" Content="{Binding InsertLabel}" Command="{Binding InsertCommand}"
|
||||||
|
IsEnabled="{Binding CanInsert}" />
|
||||||
|
|
||||||
|
<!-- Only for a snippet marked as running. For every other one there is nothing this could mean. -->
|
||||||
|
<Button Classes="secondary" Height="44" Content="{Binding RunLabel}"
|
||||||
|
Command="{Binding RunCommand}" IsVisible="{Binding SelectionRuns}"
|
||||||
|
IsEnabled="{Binding CanInsert}" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<Button Grid.Column="0" Classes="secondary" Height="44" Content="EDIT"
|
||||||
|
Command="{Binding EditCommand}" />
|
||||||
|
<Button Grid.Column="2" Classes="danger" Height="44" Content="DELETE"
|
||||||
|
Command="{Binding DeleteCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Android.Views;
|
||||||
|
|
||||||
|
/// <summary>Design 05 — the saved commands, and the two ways of sending one to a shell.</summary>
|
||||||
|
internal sealed partial class SnippetsScreen : UserControl
|
||||||
|
{
|
||||||
|
public SnippetsScreen() => AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
@@ -21,43 +21,61 @@
|
|||||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||||
|
|
||||||
<!-- ============ tabs ============ -->
|
<!-- ============ tabs ============ -->
|
||||||
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
<!--
|
||||||
BorderThickness="0,0,0,1">
|
v2 draws these as pills rather than as a segmented strip, so the row is transparent and each session
|
||||||
|
carries its own outline. The close cross moved inside the pill with the name, which is what makes it
|
||||||
|
read as one object you can dismiss rather than as two adjacent targets.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="0" Height="52">
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
<StackPanel Orientation="Horizontal" Height="44">
|
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
||||||
<ItemsControl ItemsSource="{Binding Tabs}">
|
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" /></ItemsPanelTemplate>
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||||
<Grid ColumnDefinitions="Auto,Auto">
|
<!--
|
||||||
<Button Grid.Column="0" Classes="row" MinHeight="44" Padding="12,0"
|
44 tall, where the session pills on the shell strip are 34. The difference is the close
|
||||||
|
cross: a pill you only select can be chip-sized, and a pill containing the control that ends
|
||||||
|
a shell cannot. This head's rule is 44 and this is the one control on the phone that is both
|
||||||
|
destructive and has no confirmation and no undo — see CloseTabAsync, which ends the session
|
||||||
|
the moment it is pressed.
|
||||||
|
-->
|
||||||
|
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||||
|
BorderThickness="1" CornerRadius="11" Height="44">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Classes="row" MinHeight="42" Padding="13,0" CornerRadius="11"
|
||||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||||
<!-- Green only while there is a shell behind it; see the same dot in PhoneShell. -->
|
<!-- Green only while there is a shell behind it; see the same dot in PhoneShell. -->
|
||||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="5" Height="5"
|
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
<TextBlock Classes="mono" FontSize="12" FontWeight="SemiBold"
|
||||||
|
Text="{Binding Label}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
<!--
|
<!--
|
||||||
The close cross is inside the tab, which the plan calls out: a strip-level close would
|
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
|
act on whichever tab happened to be selected, and on a phone that is a mis-tap away from
|
||||||
killing the wrong shell.
|
killing the wrong shell.
|
||||||
|
|
||||||
|
The hairline down its left edge is not decoration. The two targets are flush inside one
|
||||||
|
pill, so without a visible seam there is nothing telling a thumb where "switch to this
|
||||||
|
shell" stops and "end it" starts.
|
||||||
-->
|
-->
|
||||||
<Button Grid.Column="1" Classes="row" MinHeight="44" Width="34" Padding="0"
|
<Button Classes="row" MinHeight="42" Width="44" Padding="0" CornerRadius="0,11,11,0"
|
||||||
HorizontalContentAlignment="Center"
|
HorizontalContentAlignment="Center"
|
||||||
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="1,0,0,0"
|
||||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
<TextBlock Text="×" Foreground="{StaticResource TextFaint}" FontSize="14" />
|
<TextBlock Text="×" Foreground="{StaticResource TextFaint}" FontSize="14" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -70,11 +88,16 @@
|
|||||||
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head has
|
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head has
|
||||||
no keyboard to press it with, which is why the two heads differ here and nowhere else in this screen.
|
no keyboard to press it with, which is why the two heads differ here and nowhere else in this screen.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="1" Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderSubtle}"
|
<!--
|
||||||
BorderThickness="0,0,0,1" Padding="14,5"
|
The design's line here also carries a round-trip time and a forwarded port. Neither is drawn: the SSH
|
||||||
|
library offers no RTT measurement, and nothing in this application forwards anything. What is left is
|
||||||
|
the one fact that is real and is the one that matters — the account and endpoint actually dialled.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="1" Padding="16,5" Height="36"
|
||||||
IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}">
|
IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||||
<TextBlock Grid.Column="0" Classes="detail" FontSize="9.5" TextTrimming="CharacterEllipsis"
|
<TextBlock Grid.Column="0" Classes="detail" FontSize="10.5" TextTrimming="CharacterEllipsis"
|
||||||
|
Foreground="{StaticResource TextDim}"
|
||||||
VerticalAlignment="Center" Text="{Binding SelectedTab.Address}" />
|
VerticalAlignment="Center" Text="{Binding SelectedTab.Address}" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -137,6 +160,12 @@
|
|||||||
IsTerminalShowing rather than HasTabs, which are no longer the same question: a tab that is still
|
IsTerminalShowing rather than HasTabs, which are no longer the same question: a tab that is still
|
||||||
connecting has no pane, and showing the renderer for it would show the previous session's output
|
connecting has no pane, and showing the renderer for it would show the previous session's output
|
||||||
under the name of a machine nothing has connected to yet.
|
under the name of a machine nothing has connected to yet.
|
||||||
|
|
||||||
|
v2 insets this behind a 14-pixel radius. Not done, and not an oversight: this is a native child view
|
||||||
|
composited above everything Avalonia draws, so a rounded Border behind it clips nothing — the
|
||||||
|
corners would stay square over a frame pretending otherwise. What the design was really after is
|
||||||
|
that the terminal reads as the lit surface, and that comes from its own background, which now is the
|
||||||
|
design's #171A26. See WebAssets/terminal.css.
|
||||||
-->
|
-->
|
||||||
<NativeWebView x:Name="Renderer" IsVisible="{Binding IsTerminalShowing, FallbackValue=False}" />
|
<NativeWebView x:Name="Renderer" IsVisible="{Binding IsTerminalShowing, FallbackValue=False}" />
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -149,11 +178,10 @@
|
|||||||
then release, because holding a modifier while typing is not possible one-thumbed.
|
then release, because holding a modifier while typing is not possible one-thumbed.
|
||||||
-->
|
-->
|
||||||
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
||||||
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
<Border Grid.Row="3" IsVisible="{Binding IsTerminalShowing}" Height="50">
|
||||||
BorderThickness="0,1,0,0" IsVisible="{Binding IsTerminalShowing}">
|
|
||||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Height="44" Spacing="4"
|
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Spacing="5" Margin="12,0"
|
||||||
Margin="6,5" />
|
VerticalAlignment="Center" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -129,13 +129,13 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
// 44 wide as well as tall. The design draws them flexed across the width, which at 360dp
|
// 44 wide as well as tall. The design draws them flexed across the width, which at 360dp
|
||||||
// with ten keys is 32 pixels each — under every thumb-target guideline there is.
|
// with ten keys is 32 pixels each — under every thumb-target guideline there is.
|
||||||
MinWidth = 44,
|
MinWidth = 44,
|
||||||
Height = 34,
|
Height = 38,
|
||||||
Padding = new Thickness(10, 0),
|
Padding = new Thickness(10, 0),
|
||||||
CornerRadius = new CornerRadius(5),
|
CornerRadius = new CornerRadius(9),
|
||||||
Background = new SolidColorBrush(Color.Parse("#161B19")),
|
Background = Palette("Panel"),
|
||||||
BorderBrush = new SolidColorBrush(Color.Parse("#232927")),
|
BorderBrush = Palette("BorderMid"),
|
||||||
BorderThickness = new Thickness(1),
|
BorderThickness = new Thickness(1),
|
||||||
Foreground = new SolidColorBrush(Color.Parse("#B7C0BB")),
|
Foreground = Palette("TextDim"),
|
||||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,13 +164,18 @@ internal sealed partial class TerminalScreen : UserControl
|
|||||||
|
|
||||||
// Latched state has to be visible. A modifier that is on and does not look on is how somebody sends
|
// Latched state has to be visible. A modifier that is on and does not look on is how somebody sends
|
||||||
// ^L to a database prompt believing they typed an l.
|
// ^L to a database prompt believing they typed an l.
|
||||||
controlKey.Background = new SolidColorBrush(
|
controlKey.Background = Palette(controlLatched ? "Active" : "Panel");
|
||||||
Color.Parse(controlLatched ? "#243A2F" : "#161B19"));
|
controlKey.Foreground = Palette(controlLatched ? "AccentText" : "TextDim");
|
||||||
|
|
||||||
controlKey.Foreground = new SolidColorBrush(
|
|
||||||
Color.Parse(controlLatched ? "#3CE88F" : "#B7C0BB"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>One brush from <c>Theme/Palette.axaml</c>, by key.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The accessory row is built in code because its keys come from a table, so its colours cannot be set in
|
||||||
|
/// XAML with the rest of the screen's. Resolving them by name is the next best thing: a row that named
|
||||||
|
/// its own blues is how the palette ends up with a fifth surface nobody meant to add.
|
||||||
|
/// </remarks>
|
||||||
|
private static IBrush? Palette(string key) => Application.Current?.FindResource(key) as IBrush;
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The control translation is the ASCII one and nothing cleverer: Ctrl-A through Ctrl-Z are 0x01 to
|
/// The control translation is the ASCII one and nothing cleverer: Ctrl-A through Ctrl-Z are 0x01 to
|
||||||
/// 0x1A, which is letter & 0x1F. Applied only to letters, because Ctrl with an arrow key is a
|
/// 0x1A, which is letter & 0x1F. Applied only to letters, because Ctrl with an arrow key is a
|
||||||
|
|||||||
@@ -428,8 +428,12 @@
|
|||||||
decoration that reads as information.
|
decoration that reads as information.
|
||||||
|
|
||||||
A style rather than a value converter so that the two colours are the palette's, once. A converter
|
A style rather than a value converter so that the two colours are the palette's, once. A converter
|
||||||
would have had to name #3CE88F in C#, which is how a green ends up one shade off in the fourth place
|
would have had to name #3DDC97 in C#, which is how a green ends up one shade off in the fourth place
|
||||||
it appears.
|
it appears.
|
||||||
|
|
||||||
|
Live rather than Accent, and that is a v2 change with meaning rather than a recolour. The accent is
|
||||||
|
blue now, and the green survives as the colour of a fact — see the remark on Live in Palette.axaml.
|
||||||
|
Filling this with Accent would say a connected host is a thing to press.
|
||||||
-->
|
-->
|
||||||
<Style Selector="Ellipse.dot">
|
<Style Selector="Ellipse.dot">
|
||||||
<Setter Property="Width" Value="6" />
|
<Setter Property="Width" Value="6" />
|
||||||
@@ -437,7 +441,7 @@
|
|||||||
<Setter Property="Fill" Value="{StaticResource TextFaint}" />
|
<Setter Property="Fill" Value="{StaticResource TextFaint}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Ellipse.dot.live">
|
<Style Selector="Ellipse.dot.live">
|
||||||
<Setter Property="Fill" Value="{StaticResource Accent}" />
|
<Setter Property="Fill" Value="{StaticResource Live}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
from "inside": a press anywhere on the card below reports that control as its source and bubbles through
|
from "inside": a press anywhere on the card below reports that control as its source and bubbles through
|
||||||
here on its way to the window.
|
here on its way to the window.
|
||||||
-->
|
-->
|
||||||
<Border x:Name="Backdrop" Background="#CC0A0C0B" PointerPressed="OnBackdropPressed">
|
<!-- 80% of Canvas. Written out because a scrim is a brush with an alpha, and the palette holds no alpha
|
||||||
|
variant of a surface — see Palette.axaml, where the only pre-multiplied brushes are the accent washes. -->
|
||||||
|
<Border x:Name="Backdrop" Background="#CC0E1220" PointerPressed="OnBackdropPressed">
|
||||||
<Border Width="520" VerticalAlignment="Top" Margin="0,90,0,0"
|
<Border Width="520" VerticalAlignment="Top" Margin="0,90,0,0"
|
||||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource BorderMid}"
|
Background="{StaticResource Chrome}" BorderBrush="{StaticResource BorderMid}"
|
||||||
BorderThickness="1" CornerRadius="6">
|
BorderThickness="1" CornerRadius="6">
|
||||||
|
|||||||
@@ -37,11 +37,21 @@
|
|||||||
<Style Selector="TextBlock.entry">
|
<Style Selector="TextBlock.entry">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<!--
|
||||||
|
Two different hues, and after v2 that takes saying. These marks encode two independent facts in one
|
||||||
|
column, so they have to be told apart at a glance — and they used to be, for free, because the accent
|
||||||
|
was green and Info was blue. v2 made the accent blue too, which put #5B8CFF beside #7FB0FF: the same
|
||||||
|
hue two steps apart, which is a shade rather than a distinction.
|
||||||
|
|
||||||
|
So an executable is Live green now. It is the one use of that colour that is not about a session, and
|
||||||
|
it earns it on the same grounds — it is a fact about the file rather than something to press, and it
|
||||||
|
is the colour this marker already was before the accent moved out from under it.
|
||||||
|
-->
|
||||||
<Style Selector="TextBlock.entry.dir">
|
<Style Selector="TextBlock.entry.dir">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Info}" />
|
<Setter Property="Foreground" Value="{StaticResource Info}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="TextBlock.entry.exec">
|
<Style Selector="TextBlock.entry.exec">
|
||||||
<Setter Property="Foreground" Value="{StaticResource Accent}" />
|
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
The palette, named for what a colour is for rather than for what it looks like. Every one of these is
|
The palette, named for what a colour is for rather than for what it looks like. Every one of these is
|
||||||
from the design; the names are this codebase's, because "#0C0F0E" appearing in nine files is how a
|
from the design; the names are this codebase's, because "#0E1220" appearing in nine files is how a
|
||||||
surface ends up two shades off in the tenth.
|
surface ends up two shades off in the tenth.
|
||||||
|
|
||||||
It lives here, in the shared project, because there are now two heads drawing the same product. Two
|
It lives here, in the shared project, because there are now two heads drawing the same product. Two
|
||||||
@@ -13,62 +13,138 @@
|
|||||||
Five near-black surfaces rather than one, and the difference between them is real work: the window is
|
Five near-black surfaces rather than one, and the difference between them is real work: the window is
|
||||||
the darkest so the terminal reads as the lit thing, chrome sits one step up so the header and status
|
the darkest so the terminal reads as the lit thing, chrome sits one step up so the header and status
|
||||||
bar frame it, and the lists sit between the two so a list does not look like part of either.
|
bar frame it, and the lists sit between the two so a list does not look like part of either.
|
||||||
|
|
||||||
|
── v2 ──────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
This is the second design's palette: blue-black surfaces and a blue accent, replacing the green-black
|
||||||
|
ones the first import shipped. Both heads move together, which is the whole reason the file is here —
|
||||||
|
the Android v2 design and the desktop v2 design specify the same seventeen tokens, so a phone that went
|
||||||
|
blue while the desktop stayed green would be the drift this file exists to prevent.
|
||||||
|
|
||||||
|
Where the design names a colour, the value below is that colour exactly. The design has seventeen
|
||||||
|
tokens and this palette has rather more, so the rest are interpolated along the same ramp and are
|
||||||
|
marked. That is deliberate rather than lazy: the finer distinctions here — three border weights, four
|
||||||
|
text steps — are ones the design draws without naming, and collapsing them to match the token count
|
||||||
|
would lose the rule each one encodes.
|
||||||
-->
|
-->
|
||||||
<Color x:Key="CanvasColor">#0A0C0B</Color>
|
|
||||||
|
<!--
|
||||||
|
The surfaces, darkest first. Two things changed shape rather than merely colour.
|
||||||
|
|
||||||
|
A field is now *inset* — darker than the canvas it sits on — where it used to be raised. That follows
|
||||||
|
the design, which draws the search box as a well rather than as a tile, and it is the one place where
|
||||||
|
reading the old palette's ordering across to the new one gives the wrong answer.
|
||||||
|
|
||||||
|
And Chrome and Panel are now the same value, because the design uses one surface for both the header
|
||||||
|
and a card. They are kept as two names anyway: they answer different questions, and the day one of the
|
||||||
|
two moves is the day having merged them would have to be undone across every file that used the wrong
|
||||||
|
one.
|
||||||
|
-->
|
||||||
|
<Color x:Key="CanvasColor">#0E1220</Color>
|
||||||
<SolidColorBrush x:Key="Canvas" Color="{StaticResource CanvasColor}" />
|
<SolidColorBrush x:Key="Canvas" Color="{StaticResource CanvasColor}" />
|
||||||
<SolidColorBrush x:Key="Chrome" Color="#0D100F" />
|
<SolidColorBrush x:Key="Chrome" Color="#111629" />
|
||||||
<SolidColorBrush x:Key="Sidebar" Color="#0C0F0E" />
|
<SolidColorBrush x:Key="Sidebar" Color="#0F1426" />
|
||||||
<SolidColorBrush x:Key="Panel" Color="#0F1211" />
|
<SolidColorBrush x:Key="Panel" Color="#111629" />
|
||||||
<SolidColorBrush x:Key="Raised" Color="#111514" />
|
<SolidColorBrush x:Key="Raised" Color="#151B31" />
|
||||||
<SolidColorBrush x:Key="Field" Color="#121615" />
|
<SolidColorBrush x:Key="Field" Color="#0B0F1D" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The surface a terminal, a log and a snippet's command are drawn on, and it is not one of the five
|
||||||
|
above. The design gives every block of monospaced output this one value, warmer than the chrome around
|
||||||
|
it, and that is what makes the lit thing read as lit. Named here rather than repeated in the renderer's
|
||||||
|
stylesheet and in two screens, which is how the three of them would drift.
|
||||||
|
-->
|
||||||
|
<SolidColorBrush x:Key="TerminalSurface" Color="#171A26" />
|
||||||
|
|
||||||
<!-- Row hover, and the heavier one the chrome's own buttons use. On the phone these are press states. -->
|
<!-- Row hover, and the heavier one the chrome's own buttons use. On the phone these are press states. -->
|
||||||
<SolidColorBrush x:Key="Hover" Color="#141817" />
|
<SolidColorBrush x:Key="Hover" Color="#151B31" />
|
||||||
<SolidColorBrush x:Key="ChromeHover" Color="#1A1F1D" />
|
<SolidColorBrush x:Key="ChromeHover" Color="#1D2749" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Selection, and it is a surface rather than a highlight: the design fills a chosen host row, the current
|
||||||
|
segment of a toggle and the current chip with this and adds no border. Distinct from Hover, which is
|
||||||
|
what the finger is on rather than what is chosen.
|
||||||
|
-->
|
||||||
|
<SolidColorBrush x:Key="Active" Color="#1D2749" />
|
||||||
|
|
||||||
|
<!-- The fill behind a small monospaced tag — a group name, a storage class, a bucket. -->
|
||||||
|
<SolidColorBrush x:Key="Chip" Color="#16204A" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Three border weights, and they are not interchangeable. Strong separates one region from another,
|
Three border weights, and they are not interchangeable. Strong separates one region from another,
|
||||||
subtle separates rows inside one region, and mid is what a control draws around itself.
|
subtle separates rows inside one region, and mid is what a control draws around itself.
|
||||||
-->
|
-->
|
||||||
<SolidColorBrush x:Key="Border" Color="#1E2422" />
|
<SolidColorBrush x:Key="Border" Color="#1A2138" />
|
||||||
<SolidColorBrush x:Key="BorderSubtle" Color="#171C1A" />
|
<SolidColorBrush x:Key="BorderSubtle" Color="#151B31" />
|
||||||
<SolidColorBrush x:Key="BorderMid" Color="#2A312E" />
|
<SolidColorBrush x:Key="BorderMid" Color="#232B45" />
|
||||||
<SolidColorBrush x:Key="BorderHover" Color="#3A423E" />
|
<SolidColorBrush x:Key="BorderHover" Color="#2E3859" />
|
||||||
<SolidColorBrush x:Key="BorderFaint" Color="#232927" />
|
<SolidColorBrush x:Key="BorderFaint" Color="#1F2740" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The text ramp. Three steps, used consistently: what you read, what you glance at, and what is there
|
The text ramp. Three steps, used consistently: what you read, what you glance at, and what is there
|
||||||
only so its absence would be noticed. A fourth step would be one nobody could tell from its neighbours.
|
only so its absence would be noticed. A fourth step would be one nobody could tell from its neighbours.
|
||||||
-->
|
-->
|
||||||
<SolidColorBrush x:Key="Text" Color="#DCE3DF" />
|
<SolidColorBrush x:Key="Text" Color="#E3E7F4" />
|
||||||
<SolidColorBrush x:Key="TextDim" Color="#7E8A84" />
|
<SolidColorBrush x:Key="TextDim" Color="#8B93B0" />
|
||||||
<SolidColorBrush x:Key="TextFaint" Color="#566059" />
|
<SolidColorBrush x:Key="TextFaint" Color="#565E7E" />
|
||||||
<SolidColorBrush x:Key="TextGhost" Color="#404743" />
|
<SolidColorBrush x:Key="TextGhost" Color="#414A6B" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The accent, and the three colours that are allowed to disagree with it. Green means live, connected or
|
The accent, and it is two colours rather than one. The design fills with #5B8CFF — the button, the
|
||||||
yours; amber means a caveat worth reading; red means refused or destructive; blue is for the one thing
|
floating action button, a progress bar, a toggle that is on — and writes with #8FA7FF: the current
|
||||||
that is neither — a directory, a distinct scope — and is deliberately rare.
|
destination in the bottom bar, a link, the text inside a chip. They are not interchangeable in either
|
||||||
|
direction. The fill is too saturated to read as small text on a dark surface, and the text colour is
|
||||||
|
too pale to carry a button.
|
||||||
|
|
||||||
|
AccentInk is what goes *on* the fill. It equals Canvas today and is named separately anyway, because
|
||||||
|
"the darkest surface" and "the colour of text on a blue button" are two facts that happen to coincide,
|
||||||
|
and a light theme would separate them immediately.
|
||||||
-->
|
-->
|
||||||
<Color x:Key="AccentColor">#3CE88F</Color>
|
<Color x:Key="AccentColor">#5B8CFF</Color>
|
||||||
<SolidColorBrush x:Key="Accent" Color="{StaticResource AccentColor}" />
|
<SolidColorBrush x:Key="Accent" Color="{StaticResource AccentColor}" />
|
||||||
<SolidColorBrush x:Key="AccentSoft" Color="#3CE88F" Opacity="0.35" />
|
<SolidColorBrush x:Key="AccentText" Color="#8FA7FF" />
|
||||||
<SolidColorBrush x:Key="AccentWash" Color="#3CE88F" Opacity="0.06" />
|
<SolidColorBrush x:Key="AccentInk" Color="#0E1220" />
|
||||||
<SolidColorBrush x:Key="Warn" Color="#E8B44C" />
|
<SolidColorBrush x:Key="AccentSoft" Color="#5B8CFF" Opacity="0.35" />
|
||||||
<SolidColorBrush x:Key="WarnSoft" Color="#E8B44C" Opacity="0.35" />
|
<SolidColorBrush x:Key="AccentWash" Color="#5B8CFF" Opacity="0.06" />
|
||||||
<SolidColorBrush x:Key="WarnWash" Color="#E8B44C" Opacity="0.06" />
|
|
||||||
<SolidColorBrush x:Key="WarnText" Color="#B9A26B" />
|
|
||||||
<SolidColorBrush x:Key="Danger" Color="#E85D5D" />
|
|
||||||
<SolidColorBrush x:Key="DangerSoft" Color="#E85D5D" Opacity="0.3" />
|
|
||||||
<SolidColorBrush x:Key="DangerWash" Color="#E85D5D" Opacity="0.08" />
|
|
||||||
<SolidColorBrush x:Key="DangerText" Color="#D98A8A" />
|
|
||||||
<SolidColorBrush x:Key="Info" Color="#5DA9E8" />
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The design asks for IBM Plex Mono and IBM Plex Sans. Neither ships with this application, and neither
|
◆ Green is no longer the accent, and that is the one semantic change in this file rather than a
|
||||||
is on a stock Windows install or a stock Android one, so requesting them by name would render as
|
recolour.
|
||||||
whatever the font fallback chose that day — which is worse than choosing deliberately. Inter is
|
|
||||||
embedded by both heads and is what they already draw with.
|
It used to be both: Ellipse.dot.live filled with Accent, so "a shell is open on this host" and "this is
|
||||||
|
the thing the screen wants you to press" were the same colour by construction. The v2 design keeps a
|
||||||
|
green — #3DDC97, for the live dot, the sync light and the follow indicator — and makes the accent blue,
|
||||||
|
which finally separates the two. Live is that green, and nothing that is merely interactive may use it.
|
||||||
|
|
||||||
|
Read the pair as: blue is what you can do, green is what is true.
|
||||||
|
-->
|
||||||
|
<SolidColorBrush x:Key="Live" Color="#3DDC97" />
|
||||||
|
<SolidColorBrush x:Key="LiveWash" Color="#3DDC97" Opacity="0.12" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The three colours that are allowed to disagree with the accent. Amber means a caveat worth reading, red
|
||||||
|
means refused or destructive, and blue-light is for the one thing that is neither — an informational log
|
||||||
|
line, a directory, a distinct scope.
|
||||||
|
|
||||||
|
WarnText and DangerText are the prose weights: a wash is a very dark tint, and the signal colour that
|
||||||
|
works as a 6-pixel dot or a one-word label is not the one that reads as a sentence on top of it. The
|
||||||
|
design supplies both reds — #E86A6A labels the refusal and #FF8F8F writes it out — and one amber, so
|
||||||
|
the amber prose weight is interpolated.
|
||||||
|
-->
|
||||||
|
<SolidColorBrush x:Key="Warn" Color="#FFB454" />
|
||||||
|
<SolidColorBrush x:Key="WarnSoft" Color="#FFB454" Opacity="0.35" />
|
||||||
|
<SolidColorBrush x:Key="WarnWash" Color="#FFB454" Opacity="0.12" />
|
||||||
|
<SolidColorBrush x:Key="WarnText" Color="#E4A868" />
|
||||||
|
<SolidColorBrush x:Key="Danger" Color="#E86A6A" />
|
||||||
|
<SolidColorBrush x:Key="DangerSoft" Color="#E86A6A" Opacity="0.3" />
|
||||||
|
<SolidColorBrush x:Key="DangerWash" Color="#E86A6A" Opacity="0.12" />
|
||||||
|
<SolidColorBrush x:Key="DangerText" Color="#FF8F8F" />
|
||||||
|
<SolidColorBrush x:Key="Info" Color="#7FB0FF" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The design asks for Instrument Sans and Fira Code, as the one before it asked for IBM Plex. Neither
|
||||||
|
ships with this application, and neither is on a stock Windows install or a stock Android one, so
|
||||||
|
requesting them by name would render as whatever the font fallback chose that day — which is worse than
|
||||||
|
choosing deliberately. Inter is embedded by both heads and is what they already draw with.
|
||||||
|
|
||||||
The stack ends in the generic `monospace` rather than a Windows face, which is what makes it work on
|
The stack ends in the generic `monospace` rather than a Windows face, which is what makes it work on
|
||||||
both: Android has no Cascadia Mono or Consolas and resolves the generic name to its own mono face.
|
both: Android has no Cascadia Mono or Consolas and resolves the generic name to its own mono face.
|
||||||
|
|||||||
@@ -102,6 +102,34 @@ internal enum ShellScreen
|
|||||||
/// <summary>What has been connected to, and what has been changed.</summary>
|
/// <summary>What has been connected to, and what has been changed.</summary>
|
||||||
/// <inheritdoc cref="KnownHosts" path="/remarks" />
|
/// <inheritdoc cref="KnownHosts" path="/remarks" />
|
||||||
Logs = 8,
|
Logs = 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The phone's hub for everything the bottom bar has no room for.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Drawn by the Android head alone. The desktop has a nav rail with room for every destination, so it has
|
||||||
|
/// nothing to put behind a hub and never sets this; the v2 phone design has four slots and nine places to
|
||||||
|
/// go, so five of them live one tap deeper. It is a member of the shared enum rather than a phone-local
|
||||||
|
/// bool because it is a value of <see cref="Screen"/> like any other — the back arrow on each of those
|
||||||
|
/// five screens comes here, and a second notion of "where am I" is how the two would disagree.
|
||||||
|
/// </para>
|
||||||
|
/// <inheritdoc cref="KnownHosts" path="/remarks" />
|
||||||
|
/// </remarks>
|
||||||
|
More = 9,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objects in an S3-compatible bucket.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The same screen as <see cref="Transfers"/> over the same <see cref="TransfersViewModel"/>, and a
|
||||||
|
/// separate destination anyway. What differs is only which picker is offered, so the two do not each
|
||||||
|
/// need a screen — but they do each need a name, because the v2 design puts SFTP and S3 side by side in
|
||||||
|
/// the hub, and a single "files" entry that silently remembered which kind you last opened would be a
|
||||||
|
/// destination whose meaning depended on history. Entering either sets
|
||||||
|
/// <see cref="TransfersViewModel.Remote"/>; see <see cref="OnScreenChanged"/>.
|
||||||
|
/// </remarks>
|
||||||
|
Buckets = 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -701,6 +729,12 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
/// <inheritdoc cref="IsHostsScreen" />
|
/// <inheritdoc cref="IsHostsScreen" />
|
||||||
internal bool IsLogsScreen => Screen is ShellScreen.Logs;
|
internal bool IsLogsScreen => Screen is ShellScreen.Logs;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsHostsScreen" />
|
||||||
|
internal bool IsMoreScreen => Screen is ShellScreen.More;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsHostsScreen" />
|
||||||
|
internal bool IsBucketsScreen => Screen is ShellScreen.Buckets;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the nav rail should light its Hosts entry.
|
/// Whether the nav rail should light its Hosts entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -734,6 +768,28 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
/// <inheritdoc cref="IsHostsShowing" />
|
/// <inheritdoc cref="IsHostsShowing" />
|
||||||
internal bool IsLogsShowing => IsShowingPages && IsLogsScreen;
|
internal bool IsLogsShowing => IsShowingPages && IsLogsScreen;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsHostsShowing" />
|
||||||
|
internal bool IsMoreShowing => IsShowingPages && IsMoreScreen;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsHostsShowing" />
|
||||||
|
internal bool IsBucketsShowing => IsShowingPages && IsBucketsScreen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the phone's MORE tab should light.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The hub and everything behind it, because a bottom bar that went dark the moment you opened one of
|
||||||
|
/// its destinations would be a bar that only ever lights three of its four entries. This is the one
|
||||||
|
/// place where "which tab" and "which screen" are deliberately not the same question — the other three
|
||||||
|
/// tabs are each exactly one screen, and this one is six.
|
||||||
|
///
|
||||||
|
/// Preferences is in the list because the phone reaches it through the hub. The desktop reaches it from
|
||||||
|
/// the rail and never asks this.
|
||||||
|
/// </remarks>
|
||||||
|
internal bool IsMoreSurface =>
|
||||||
|
IsShowingPages && Screen is ShellScreen.More or ShellScreen.Snippets or ShellScreen.Logs
|
||||||
|
or ShellScreen.Transfers or ShellScreen.Buckets or ShellScreen.Preferences;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the terminal's WebView may be on screen at this instant.
|
/// Whether the terminal's WebView may be on screen at this instant.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2435,6 +2491,43 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
{
|
{
|
||||||
_ = teams.LoadAsync(CancellationToken.None);
|
_ = teams.LoadAsync(CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Goes to the file screen with one of the two kinds of remote offered.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The phone splits SFTP and S3 into two destinations over this one view model; the desktop has a
|
||||||
|
/// single FILES screen with the toggle on it. So the kind is set here, by the thing that navigates,
|
||||||
|
/// rather than in <see cref="OnScreenChanged"/> — which would have made every arrival at
|
||||||
|
/// <see cref="ShellScreen.Transfers"/> force the picker back to hosts, including the desktop's own nav
|
||||||
|
/// rail arriving at a screen with a bucket already open.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <b>It will not change the kind while something is open.</b> There is one session behind both
|
||||||
|
/// destinations, so switching the picker under a live one would leave a screen titled S3 listing an
|
||||||
|
/// SFTP host's files. Refusing and saying so is the honest half of sharing a view model between two
|
||||||
|
/// destinations; the screen keeps showing what is actually open.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[RelayCommand]
|
||||||
|
private void ShowFiles(RemoteKind kind)
|
||||||
|
{
|
||||||
|
if (Transfers.IsConnected && Transfers.Remote != kind)
|
||||||
|
{
|
||||||
|
Transfers.Status = kind is RemoteKind.Bucket
|
||||||
|
? "An SFTP session is open. Close it before opening a bucket."
|
||||||
|
: "A bucket is open. Close it before connecting to a host.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Transfers.Remote = kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
Screen = kind is RemoteKind.Bucket ? ShellScreen.Buckets : ShellScreen.Transfers;
|
||||||
|
Surface = ShellSurface.Page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="OnScreenChanged" />
|
/// <inheritdoc cref="OnScreenChanged" />
|
||||||
@@ -2456,6 +2549,8 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
OnPropertyChanged(nameof(IsImportScreen));
|
OnPropertyChanged(nameof(IsImportScreen));
|
||||||
OnPropertyChanged(nameof(IsSnippetsScreen));
|
OnPropertyChanged(nameof(IsSnippetsScreen));
|
||||||
OnPropertyChanged(nameof(IsLogsScreen));
|
OnPropertyChanged(nameof(IsLogsScreen));
|
||||||
|
OnPropertyChanged(nameof(IsMoreScreen));
|
||||||
|
OnPropertyChanged(nameof(IsBucketsScreen));
|
||||||
|
|
||||||
OnPropertyChanged(nameof(IsShowingPages));
|
OnPropertyChanged(nameof(IsShowingPages));
|
||||||
OnPropertyChanged(nameof(IsHostsShowing));
|
OnPropertyChanged(nameof(IsHostsShowing));
|
||||||
@@ -2466,6 +2561,9 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
|||||||
OnPropertyChanged(nameof(IsKnownHostsShowing));
|
OnPropertyChanged(nameof(IsKnownHostsShowing));
|
||||||
OnPropertyChanged(nameof(IsSnippetsShowing));
|
OnPropertyChanged(nameof(IsSnippetsShowing));
|
||||||
OnPropertyChanged(nameof(IsLogsShowing));
|
OnPropertyChanged(nameof(IsLogsShowing));
|
||||||
|
OnPropertyChanged(nameof(IsMoreShowing));
|
||||||
|
OnPropertyChanged(nameof(IsBucketsShowing));
|
||||||
|
OnPropertyChanged(nameof(IsMoreSurface));
|
||||||
|
|
||||||
RaiseTerminalState();
|
RaiseTerminalState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,15 @@ internal sealed class RemoteEntryRowViewModel(SftpEntry entry)
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal string Size => entry.Kind is SftpEntryKind.File ? ByteSize.Format(entry.Length) : string.Empty;
|
internal string Size => entry.Kind is SftpEntryKind.File ? ByteSize.Format(entry.Length) : string.Empty;
|
||||||
|
|
||||||
internal string Modified => Timestamps.Format(entry.LastWriteTimeUtc);
|
/// <remarks>
|
||||||
|
/// Blank when there is no timestamp, for the reason the size above is blank for a directory. A bucket
|
||||||
|
/// has no folders — a prefix is inferred from the keys under it — so the object store has nothing to
|
||||||
|
/// report for one and leaves the timestamp at its default. Formatting that prints
|
||||||
|
/// <c>0001-01-01 00:00</c>, which is not a quiet "unknown": it is a date, and a listing that states one
|
||||||
|
/// for every folder in a bucket is stating something untrue.
|
||||||
|
/// </remarks>
|
||||||
|
internal string Modified =>
|
||||||
|
entry.LastWriteTimeUtc == default ? string.Empty : Timestamps.Format(entry.LastWriteTimeUtc);
|
||||||
|
|
||||||
/// <summary>The mode as <c>drwxr-xr-x</c>, which is the design's <c>PERMS</c> column.</summary>
|
/// <summary>The mode as <c>drwxr-xr-x</c>, which is the design's <c>PERMS</c> column.</summary>
|
||||||
internal string Permissions => entry.Permissions;
|
internal string Permissions => entry.Permissions;
|
||||||
|
|||||||
@@ -4,10 +4,23 @@
|
|||||||
layout systems disagreeing about the same pixels.
|
layout systems disagreeing about the same pixels.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
The three colours the page decides for itself, and they are the v2 design's rather than a second opinion
|
||||||
|
about it. The background, #171a26, is `TerminalSurface` in Palette.axaml — the same surface a snippet's
|
||||||
|
command block and the log view are drawn on. The muted grey is `TextDim`.
|
||||||
|
|
||||||
|
The foreground is the one that is not a palette token: the design sets terminal text to #d8dcea, a step
|
||||||
|
below the #E3E7F4 it uses for interface text, and that difference is deliberate on its part — a wall of
|
||||||
|
monospace at full contrast is harder to read than the labels around it, not easier.
|
||||||
|
|
||||||
|
The page cannot read an Avalonia resource in any case — it is a WebView served over a loopback socket,
|
||||||
|
not part of the visual tree — so these are repeated here and in the theme object in terminal.js, and
|
||||||
|
both sites say so.
|
||||||
|
*/
|
||||||
:root {
|
:root {
|
||||||
--dodo-background: #10131a;
|
--dodo-background: #171a26;
|
||||||
--dodo-foreground: #d5d8de;
|
--dodo-foreground: #d8dcea;
|
||||||
--dodo-muted: #7b8394;
|
--dodo-muted: #8b93b0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -204,8 +204,9 @@ function createSession(sessionId) {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize,
|
fontSize,
|
||||||
scrollback: SCROLLBACK_LINES,
|
scrollback: SCROLLBACK_LINES,
|
||||||
// Matches terminal.css, so the canvas and the page agree on the background.
|
// Matches terminal.css, so the canvas and the page agree on the background. Both are the v2
|
||||||
theme: { background: '#10131a', foreground: '#d5d8de' },
|
// design's terminal surface — `TerminalSurface` in Palette.axaml. See the note in terminal.css.
|
||||||
|
theme: { background: '#171a26', foreground: '#d8dcea' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const fit = new FitAddon.FitAddon();
|
const fit = new FitAddon.FitAddon();
|
||||||
|
|||||||
Reference in New Issue
Block a user