Public Access
Android: the redesign — a Vaults tab with hosts, keychain and snippets, card lists, a keychain that can add, and files that act on what is chosen
The bottom bar is Vaults · Connections · Settings, drawn as Material 3 pills with icons from the embedded Material font and a count of open sessions on Connections, which replaces the strip of shells above the bar. Vaults is the open vault: its header carries the name as a switch to the vaults screen, the sync light and a lock icon, and a Hosts · Keychain · Snippets segmented control sits under it. The keychain and snippets move there from Settings (IsVaultSurface lights the tab; IsMoreSurface no longer lists them), and back from either goes to the hosts. Settings becomes an account card and grouped sections: security, vaults and history, files, this app. Host rows are cards with a terminal tile and the live dot on its corner. The connect sheet names the host and its address, labels its box, focuses it as it opens, reveals the password on request, and puts CANCEL left and CONNECT right; every two-button row on the head now confirms on the right. While a hosts-screen sheet is up the header and bar are dimmed too and a tap on them lowers it, and the bar stands down while the software keyboard is up. The keychain gains a floating + and a sheet of the desktop's five adds (generate a key, paste a key, a password, a bucket, a tag) over the editors VaultViewModel already had, drawn as a full-screen page with labels above the boxes; a chosen item gets EDIT and COPY PUBLIC KEY beside MOVE and DELETE. Snippets get a floating + and cards that say RUNS or INSERTS. Files trade the seven-button block for a selection bar (save, and a ⋮ sheet with rename, move, permissions and a red delete), folder and file tiles, an upload button, and header actions for a terminal on the same host, refresh and close; a terminal's + sheet opens SFTP on that terminal's own host. The theme takes the audit's visual findings: Fluent's ClipToBounds is cleared on glowing buttons so the glow is no longer cut to a square (the desktop's Button.accent too), prose is Montserrat with mono kept for addresses and code, disabled secondary buttons are quieter than enabled ones, Fluent's dropdown, checkbox, switch, focus and selection brushes come from the palette, touch no longer leaves hover fills behind, and lists, nav entries and icon buttons carry names for TalkBack. Builds clean; checked on the emulator end to end: unlock, the three vault views, adding and deleting a password, connecting through the new sheet, files on the terminal's host with the selection bar and its sheet. App tests 727 of 733 pass with 5 skipped; the one failure, TheCountsAPhoneShows_ FollowEveryForwardsState, passes three runs of three alone and is untouched here. Layout tests pass.
This commit is contained in:
1 parent
6806e542f9
commit
5b32c76c95
24 files changed
+2022
-654
No files matched your search
@@ -36,6 +36,156 @@
|
||||
this paragraph to learn it was deliberate.
|
||||
-->
|
||||
|
||||
<!--
|
||||
◆ FLUENT'S OWN BRUSHES, ANSWERED ONCE HERE RATHER THAN CONTROL BY CONTROL.
|
||||
|
||||
The 2026-10-02 audit (M5) found the controls this file never styled still drawing Fluent's defaults
|
||||
among the ones it did: a dropdown with a bright white border on a black fill beside 10-radius fields on
|
||||
#101019, a checkbox in Fluent's blue, a switch drawn as a bare circle, and a focused box whose fill went
|
||||
to pure black. Each of those reads its colours through a DynamicResource key, and a key defined here is
|
||||
found before the theme's own, because Avalonia searches an application's styles last-first and this file
|
||||
is included after FluentTheme. So one block of keys restyles every instance on the head — the ones on
|
||||
screens written later included — where a selector per control would restyle only the ones somebody
|
||||
remembered.
|
||||
|
||||
Every value is a palette brush, never a new colour. The focused border is the accent rather than a
|
||||
grey: a box with the keyboard up is the one control the user is currently writing into, and the
|
||||
redesign gives it the ring that says so.
|
||||
-->
|
||||
<Styles.Resources>
|
||||
<StaticResource x:Key="TextControlBackground" ResourceKey="Field" />
|
||||
<StaticResource x:Key="TextControlBackgroundPointerOver" ResourceKey="Field" />
|
||||
<StaticResource x:Key="TextControlBackgroundFocused" ResourceKey="Field" />
|
||||
<StaticResource x:Key="TextControlBorderBrush" ResourceKey="BorderMid" />
|
||||
<StaticResource x:Key="TextControlBorderBrushPointerOver" ResourceKey="BorderMid" />
|
||||
<StaticResource x:Key="TextControlBorderBrushFocused" ResourceKey="TerminalTabAccent" />
|
||||
<StaticResource x:Key="TextControlForegroundFocused" ResourceKey="Text" />
|
||||
<StaticResource x:Key="TextControlPlaceholderForeground" ResourceKey="TextGhost" />
|
||||
<StaticResource x:Key="TextControlPlaceholderForegroundFocused" ResourceKey="TextGhost" />
|
||||
<StaticResource x:Key="TextControlPlaceholderForegroundPointerOver" ResourceKey="TextGhost" />
|
||||
|
||||
<StaticResource x:Key="ComboBoxBackground" ResourceKey="Field" />
|
||||
<StaticResource x:Key="ComboBoxBackgroundPointerOver" ResourceKey="Field" />
|
||||
<StaticResource x:Key="ComboBoxBackgroundPressed" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="ComboBoxBackgroundUnfocused" ResourceKey="Field" />
|
||||
<StaticResource x:Key="ComboBoxBackgroundDisabled" ResourceKey="Raised" />
|
||||
<StaticResource x:Key="ComboBoxBorderBrush" ResourceKey="BorderMid" />
|
||||
<StaticResource x:Key="ComboBoxBorderBrushPointerOver" ResourceKey="BorderMid" />
|
||||
<StaticResource x:Key="ComboBoxBorderBrushPressed" ResourceKey="TerminalTabAccent" />
|
||||
<StaticResource x:Key="ComboBoxBorderBrushDisabled" ResourceKey="Border" />
|
||||
<StaticResource x:Key="ComboBoxBackgroundBorderBrushFocused" ResourceKey="TerminalTabAccent" />
|
||||
<StaticResource x:Key="ComboBoxBackgroundBorderBrushUnfocused" ResourceKey="BorderMid" />
|
||||
<StaticResource x:Key="ComboBoxForeground" ResourceKey="Text" />
|
||||
<StaticResource x:Key="ComboBoxForegroundFocused" ResourceKey="Text" />
|
||||
<StaticResource x:Key="ComboBoxForegroundFocusedPressed" ResourceKey="Text" />
|
||||
<StaticResource x:Key="ComboBoxDropDownGlyphForeground" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocused" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocusedPressed" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="ComboBoxDropDownBackground" ResourceKey="Chip" />
|
||||
<StaticResource x:Key="ComboBoxDropDownBorderBrush" ResourceKey="BorderHover" />
|
||||
<StaticResource x:Key="ComboBoxItemBackgroundPointerOver" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="ComboBoxItemBackgroundPressed" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="ComboBoxItemBackgroundSelected" ResourceKey="Active" />
|
||||
<StaticResource x:Key="ComboBoxItemBackgroundSelectedPointerOver" ResourceKey="Active" />
|
||||
<StaticResource x:Key="ComboBoxItemBackgroundSelectedPressed" ResourceKey="Active" />
|
||||
<StaticResource x:Key="ComboBoxItemForegroundSelected" ResourceKey="AccentText" />
|
||||
<StaticResource x:Key="ComboBoxItemForegroundSelectedPointerOver" ResourceKey="AccentText" />
|
||||
<StaticResource x:Key="ComboBoxItemForegroundSelectedPressed" ResourceKey="AccentText" />
|
||||
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundFillUnchecked" ResourceKey="Field" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundFillUncheckedPointerOver" ResourceKey="Field" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundFillUncheckedPressed" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundStrokeUnchecked" ResourceKey="TextGhost" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundStrokeUncheckedPointerOver" ResourceKey="TextGhost" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundStrokeUncheckedPressed" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundFillChecked" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundFillCheckedPointerOver" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="CheckBoxCheckBackgroundFillCheckedPressed" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="CheckBoxCheckGlyphForegroundChecked" ResourceKey="AccentInk" />
|
||||
<StaticResource x:Key="CheckBoxCheckGlyphForegroundCheckedPointerOver" ResourceKey="AccentInk" />
|
||||
<StaticResource x:Key="CheckBoxCheckGlyphForegroundCheckedPressed" ResourceKey="AccentInk" />
|
||||
<StaticResource x:Key="CheckBoxForegroundUnchecked" ResourceKey="Text" />
|
||||
<StaticResource x:Key="CheckBoxForegroundUncheckedPointerOver" ResourceKey="Text" />
|
||||
<StaticResource x:Key="CheckBoxForegroundUncheckedPressed" ResourceKey="Text" />
|
||||
<StaticResource x:Key="CheckBoxForegroundChecked" ResourceKey="Text" />
|
||||
<StaticResource x:Key="CheckBoxForegroundCheckedPointerOver" ResourceKey="Text" />
|
||||
<StaticResource x:Key="CheckBoxForegroundCheckedPressed" ResourceKey="Text" />
|
||||
|
||||
<StaticResource x:Key="ToggleSwitchFillOn" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="ToggleSwitchFillOnPointerOver" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="ToggleSwitchFillOnPressed" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="ToggleSwitchStrokeOn" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="ToggleSwitchStrokeOnPointerOver" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="ToggleSwitchStrokeOnPressed" ResourceKey="Accent" />
|
||||
<StaticResource x:Key="ToggleSwitchKnobFillOn" ResourceKey="AccentInk" />
|
||||
<StaticResource x:Key="ToggleSwitchKnobFillOnPointerOver" ResourceKey="AccentInk" />
|
||||
<StaticResource x:Key="ToggleSwitchKnobFillOnPressed" ResourceKey="AccentInk" />
|
||||
<StaticResource x:Key="ToggleSwitchFillOff" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="ToggleSwitchFillOffPointerOver" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="ToggleSwitchFillOffPressed" ResourceKey="Hover" />
|
||||
<StaticResource x:Key="ToggleSwitchStrokeOff" ResourceKey="TextGhost" />
|
||||
<StaticResource x:Key="ToggleSwitchStrokeOffPointerOver" ResourceKey="TextGhost" />
|
||||
<StaticResource x:Key="ToggleSwitchStrokeOffPressed" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="ToggleSwitchKnobFillOff" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="ToggleSwitchKnobFillOffPointerOver" ResourceKey="TextFaint" />
|
||||
<StaticResource x:Key="ToggleSwitchKnobFillOffPressed" ResourceKey="TextFaint" />
|
||||
</Styles.Resources>
|
||||
|
||||
<!--
|
||||
Touch has no hover, so a pointer-over fill is one that sticks after a tap until the next touch lands
|
||||
somewhere else — the grey square the audit found left behind on Vaults' refresh button (M5). The
|
||||
transparent shapes keep their resting fill under the pointer; pressed is the feedback a finger gets.
|
||||
-->
|
||||
<Style Selector="Button.icon:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="Button.row:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="Button.secondary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ A selected row in a list is not lit. The hosts list says why in HostsScreen.axaml — a lit row would be
|
||||
a mark with no action behind it — but only its own Border.hostrow.chosen was ever changed, and Fluent's
|
||||
ListBoxItem:selected went on painting #3D486B, a slate blue the palette has never held, under whichever
|
||||
row had been tapped last (audit M3). Pointer-over goes with it for the reason the button keys above do.
|
||||
-->
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The dropdown, on TextBox.field's numbers so the two sit in one form as one kind of box: 44 tall, a
|
||||
10 radius, Field on BorderMid. The colours come from the keys above; these are the geometry Fluent sets
|
||||
as plain values.
|
||||
-->
|
||||
<Style Selector="ComboBox">
|
||||
<Setter Property="MinHeight" Value="44" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="14,0,10,0" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
A checkbox's caption on the box's own line. Fluent tops the content of a CheckBox taller than its box,
|
||||
so in a 44-pixel row the label sat below the tick's centre (audit M5).
|
||||
-->
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.primary">
|
||||
<Setter Property="Height" Value="48" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
@@ -46,6 +196,14 @@
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<!--
|
||||
◆ Off, because Fluent's Button theme turns it on, and the glow below is drawn outside the control by
|
||||
definition. With the clip on, the blur was cut at the button's square bounds and showed as a lighter
|
||||
rectangle behind the rounded fill on every primary button and the FAB (audit M1). Measured in the
|
||||
desktop's layout harness: ClipToBounds reads True under the theme, and cleared, the glow renders whole.
|
||||
The desktop's Button.accent carries the same setter for the same reason.
|
||||
-->
|
||||
<Setter Property="ClipToBounds" Value="False" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
@@ -86,6 +244,11 @@
|
||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextFaint}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The alternative to a primary button, and in a row of the two it is on the LEFT: Android puts the action
|
||||
that confirms on the right, and every two-button row on this head had it the other way round until the
|
||||
2026-10-04 redesign (audit M12) — CANCEL | SAVE, KEEP | DELETE, CANCEL | CONNECT.
|
||||
-->
|
||||
<Style Selector="Button.secondary">
|
||||
<Setter Property="Height" Value="48" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
@@ -100,6 +263,22 @@
|
||||
<Setter Property="FontSize" Value="11.5" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
<!--
|
||||
◆ Disabled is quieter than enabled, which Fluent had the wrong way round: with no rule here its #3C3C43
|
||||
disabled slab filled the outline, so SFTP's unavailable SAVE FILE was the heaviest button in its row
|
||||
(audit M4). The same flat-and-unlit treatment Button.primary:disabled gets, on an outline.
|
||||
-->
|
||||
<Style Selector="Button.secondary /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="Button.secondary:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||||
</Style>
|
||||
<Style Selector="Button.secondary:disabled /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextGhost}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.danger">
|
||||
<Setter Property="Height" Value="48" />
|
||||
@@ -324,6 +503,8 @@
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="24" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<!-- Off for the glow; see the remark on Button.primary's own setter, which is the same fix. -->
|
||||
<Setter Property="ClipToBounds" Value="False" />
|
||||
</Style>
|
||||
<!-- See the remark above Button.primary's own /template/ ContentPresenter rule: BoxShadow has no home on
|
||||
a Button itself, so the fill moves down here alongside it rather than staying a plain Setter. -->
|
||||
@@ -468,9 +649,9 @@
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="10.5" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
<!-- Sans since the 2026-10-04 redesign: a category is a word, "SSH keys 3", rather than an identifier. -->
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Name="Chip" Background="{TemplateBinding Background}"
|
||||
@@ -493,11 +674,88 @@
|
||||
</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.
|
||||
◆ A segmented control, which this file used to argue against having.
|
||||
|
||||
The argument was that the one the v2 design drew — LOCAL / REMOTE over the file listing — had nothing to
|
||||
switch on a phone, and the choices that were real were chip rows. The 2026-10-04 redesign has a choice
|
||||
that is a segmented control in every sense: the open vault's HOSTS · KEYCHAIN · SNIPPETS, three equal,
|
||||
exclusive views of one thing, always all three, never scrolled. Chips would make it a filter; a well with
|
||||
one lit segment says it is where you are.
|
||||
|
||||
The well is Panel on BorderSubtle at 14, the lit segment Active at 10 — a step inside it on the ladder,
|
||||
as a button sits inside a card. Sans and 14, since these are words rather than identifiers.
|
||||
-->
|
||||
<Style Selector="Border.segmented">
|
||||
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtle}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Padding" Value="4" />
|
||||
</Style>
|
||||
<Style Selector="RadioButton.segment">
|
||||
<Setter Property="MinHeight" Value="40" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="Margin" Value="2,0" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="{TemplateBinding CornerRadius}"
|
||||
MinHeight="{TemplateBinding MinHeight}">
|
||||
<ContentPresenter Content="{TemplateBinding Content}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="RadioButton.segment:checked">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
<Style Selector="RadioButton.segment:pressed">
|
||||
<Setter Property="Background" Value="{StaticResource Hover}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
The eye beside a secret box: outlined like the box it sits beside, lit while it is revealing. Lit with
|
||||
the accent ring the focused box wears, so "you can read this" is said in the same colour as "you are
|
||||
typing here".
|
||||
-->
|
||||
<Style Selector="ToggleButton.reveal">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.reveal /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Field}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.reveal:checked /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TerminalTabAccent}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
One Material icon: a codepoint of the embedded font the desktop draws its icons with, standing where a
|
||||
Unicode glyph stood (audit M7). Hidden from the accessibility tree — the codepoint reads as nothing, and the
|
||||
control around it carries the name — and it inherits its colour, so an icon in a button is the button's.
|
||||
-->
|
||||
<Style Selector="TextBlock.icon">
|
||||
<Setter Property="FontFamily" Value="{StaticResource IconFont}" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox.field">
|
||||
<Setter Property="Height" Value="44" />
|
||||
@@ -657,25 +915,45 @@
|
||||
row's own height around — now say TextWrapping="NoWrap" beside their TextTrimming, which is the pairing
|
||||
that was always meant and was previously being had by accident.
|
||||
-->
|
||||
<!--
|
||||
◆ PROSE IS IN THE SANS FACE NOW, AND THAT IS THE RECORDED DECISION CATCHING UP RATHER THAN A NEW ONE.
|
||||
|
||||
docs/design-import-gaps.md ("The phone catches up") says the phone shares the desktop's faces, and on
|
||||
the desktop a hint, a caption and a settings title are Montserrat while the mono is kept for what is
|
||||
machine-shaped. These three still set MonoFont, so every paragraph on this head was JetBrains Mono —
|
||||
long, wide-set lines that the 2026-10-02 audit (M2) found tiring to read on the enrolment, settings and
|
||||
SFTP screens. They now take the application's default family, which is Montserrat (see
|
||||
DodoSshAndroidApplication.CustomizeAppBuilder).
|
||||
|
||||
One size step up with the change: Montserrat at the old mono sizes set noticeably smaller on the line,
|
||||
and 10-pixel captions were already at the floor. A block that shows an address, a path or a command
|
||||
rather than a sentence says Classes="detail code" at its own site — see TextBlock.detail.code below — so
|
||||
the split is still made where the content is known.
|
||||
-->
|
||||
<Style Selector="TextBlock.detail">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="FontSize" Value="10" />
|
||||
<Setter Property="FontSize" Value="11.5" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.body">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontSize" Value="12.5" />
|
||||
<Setter Property="LineHeight" Value="19" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.title">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
<!--
|
||||
The other half of that split: a detail line that is an address, a path, a fingerprint fragment or a
|
||||
command. Combined with .detail rather than replacing it, so wrapping and colour stay the caption's.
|
||||
-->
|
||||
<Style Selector="TextBlock.detail.code">
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
A screen's own name, in the header. The one place this design is not monospaced: the design sets every
|
||||
|
||||
@@ -56,16 +56,19 @@
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*,Auto,Auto">
|
||||
|
||||
<!-- ============ header ============ -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Height="56" Margin="8,0">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto" Height="56" Margin="8,0"
|
||||
IsVisible="{Binding !HasRemoteBatch}">
|
||||
<!--
|
||||
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
|
||||
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
|
||||
somewhere the layout has removed. See PhoneShell.IsWide.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||
<Button Grid.Column="0" Classes="icon" AutomationProperties.Name="Back"
|
||||
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="heading" Text="SFTP" Margin="4,0"
|
||||
IsVisible="{Binding ShowsHostPicker}" />
|
||||
@@ -78,10 +81,59 @@
|
||||
<TextBlock Text="{Binding ConnectedTo}" TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
|
||||
<Button Grid.Column="3" Classes="icon" Content="↻" Command="{Binding RefreshRemoteCommand}"
|
||||
IsVisible="{Binding IsConnected}" />
|
||||
<!--
|
||||
◆ The other half of this host: a terminal on the machine whose files are showing. The redesign puts
|
||||
the two side by side as one session's two views; this application holds them as two connections,
|
||||
so what this does is the desktop's "Open terminal" — a new shell to the same host, through the same
|
||||
command the session sidebar uses. A bucket has no shell, so it is SFTP only.
|
||||
-->
|
||||
<Button Grid.Column="3" Classes="icon" AutomationProperties.Name="Open a terminal on this host"
|
||||
IsVisible="{Binding IsHostConnected}"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).OpenTerminalForFilesHostCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
<Button Grid.Column="4" Classes="icon" AutomationProperties.Name="Refresh"
|
||||
Command="{Binding RefreshRemoteCommand}" IsVisible="{Binding IsConnected}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
<!-- CLOSE was a button of its own in the block at the foot of the screen; it is the session's, so it
|
||||
is on the session's header. -->
|
||||
<Button Grid.Column="5" Classes="icon" AutomationProperties.Name="Close this session"
|
||||
Command="{Binding DisconnectCommand}" IsVisible="{Binding IsConnected}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
◆ THE SELECTION BAR, in the header's row and in its place, as the hosts screen's action bar is.
|
||||
|
||||
The 2026-10-04 redesign's answer to the audit's M11, where seven buttons sat under the listing whether
|
||||
or not anything was chosen, and four of them answered a tap on nothing with a line in the status. Now
|
||||
the actions appear when there is something to act on: a tap on a file, or a tick. SAVE is on the bar
|
||||
because it is the one somebody reaches for; the four that change a file are behind the ⋮, in a sheet
|
||||
with room to say what each does. The ✕ lets go of the choice.
|
||||
-->
|
||||
<Border Grid.Row="0" Height="56" Background="{StaticResource Active}" Padding="4,0"
|
||||
IsVisible="{Binding HasRemoteBatch}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<Button Grid.Column="0" Classes="icon" AutomationProperties.Name="Clear the selection"
|
||||
Command="{Binding ClearRemoteBatchCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" Foreground="{StaticResource Text}" />
|
||||
</Button>
|
||||
<TextBlock Grid.Column="1" Margin="6,0" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"
|
||||
Foreground="{StaticResource Text}" Text="{Binding RemoteBatchLabel}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="2" Classes="icon" AutomationProperties.Name="Save to this phone"
|
||||
IsEnabled="{Binding CanDownload}" Click="OnSaveFile">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" Foreground="{StaticResource Text}" />
|
||||
</Button>
|
||||
<Button Grid.Column="3" Classes="icon" AutomationProperties.Name="More actions"
|
||||
Command="{Binding OpenEntryMenuCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" Foreground="{StaticResource Text}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ opening one ============ -->
|
||||
<!--
|
||||
ShowsRemoteInvitation rather than !IsConnected, which stopped being the same question when a
|
||||
@@ -164,9 +216,9 @@
|
||||
<!-- ◆ 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"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="TRUST AND OPEN"
|
||||
Command="{Binding TrustHostKeyCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding RejectHostKeyCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -225,12 +277,12 @@
|
||||
</Grid>
|
||||
|
||||
<!-- ============ where we are ============ -->
|
||||
<ScrollViewer Grid.Row="4" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
<ScrollViewer Grid.Row="4" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled"
|
||||
IsVisible="{Binding ShowsRemoteContents}" Margin="0,0,0,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" Margin="14,0" VerticalAlignment="Center">
|
||||
<Button Classes="row" MinHeight="36" Padding="9,0" CornerRadius="10"
|
||||
Command="{Binding RemoteUpCommand}">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="↑" Foreground="{StaticResource AccentText}" />
|
||||
<Button Classes="row" MinHeight="40" Padding="9,0" CornerRadius="10"
|
||||
AutomationProperties.Name="Up one folder" Command="{Binding RemoteUpCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="18" Foreground="{StaticResource AccentText}" />
|
||||
</Button>
|
||||
<ItemsControl ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -241,7 +293,7 @@
|
||||
<Button Classes="row" MinHeight="36" Padding="7,0" CornerRadius="10"
|
||||
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
||||
CommandParameter="{Binding Path}">
|
||||
<TextBlock Classes="detail" FontSize="11" Text="{Binding Name}" />
|
||||
<TextBlock Classes="detail code" FontSize="11" Text="{Binding Name}" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -265,7 +317,7 @@
|
||||
answers: an empty scrolling strip under the breadcrumb would read as a loading row rather than as
|
||||
"this host has nothing pinned".
|
||||
-->
|
||||
<ScrollViewer Grid.Row="5" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
<ScrollViewer Grid.Row="5" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled"
|
||||
IsVisible="{Binding HasConnectedPins}" Margin="0,0,0,4">
|
||||
<ItemsControl ItemsSource="{Binding ConnectedPinnedPaths}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -317,6 +369,8 @@
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Margin" Value="10,1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<!-- A name for TalkBack rather than the row view model's type name (audit H4). -->
|
||||
<Setter Property="AutomationProperties.Name" Value="{ReflectionBinding Name}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
@@ -339,25 +393,33 @@
|
||||
<CheckBox Grid.Column="0" Width="40" MinWidth="0" VerticalAlignment="Center"
|
||||
IsChecked="{Binding IsChecked}" Click="OnRemoteRowChecked" />
|
||||
|
||||
<TextBlock Grid.Column="1" Width="16" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" FontSize="12" Text="▸"
|
||||
Foreground="{StaticResource AccentText}" IsVisible="{Binding IsNavigable}" />
|
||||
<TextBlock Grid.Column="1" Width="16" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" FontSize="12" Text="·"
|
||||
Foreground="{StaticResource TextFaint}" IsVisible="{Binding IsFile}" />
|
||||
<!--
|
||||
◆ A folder looks like a folder now. It was a 12-pixel purple ▸, and a file a dot, which on a
|
||||
listing of names that are all set in the same face left a directory to be told apart by a
|
||||
glyph a thumb covers (audit M11). The tiles are the redesign's: the accent's wash for a place
|
||||
to go, a quieter one for a thing to take.
|
||||
-->
|
||||
<Border Grid.Column="1" Width="38" Height="38" CornerRadius="10"
|
||||
Background="{StaticResource AccentSoft}" IsVisible="{Binding IsNavigable}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="20" Foreground="{StaticResource AccentText}" />
|
||||
</Border>
|
||||
<Border Grid.Column="1" Width="38" Height="38" CornerRadius="10"
|
||||
Background="{StaticResource Hover}" IsVisible="{Binding IsFile}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="20" Foreground="{StaticResource TextFaint}" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="2" Margin="11,0,0,0" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="12.5" Text="{Binding Name}"
|
||||
<StackPanel Grid.Column="2" Margin="12,0,0,0" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<TextBlock Classes="detail" Text="{Binding Size}"
|
||||
<TextBlock Classes="detail code" 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}" />
|
||||
<TextBlock Classes="detail code" Text="{Binding Permissions}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="3" Classes="detail" Text="{Binding Modified}"
|
||||
<TextBlock Grid.Column="3" Classes="detail code" Text="{Binding Modified}"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -410,7 +472,7 @@
|
||||
<!-- ============ what to do with the chosen entry ============ -->
|
||||
<!-- DeepChrome rather than Chrome, since v5 — see the remark on PhoneShell's vault header. -->
|
||||
<Border Grid.Row="8" IsVisible="{Binding IsConnected}" Background="{StaticResource DeepChrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="12,10">
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="14,8">
|
||||
<StackPanel Spacing="9">
|
||||
|
||||
<!-- ◆ Counted, and it replaces the actions in place rather than stacking over the row it names. -->
|
||||
@@ -423,9 +485,9 @@
|
||||
<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"
|
||||
<Button Grid.Column="2" Classes="danger" Height="44" Content="DELETE"
|
||||
Command="{Binding ConfirmDeleteRemoteCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="KEEP"
|
||||
Command="{Binding CancelDeleteRemoteCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -443,7 +505,7 @@
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||||
Text="{Binding PendingRemoteMove.Question}" />
|
||||
<TextBlock Classes="detail" TextWrapping="Wrap" Text="{Binding PendingRemoteMove.FullPath}" />
|
||||
<TextBlock Classes="detail code" TextWrapping="Wrap" Text="{Binding PendingRemoteMove.FullPath}" />
|
||||
<TextBox Classes="field" Text="{Binding RemoteMoveTarget}"
|
||||
PlaceholderText="{Binding PendingRemoteMove.FieldHint}">
|
||||
<TextBox.KeyBindings>
|
||||
@@ -452,10 +514,10 @@
|
||||
</TextBox>
|
||||
<TextBlock Classes="body" Text="{Binding PendingRemoteMove.Note}" />
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44"
|
||||
Content="{Binding PendingRemoteMove.ConfirmLabel}"
|
||||
Command="{Binding ConfirmMoveRemoteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelMoveRemoteCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -521,10 +583,10 @@
|
||||
Text="{Binding PendingRemotePermissions.Preview}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="APPLY"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="APPLY"
|
||||
Command="{Binding ConfirmRemotePermissionsCommand}"
|
||||
IsEnabled="{Binding PendingRemotePermissions.HasChanges}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelRemotePermissionsCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -579,54 +641,13 @@
|
||||
IsVisible="{Binding HasTransfers}" Command="{Binding ClearCompletedCommand}" />
|
||||
|
||||
<!--
|
||||
◆ The two directions, on their own row above the pair below — because DELETE is the button on this
|
||||
screen that nothing can undo, and it must not sit at a thumb's width from the ones somebody presses
|
||||
often. ADD FILES is the primary of the two: it is the one that needs no selection, and the one this
|
||||
screen exists for on a phone.
|
||||
|
||||
SAVE FILE takes the selected row rather than several, and that asymmetry is the platform's: the
|
||||
save picker names one destination. CanDownload is the desktop's own flag for the same question —
|
||||
a file is selected and something is connected — and is reused rather than restated here.
|
||||
◆ The seven buttons that were here are gone, and every one of them went somewhere it is wanted:
|
||||
ADD FILES is the floating button over the listing, SAVE FILE is on the selection bar, RENAME, MOVE,
|
||||
PERMS and DELETE are in the sheet behind its ⋮, and CLOSE is on the header. The paragraph explaining
|
||||
the first two went with them, to the sheet's own SAVE row, where it is read at the moment it is true.
|
||||
What is left down here is what is genuinely about the session as a whole: the questions above, the
|
||||
queue, and the status.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="ADD FILES" Click="OnAddFiles" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="SAVE FILE" Click="OnSaveFile"
|
||||
IsEnabled="{Binding CanDownload}" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
◆ The two that put a file right rather than taking it away, above the row DELETE is on and away
|
||||
from it. Both open a form in place of these buttons — see the card above — because a phone has
|
||||
nowhere to put a text field except where something else already was.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="RENAME"
|
||||
Command="{Binding RenameRemoteCommand}" IsEnabled="{Binding CanRenameRemote}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="MOVE"
|
||||
Command="{Binding MoveRemoteCommand}" IsEnabled="{Binding CanMoveRemote}" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
◆ PERMS beside DELETE rather than on the row above, which is full: it is the fourth thing that can
|
||||
be done to a chosen row and the third that puts one right. Dark on a bucket, where an object has an
|
||||
access policy rather than nine bits — see CanChangeRemotePermissions.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="PERMS"
|
||||
Command="{Binding ChangeRemotePermissionsCommand}"
|
||||
IsEnabled="{Binding CanChangeRemotePermissions}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="DELETE"
|
||||
Command="{Binding DeleteRemoteCommand}" IsEnabled="{Binding CanDeleteRemote}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CLOSE"
|
||||
Command="{Binding DisconnectCommand}" />
|
||||
<Panel Grid.Column="2" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="body" IsVisible="{Binding !IsAskingAboutRemoteEntry}"
|
||||
Text="ADD FILES picks documents to send. SAVE FILE asks where the selected file should be kept — the file is created there when you choose it, so a transfer that fails leaves it empty." />
|
||||
|
||||
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
||||
Text="{Binding Status}" />
|
||||
@@ -634,6 +655,105 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ============ add ============ -->
|
||||
<!--
|
||||
◆ ADD FILES as the floating button, the one shape every add on this head takes. In the listing's row
|
||||
so it rides with the list rather than over the questions and the queue under it. Not while something
|
||||
is chosen: the bar across the top is then about that, and an add beside it would be a second subject.
|
||||
-->
|
||||
<Button Grid.Row="6" Classes="fab" Margin="0,0,18,18" HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
AutomationProperties.Name="Add files from this phone" Click="OnAddFiles">
|
||||
<Button.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="IsConnected" />
|
||||
<Binding Path="!HasRemoteBatch" />
|
||||
</MultiBinding>
|
||||
</Button.IsVisible>
|
||||
<TextBlock Classes="icon" Text="" FontSize="24" />
|
||||
</Button>
|
||||
|
||||
<!-- ============ ◆ what can be done to the chosen rows ============ -->
|
||||
<!--
|
||||
The add sheet's shape on the hosts screen, doing the files screen's job: a scrim, a bottom panel with
|
||||
its top corners rounded, dismissible because "which of these" has no wrong answer. DELETE is under a rule
|
||||
and in red — it was the one neutral DELETE on this head (audit M11) — and each entry is dark rather than
|
||||
gone when it cannot be done, so the four keep their places under a thumb.
|
||||
|
||||
Each entry also lowers the sheet as it runs: three of the four open a question in the space under the
|
||||
listing, and the sheet left over it would be the one thing in the way of answering.
|
||||
-->
|
||||
<Panel Grid.Row="0" Grid.RowSpan="9" IsVisible="{Binding IsEntryMenuOpen}">
|
||||
<Button Classes="scrim" Command="{Binding CloseEntryMenuCommand}" />
|
||||
|
||||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="22,22,0,0" Padding="12,10,12,16">
|
||||
<StackPanel>
|
||||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,12" />
|
||||
|
||||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold" Margin="12,0,12,8"
|
||||
Text="{Binding RemoteBatchLabel}" TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<Button Classes="row" IsEnabled="{Binding CanDownload}" Click="OnSaveFile"
|
||||
AutomationProperties.Name="Save to this phone">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Save to this phone" />
|
||||
<TextBlock Classes="detail" Text="Asks where first. The file is made there when you choose, so a transfer that fails leaves it empty." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding RenameRemoteCommand}" IsEnabled="{Binding CanRenameRemote}"
|
||||
Click="OnEntryMenuChosen" AutomationProperties.Name="Rename">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22" />
|
||||
<TextBlock Grid.Column="2" FontSize="14.5" FontWeight="SemiBold" Text="Rename"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding MoveRemoteCommand}" IsEnabled="{Binding CanMoveRemote}"
|
||||
Click="OnEntryMenuChosen" AutomationProperties.Name="Move">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22" />
|
||||
<TextBlock Grid.Column="2" FontSize="14.5" FontWeight="SemiBold" Text="Move"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding ChangeRemotePermissionsCommand}"
|
||||
IsEnabled="{Binding CanChangeRemotePermissions}" Click="OnEntryMenuChosen"
|
||||
AutomationProperties.Name="Permissions">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Permissions" />
|
||||
<TextBlock Classes="detail" Text="Read, write and execute for the owner, the group and everybody else." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Border Height="1" Background="{StaticResource Border}" Margin="12,6" />
|
||||
|
||||
<Button Classes="row" Command="{Binding DeleteRemoteCommand}" IsEnabled="{Binding CanDeleteRemote}"
|
||||
Click="OnEntryMenuChosen" AutomationProperties.Name="Delete">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource DangerText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource DangerText}"
|
||||
Text="Delete" />
|
||||
<TextBlock Classes="detail" Text="Asks first, and says how many. There is no undo." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -41,6 +41,19 @@ internal sealed partial class FilesScreen : UserControl
|
||||
/// rather than of any row, and each listing replaces every row it has. See
|
||||
/// <c>TransfersViewModel.RemoteChecksChanged</c>.
|
||||
/// </remarks>
|
||||
/// <summary>An entry in the actions sheet was chosen: the sheet goes down as its command runs.</summary>
|
||||
/// <remarks>
|
||||
/// Three of the four open a question under the listing, and the sheet left over it would be the one thing
|
||||
/// in the way of answering. The command is bound in the markup; this only lowers the sheet.
|
||||
/// </remarks>
|
||||
private void OnEntryMenuChosen(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is TransfersViewModel transfers)
|
||||
{
|
||||
transfers.IsEntryMenuOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRemoteRowChecked(object? sender, RoutedEventArgs e) =>
|
||||
(DataContext as TransfersViewModel)?.RemoteChecksChanged();
|
||||
|
||||
@@ -116,6 +129,11 @@ internal sealed partial class FilesScreen : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// From the actions sheet as well as the bar, and the sheet goes down either way: the save picker is
|
||||
// about to cover the screen, and coming back to the sheet still up would be coming back to a question
|
||||
// already answered.
|
||||
transfers.IsEntryMenuOpen = false;
|
||||
|
||||
if (transfers.SelectedRemoteEntry is not { IsFile: true } row)
|
||||
{
|
||||
transfers.Status = "Choose a file on the host to save.";
|
||||
|
||||
@@ -67,15 +67,15 @@
|
||||
held as ids rather than as rows, see VaultViewModel.chosenHostIds — so searching for the second half
|
||||
of a selection and ticking it is a thing this screen supports rather than merely survives.
|
||||
-->
|
||||
<Border Grid.Row="0" Margin="14,10,14,4" Background="{StaticResource Field}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="10" Height="44">
|
||||
<Border Grid.Row="0" Margin="16,10,16,4" Background="{StaticResource Field}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="12" Height="46">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Text="⌕" Foreground="{StaticResource TextFaint}" FontSize="13"
|
||||
Margin="13,0,0,0" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" Foreground="{StaticResource TextFaint}"
|
||||
FontSize="20" Margin="12,0,0,0" />
|
||||
<TextBox Grid.Column="1" Text="{Binding HostFilter}" PlaceholderText="Search hosts"
|
||||
Padding="9,0" VerticalContentAlignment="Center"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Foreground="{StaticResource Text}" FontFamily="{StaticResource MonoFont}" FontSize="12" />
|
||||
Foreground="{StaticResource Text}" FontSize="14" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -181,11 +181,11 @@
|
||||
Text="{Binding ChosenBindingToBringNote}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44"
|
||||
Content="{Binding ChosenHostVaultPanelVerb}"
|
||||
Command="{Binding ConfirmSendChosenHostsToAVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelSendChosenHostsToAVaultCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -224,9 +224,9 @@
|
||||
Text="A group lends its port, its account and its key to every host filed under it that says nothing itself, so this can change what these machines dial. Nothing else about them moves." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="FILE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="FILE"
|
||||
Command="{Binding ConfirmRegroupChosenHostsCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelRegroupChosenHostsCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -250,9 +250,9 @@
|
||||
Text="{Binding PendingDeletion.Usage}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||
<Button Grid.Column="0" Classes="danger" Height="44" Content="DELETE"
|
||||
<Button Grid.Column="2" Classes="danger" Height="44" Content="DELETE"
|
||||
Command="{Binding ConfirmDeleteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||||
Command="{Binding CancelDeleteCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -291,9 +291,9 @@
|
||||
</CheckBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||
<Button Grid.Column="0" Classes="danger" Height="44" Content="DELETE"
|
||||
<Button Grid.Column="2" Classes="danger" Height="44" Content="DELETE"
|
||||
Command="{Binding ConfirmDeleteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||||
Command="{Binding CancelDeleteCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -338,9 +338,9 @@
|
||||
Text="The group goes with the groups inside it and every host filed under them, all re-encrypted with the other vault's key. Any group it is nested under stays behind, so it arrives at the top level, and the hosts' tags stay behind with it." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="MOVE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="MOVE"
|
||||
Command="{Binding ConfirmMoveGroupCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelMoveGroupCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -374,11 +374,21 @@
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Margin" Value="10,1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Margin" Value="16,4" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<!--
|
||||
A name for TalkBack. Without it the item announced its view model's type name —
|
||||
"DodoSSH.Client.Shell.ViewModels.HostRowViewModel" (audit H4). Both row kinds in this list carry a
|
||||
Label, and a reflection binding is what lets one setter serve the two of them.
|
||||
-->
|
||||
<Setter Property="AutomationProperties.Name" Value="{ReflectionBinding Label}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ A host is a card since the 2026-10-04 redesign: Panel on a hairline at the 16 radius, where it was a
|
||||
transparent row on the canvas. A list of machines is a list of things, each one a target, and a card
|
||||
says where one ends and the next begins without a rule between them.
|
||||
|
||||
◆ The ticked row, filled. On a Border inside the item rather than on the item's own selected state,
|
||||
because "ticked" is a property of the host and the list has no idea about it — the fill and the
|
||||
tick at the right-hand end are the same fact drawn twice, which is deliberate: a fill alone is
|
||||
@@ -386,11 +396,14 @@
|
||||
360dp row.
|
||||
-->
|
||||
<Style Selector="Border.hostrow">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
</Style>
|
||||
<Style Selector="Border.hostrow.chosen">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TerminalTabAccent}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
|
||||
@@ -402,7 +415,7 @@
|
||||
the filter, which is what a heading over a filtered list has to say.
|
||||
-->
|
||||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" Margin="8,12,8,5">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" Margin="4,14,0,2">
|
||||
<TextBlock Grid.Column="0" Classes="detail" Text="{Binding Chevron}" VerticalAlignment="Center" />
|
||||
|
||||
<!--
|
||||
@@ -419,8 +432,9 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Count}" FontSize="9"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Grid.Column="2" Classes="tag outline" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Count}" />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ The phone's only way to do anything to a group. The desktop reaches all three from a menu on
|
||||
@@ -435,25 +449,40 @@
|
||||
|
||||
Hidden on the ungrouped heading, which has no group behind it to act on.
|
||||
-->
|
||||
<Button Grid.Column="3" Classes="icon" FontSize="15" Content="⋯" Margin="4,0,0,0"
|
||||
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0"
|
||||
AutomationProperties.Name="{Binding Label, StringFormat='Group {0}: edit, move or delete'}"
|
||||
IsVisible="{Binding GroupId, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Command="{Binding $parent[views:HostsScreen].((vm:VaultViewModel)DataContext).OpenGroupSheetCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="20" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- A host. -->
|
||||
<DataTemplate DataType="vm:HostRowViewModel">
|
||||
<Border Classes="hostrow" Classes.chosen="{Binding IsChosen}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" MinHeight="56" Margin="14,12">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" MinHeight="48" Margin="12,11">
|
||||
|
||||
<!-- 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}" Width="8" Height="8"
|
||||
VerticalAlignment="Center" />
|
||||
<!--
|
||||
◆ The redesign's tile: a terminal mark on the accent's wash, with the live dot on its corner.
|
||||
Green still means one thing only — a terminal is open on this host right now, see the .dot
|
||||
style — and riding the tile it says which machine without spending a column on it.
|
||||
-->
|
||||
<Panel Grid.Column="0" Width="42" Height="42" VerticalAlignment="Center">
|
||||
<Border CornerRadius="12" Background="{StaticResource AccentSoft}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="21" Foreground="{StaticResource AccentText}" />
|
||||
</Border>
|
||||
<Border Width="13" Height="13" CornerRadius="7" Background="{StaticResource Panel}"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-3,-3"
|
||||
IsVisible="{Binding IsConnected}">
|
||||
<Ellipse Classes="dot live" Width="9" Height="9" />
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="12,0,0,0" Spacing="3" VerticalAlignment="Center">
|
||||
<StackPanel Grid.Column="1" Margin="14,0,0,0" Spacing="4" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="14" FontWeight="SemiBold"
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="15" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="tag outline" IsVisible="{Binding HasVaultBadge}">
|
||||
<TextBlock Text="{Binding VaultBadge}" />
|
||||
@@ -464,10 +493,10 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||||
<TextBlock Classes="detail code" FontSize="11.5" Text="{Binding Address}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="·" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Authentication}" />
|
||||
<TextBlock Classes="detail" Text="·" />
|
||||
<TextBlock Classes="detail" Text="{Binding Authentication}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
@@ -499,8 +528,8 @@
|
||||
Background="{StaticResource LiveWash}">
|
||||
<TextBlock Text="live" Foreground="{StaticResource Live}" />
|
||||
</Border>
|
||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||
VerticalAlignment="Center" IsVisible="{Binding !IsConnected}" />
|
||||
<TextBlock Grid.Column="2" Classes="icon" Text="" Foreground="{StaticResource TextGhost}"
|
||||
FontSize="22" IsVisible="{Binding !IsConnected}" />
|
||||
|
||||
<!--
|
||||
◆ The tick, in a column of its own at the far end rather than in place of the dot or the
|
||||
@@ -509,9 +538,8 @@
|
||||
choosing a host hide a fact about it. An Auto column takes no width while the mark is
|
||||
collapsed, so a list with nothing ticked is laid out exactly as it was.
|
||||
-->
|
||||
<TextBlock Grid.Column="3" Text="✓" Margin="10,0,0,0" FontSize="15"
|
||||
Foreground="{StaticResource AccentText}" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsChosen}" />
|
||||
<TextBlock Grid.Column="3" Classes="icon" Text="" Margin="10,0,0,0" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" IsVisible="{Binding IsChosen}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
@@ -604,9 +632,9 @@
|
||||
Text="A host inside this group uses these wherever it says nothing itself. They are read at connect time rather than copied in, so changing one here changes every host that never overrode it." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="SAVE"
|
||||
Command="{Binding SaveGroupCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelGroupEditCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -628,10 +656,13 @@
|
||||
twelfth machine while eleven are ticked for deletion is worse, because it does something and the
|
||||
something is not what the screen is about.
|
||||
-->
|
||||
<Button Grid.Row="2" Classes="fab" Content="+" Margin="0,0,18,18"
|
||||
<Button Grid.Row="2" Classes="fab" Margin="0,0,18,18"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
AutomationProperties.Name="Add a host or a group"
|
||||
IsVisible="{Binding ShowsAddButton}"
|
||||
Command="{Binding OpenAddSheetCommand}" />
|
||||
Command="{Binding OpenAddSheetCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="26" />
|
||||
</Button>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -646,9 +677,10 @@
|
||||
"Which kind of thing are you adding" has no wrong answer and no answer at all is one of them.
|
||||
|
||||
And it lives here rather than in PhoneShell. HostKeySheet sits up there because the files screen raises
|
||||
it too; nothing but this screen raises this. The cost is that the scrim covers the screen and not the
|
||||
64px nav bar underneath, so the four destinations stay lit and tappable — which for a menu is right:
|
||||
navigating away is a perfectly good way to decide not to add anything.
|
||||
it too; nothing but this screen raises this. The cost used to be that the scrim covered the screen and
|
||||
not the header or the nav bar, which stayed lit and tappable round every sheet (audit M12). PhoneShell
|
||||
now dims those two rows as well while any sheet here is up, and a tap on them lowers the sheet — see
|
||||
PhoneShell.ShowsSheetScrim.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsAddSheetOpen}">
|
||||
|
||||
@@ -670,7 +702,7 @@
|
||||
|
||||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding NewHostCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="New host" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="New host" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="A machine to connect to, filed under a group if you want one." />
|
||||
</StackPanel>
|
||||
@@ -678,7 +710,7 @@
|
||||
|
||||
<Button Classes="row" Command="{Binding NewGroupCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="New group" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="New group" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="A heading for hosts, and the port, user and key they fall back to." />
|
||||
</StackPanel>
|
||||
@@ -742,19 +774,19 @@
|
||||
|
||||
<Button Classes="row" IsVisible="{Binding HasOneChosenHost}"
|
||||
Command="{Binding ConnectToChosenHostCommand}">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Text="Connect" />
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" IsVisible="{Binding HasOneChosenHost}"
|
||||
Command="{Binding BrowseChosenHostCommand}">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Text="Connect via SFTP" />
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding MoveChosenHostsToVaultCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Move to vault" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Move to vault" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Re-encrypted with the other vault's key. Groups and tags stay behind." />
|
||||
</StackPanel>
|
||||
@@ -762,19 +794,19 @@
|
||||
|
||||
<Button Classes="row" Command="{Binding CopyChosenHostsToVaultCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Copy to vault" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Copy to vault" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="A second copy over there, and these stay where they are." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding RegroupChosenHostsCommand}">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Text="Change group" />
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding DuplicateChosenHostsCommand}">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
Text="Duplicate" />
|
||||
</Button>
|
||||
|
||||
@@ -782,7 +814,7 @@
|
||||
|
||||
<Button Classes="row" Command="{Binding DeleteChosenHostsCommand}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Danger}" Text="Remove" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Asks first, and says how many. There is no undo." />
|
||||
@@ -816,34 +848,73 @@
|
||||
the box the next tap reads, and it would satisfy the length check that decides whether to raise this at
|
||||
all. See CancelConnectPassword.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsAskingForConnectPassword}">
|
||||
<Panel x:Name="PasswordSheet" IsVisible="{Binding IsAskingForConnectPassword}">
|
||||
|
||||
<Button Classes="scrim" Command="{Binding CancelConnectPasswordCommand}" />
|
||||
|
||||
<!--
|
||||
◆ The 2026-10-04 redesign's connect sheet. Three changes from the audit (M12, and the placeholder-as-
|
||||
label finding), and one from the redesign itself:
|
||||
|
||||
· CANCEL on the left and CONNECT on the right, which is the order Android puts a confirming action
|
||||
in — the sheet had them the other way round, and so did every two-button row on this head;
|
||||
· the box has a label over it, so what it is for is still on screen once something is typed;
|
||||
· it takes the focus as it opens — see HostsScreen.FocusThePasswordBox — so the keyboard is up
|
||||
without a second tap on the one box the sheet exists for;
|
||||
· the host is named at the top with its address under it, in the card shape the list draws it in,
|
||||
and a ✕ beside it closes the sheet as the scrim does.
|
||||
|
||||
The eye beside the box reveals what was typed, for the one case a phone keyboard makes common: a
|
||||
password typed blind on glass is a password typed wrong. It is TextBox.RevealPassword, so the secret
|
||||
is never copied anywhere to be shown.
|
||||
-->
|
||||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||||
<StackPanel Spacing="10">
|
||||
CornerRadius="22,22,0,0" Padding="20,10,20,18">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,6" />
|
||||
HorizontalAlignment="Center" Margin="0,0,0,4" />
|
||||
|
||||
<!--
|
||||
It names the host, as the bar did and for the same reason: the row it was tapped on is behind a
|
||||
sheet and a scrim, and "which machine is this password for" is the one thing somebody about to
|
||||
type a secret has to be sure of.
|
||||
-->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="label" Text="CONNECT TO" />
|
||||
<TextBlock Classes="mono" FontSize="11" VerticalAlignment="Center"
|
||||
Text="{Binding SelectedHost.Label}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="Auto,12,*,Auto">
|
||||
<Border Grid.Column="0" Width="42" Height="42" CornerRadius="12"
|
||||
Background="{StaticResource AccentSoft}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="21" Foreground="{StaticResource AccentText}" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="15" FontWeight="SemiBold"
|
||||
Text="{Binding SelectedHost.Label}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail code" Text="{Binding SelectedHost.Address}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="3" Classes="icon" AutomationProperties.Name="Close"
|
||||
Command="{Binding CancelConnectPasswordCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<TextBox Classes="field secret" Text="{Binding ConnectPassword}" PlaceholderText="password">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding ConnectCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="label" Text="PASSWORD" />
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox x:Name="PasswordBox" Grid.Column="0" Classes="field secret" Height="48"
|
||||
Text="{Binding ConnectPassword}" PlaceholderText="the account's password"
|
||||
AutomationProperties.Name="Password">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding ConnectCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<ToggleButton Grid.Column="2" Classes="reveal" Width="48" Height="48"
|
||||
AutomationProperties.Name="Show the password"
|
||||
IsChecked="{Binding #PasswordBox.RevealPassword}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="21" />
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
The tick is the phone's whole answer to storing a password, and on this head it is the only one:
|
||||
@@ -853,15 +924,15 @@
|
||||
quietly lives.
|
||||
-->
|
||||
<CheckBox IsChecked="{Binding RemembersConnectPassword}" MinHeight="44">
|
||||
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||||
<TextBlock FontSize="13.5" TextWrapping="Wrap" Foreground="{StaticResource Text}"
|
||||
Text="Remember this password for this host" />
|
||||
</CheckBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="CONNECT"
|
||||
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Grid ColumnDefinitions="*,10,*">
|
||||
<Button Grid.Column="0" Classes="secondary" Height="48" Content="CANCEL"
|
||||
Command="{Binding CancelConnectPasswordCommand}" />
|
||||
<Button Grid.Column="2" Classes="primary" Height="48" Content="CONNECT"
|
||||
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
@@ -912,7 +983,7 @@
|
||||
Command="{Binding EditGroupFromHeadingCommand}"
|
||||
CommandParameter="{Binding GroupSheet}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Edit" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Edit" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="Its name, where it sits, and the port, user and key its hosts fall back to." />
|
||||
</StackPanel>
|
||||
@@ -922,7 +993,7 @@
|
||||
Command="{Binding MoveGroupFromHeadingCommand}"
|
||||
CommandParameter="{Binding GroupSheet}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}"
|
||||
Text="Move to another vault" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Takes the groups inside it and every host filed under them, re-encrypted with the other vault's key." />
|
||||
@@ -935,7 +1006,7 @@
|
||||
Command="{Binding DeleteGroupFromHeadingCommand}"
|
||||
CommandParameter="{Binding GroupSheet}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Danger}" Text="Delete" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Asks first, and asks separately whether the hosts filed under it go too." />
|
||||
@@ -1066,10 +1137,10 @@
|
||||
<TextBlock Classes="detail" TextWrapping="Wrap"
|
||||
Text="Added to the keychain as soon as you press ADD, so it stays even if you leave this host without saving." />
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Content="ADD" MinHeight="44"
|
||||
<Button Grid.Column="2" Classes="primary" Content="ADD" MinHeight="44"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Command="{Binding AddEditorCredentialCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Content="CANCEL" MinHeight="44"
|
||||
<Button Grid.Column="0" Classes="secondary" Content="CANCEL" MinHeight="44"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Command="{Binding CancelEditorCredentialCommand}" />
|
||||
</Grid>
|
||||
@@ -1265,9 +1336,9 @@
|
||||
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,6,0,0">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="SAVE"
|
||||
Command="{Binding SaveHostCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelEditCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
@@ -62,8 +63,30 @@ internal sealed partial class HostsScreen : UserControl
|
||||
|
||||
InputElement.SetIsHoldingEnabled(rows, true);
|
||||
rows.Holding += OnRowHeld;
|
||||
|
||||
var sheet = this.FindControl<Panel>("PasswordSheet")!;
|
||||
var box = this.FindControl<TextBox>("PasswordBox")!;
|
||||
|
||||
sheet.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.Property == IsVisibleProperty && sheet.IsVisible)
|
||||
{
|
||||
FocusThePasswordBox(box);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the focus, and so the keyboard, in the connect sheet's box as the sheet opens.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The sheet exists for that one box, and it used to open with nothing focused — a second tap before
|
||||
/// typing could start (audit M12). Posted at <c>Loaded</c> priority because the box is not yet laid out
|
||||
/// on the turn the sheet's visibility changes, and focusing a control with no bounds raises no keyboard.
|
||||
/// </remarks>
|
||||
private static void FocusThePasswordBox(TextBox box) =>
|
||||
Dispatcher.UIThread.Post(() => box.Focus(NavigationMethod.Tab), DispatcherPriority.Loaded);
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the row that was tapped, or ticks it where hosts are already ticked.
|
||||
/// </summary>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:views="using:DodoSSH.Client.Android.Views"
|
||||
xmlns:ssh="using:DodoSSH.Client.Ssh"
|
||||
x:Class="DodoSSH.Client.Android.Views.KeychainScreen"
|
||||
x:DataType="vm:VaultViewModel"
|
||||
Background="{StaticResource Canvas}">
|
||||
@@ -32,38 +33,42 @@
|
||||
This screen moved behind SETTINGS, so it draws its own header — the one every hub screen has. It is not
|
||||
a decoration: the shell's header stands down behind the hub, and without a row of its own there would be
|
||||
no back arrow and nothing on screen saying what this list is.
|
||||
|
||||
── 2026-10-04 redesign ───────────────────────────────────────────────────────────────────────────────
|
||||
And it moved back out, under VAULTS: the keychain is the open vault's contents as much as its hosts are,
|
||||
so it is the middle of the vault's HOSTS · KEYCHAIN · SNIPPETS switch, and the shell's header is over it
|
||||
again. The header below is now drawn only where that switch is not.
|
||||
-->
|
||||
|
||||
<Panel>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
|
||||
<!-- ============ header ============ -->
|
||||
<!--
|
||||
The same shape as the one over PREFERENCES in PhoneShell, and pointed at the same place. Back goes to
|
||||
the hub rather than to HOSTS, because the hub is where this screen is reached from and back that
|
||||
skipped it would strand the four rows underneath it.
|
||||
◆ Wide surfaces only, since the 2026-10-04 redesign. On a narrow one this screen is the KEYCHAIN third of
|
||||
the open vault's switch, under the vault's own header — see PhoneShell.ShowsVaultTabs — and a second row
|
||||
saying "Keychain" under a lit KEYCHAIN segment would be the two rows of chrome this head avoids. Its back
|
||||
arrow went with it: back from here is the system gesture, which goes to the vault's hosts.
|
||||
|
||||
A wide surface has no switch — the rail lists the three — so the screen's own name stays there, without
|
||||
an arrow, for the reason it never had one there: the rail is how you leave.
|
||||
-->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*" Height="56" Margin="8,0">
|
||||
<!--
|
||||
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
|
||||
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
|
||||
somewhere the layout has removed. See PhoneShell.IsWide.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Text="Keychain" Margin="4,0" />
|
||||
<Grid Grid.Row="0" Height="56" Margin="12,0" IsVisible="{Binding $parent[views:PhoneShell].IsWide}">
|
||||
<TextBlock Classes="heading" Text="Keychain" Margin="4,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- ============ categories ============ -->
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
<!-- Hidden rather than Auto: the row still scrolls, but Fluent's desktop scrollbar was drawn across the
|
||||
bottom of the chips themselves (audit M5). -->
|
||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled"
|
||||
Margin="0,12,0,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="16,0">
|
||||
|
||||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsAll, Mode=OneWay}"
|
||||
Command="{Binding ShowSectionCommand}" CommandParameter="{x:Static vm:VaultSection.All}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="ALL" />
|
||||
<TextBlock Text="All" />
|
||||
<TextBlock Text="{Binding TotalItemCount}" />
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
@@ -71,7 +76,7 @@
|
||||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsKeys, Mode=OneWay}"
|
||||
Command="{Binding ShowSectionCommand}" CommandParameter="{x:Static vm:VaultSection.Keys}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="SSH KEYS" />
|
||||
<TextBlock Text="SSH keys" />
|
||||
<TextBlock Text="{Binding Keys.Count}" />
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
@@ -80,7 +85,7 @@
|
||||
Command="{Binding ShowSectionCommand}"
|
||||
CommandParameter="{x:Static vm:VaultSection.Credentials}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="PASSWORDS" />
|
||||
<TextBlock Text="Passwords" />
|
||||
<TextBlock Text="{Binding Credentials.Count}" />
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
@@ -95,7 +100,7 @@
|
||||
Command="{Binding ShowSectionCommand}"
|
||||
CommandParameter="{x:Static vm:VaultSection.Tags}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="TAGS" />
|
||||
<TextBlock Text="Tags" />
|
||||
<TextBlock Text="{Binding Tags.Count}" />
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
@@ -104,7 +109,7 @@
|
||||
Command="{Binding ShowSectionCommand}"
|
||||
CommandParameter="{x:Static vm:VaultSection.Buckets}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="BUCKETS" />
|
||||
<TextBlock Text="Buckets" />
|
||||
<TextBlock Text="{Binding ObjectStores.Count}" />
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
@@ -134,35 +139,45 @@
|
||||
style below, which had nothing to colour until now. DELETE is the last button that should be pressed on
|
||||
a guess about which row it means.
|
||||
-->
|
||||
<Border Grid.Row="2" IsVisible="{Binding ShowsItemActions}" Margin="12,4"
|
||||
<!--
|
||||
◆ EDIT is here now, beside MOVE and DELETE, because there is something for it to open: the editor page at
|
||||
the foot of this file, which is the desktop's five editors drawn for a phone. The three sit in a row
|
||||
under the item's name rather than beside it, which is what fits at 360dp; DELETE keeps the danger
|
||||
outline and the far end of the row.
|
||||
|
||||
COPY PUBLIC KEY under them for a key that has one, because a phone is where somebody is standing at a
|
||||
machine that needs it in authorized_keys.
|
||||
-->
|
||||
<Border Grid.Row="2" IsVisible="{Binding ShowsItemActions}" Margin="16,4"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="12" Padding="14,10">
|
||||
<Grid ColumnDefinitions="*,10,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="label" Text="SELECTED" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||||
Text="{Binding SelectedVaultItem.Name}" TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="tag outline">
|
||||
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
BorderThickness="1" CornerRadius="14" Padding="14,12">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="mono" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||
Text="{Binding SelectedVaultItem.Name}" TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="tag outline">
|
||||
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
MOVE beside it, and only where there is somewhere to move to — the rule the host's MOVE follows on
|
||||
this head, for the reason a phone has: there is no room to draw a button that answers with a
|
||||
refusal. It is the ghost of the pair rather than the danger one, because a move is undone by
|
||||
moving it back.
|
||||
-->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="secondary" Height="44" Width="86" Content="MOVE"
|
||||
<Grid ColumnDefinitions="*,8,*,8,*">
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="EDIT"
|
||||
Command="{Binding EditSelectedItemCommand}" />
|
||||
<!--
|
||||
MOVE only where there is somewhere to move to — the rule the host's MOVE follows on this head, for
|
||||
the reason a phone has: there is no room to draw a button that answers with a refusal.
|
||||
-->
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="MOVE"
|
||||
IsVisible="{Binding CanMoveSelectedItem}"
|
||||
Command="{Binding MoveSelectedItemCommand}" />
|
||||
<Button Classes="danger" Height="44" Width="104" Content="DELETE"
|
||||
<Button Grid.Column="4" Classes="danger" Height="44" Content="DELETE"
|
||||
Command="{Binding DeleteSelectedItemCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Button Classes="secondary" Height="44" Content="COPY PUBLIC KEY"
|
||||
Command="{Binding CopyPublicKeyCommand}"
|
||||
IsVisible="{Binding SelectedKey.Key.PublicKey, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=False}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ============ ◆ the counted confirmation ============ -->
|
||||
@@ -183,9 +198,9 @@
|
||||
IsVisible="{Binding PendingDeletion.HasUsage}"
|
||||
Text="{Binding PendingDeletion.Usage}" />
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,6,0,0">
|
||||
<Button Grid.Column="0" Classes="danger" Content="DELETE"
|
||||
<Button Grid.Column="2" Classes="danger" Content="DELETE"
|
||||
Command="{Binding ConfirmDeleteCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||||
Command="{Binding CancelDeleteCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -224,9 +239,9 @@
|
||||
IsVisible="{Binding HasMovingItemUsage}"
|
||||
Text="{Binding MovingItemUsage}" />
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="MOVE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="MOVE"
|
||||
Command="{Binding ConfirmMoveItemCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelMoveItemCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -239,12 +254,15 @@
|
||||
<TextBlock Classes="body" IsVisible="{Binding !HasVaultItems}" Margin="24,20"
|
||||
VerticalAlignment="Top" Text="{Binding EmptySectionMessage}" />
|
||||
|
||||
<ListBox ItemsSource="{Binding VaultItems}" SelectedItem="{Binding SelectedVaultItem}"
|
||||
<!-- The foot padding is the floating button's: the last card scrolls clear of it. -->
|
||||
<ListBox ItemsSource="{Binding VaultItems}" SelectedItem="{Binding SelectedVaultItem}" Padding="0,0,0,88"
|
||||
Background="Transparent" BorderThickness="0" IsVisible="{Binding HasVaultItems}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<!-- A name for TalkBack rather than the row view model's type name (audit H4). -->
|
||||
<Setter Property="AutomationProperties.Name" Value="{ReflectionBinding Name}" />
|
||||
</Style>
|
||||
<!--
|
||||
The fill goes on the card and not on the ContentPresenter behind it, which is where HOSTS and
|
||||
@@ -255,28 +273,28 @@
|
||||
-->
|
||||
<Style Selector="ListBoxItem:selected Border.card">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TerminalTabAccent}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
||||
<Border Classes="card" Margin="12,4">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" MinHeight="64">
|
||||
<Border Classes="card" Margin="16,4" CornerRadius="16" Background="{StaticResource Panel}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" MinHeight="56">
|
||||
|
||||
<!--
|
||||
The glyph tile. One mark for all three kinds rather than three: a key, a password and a
|
||||
bucket are already named by the badge beside them, and three near-identical monochrome
|
||||
glyphs at 14px would be decoration that reads as a distinction.
|
||||
-->
|
||||
<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 Grid.Column="0" Width="40" Height="40" CornerRadius="12"
|
||||
Background="{StaticResource AccentSoft}" VerticalAlignment="Center">
|
||||
<TextBlock Classes="icon" Text="{Binding IconGlyph}" FontSize="20"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Margin="12,0" Spacing="3" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||
<TextBlock Classes="mono" FontSize="14.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="tag outline">
|
||||
<TextBlock Text="{Binding Type}" />
|
||||
@@ -287,18 +305,310 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<!-- What is known *about* an item, never the secret. -->
|
||||
<TextBlock Classes="detail" Text="{Binding Detail}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail code" Text="{Binding Detail}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Classes="icon" Text="" Foreground="{StaticResource TextGhost}"
|
||||
FontSize="22" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- ============ add ============ -->
|
||||
<!--
|
||||
◆ The floating +, which this screen never had: its empty state asked for an SSH key or a password over
|
||||
a screen with no way to add either (audit M9). It raises the add sheet below, because there are five
|
||||
kinds of thing to add here and one button.
|
||||
-->
|
||||
<Button Classes="fab" Margin="0,0,18,18" HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
AutomationProperties.Name="Add to the keychain" Command="{Binding OpenKeychainAddSheetCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="26" />
|
||||
</Button>
|
||||
</Panel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ============ the add sheet ============ -->
|
||||
<!--
|
||||
The desktop's "+ New key" menu, as the hosts screen's add sheet: a scrim, a bottom panel, dismissible
|
||||
because "which kind" has no wrong answer. Each row opens its editor and lowers the sheet as it does —
|
||||
see KeychainScreen.OnAddChosen. Generating comes first, as it does on the desktop: on a phone it is
|
||||
the way to a key that never existed as a file anywhere.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsKeychainAddSheetOpen}">
|
||||
<Button Classes="scrim" Command="{Binding CloseKeychainAddSheetCommand}" />
|
||||
|
||||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="22,22,0,0" Padding="12,10,12,16">
|
||||
<StackPanel>
|
||||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,12" />
|
||||
<TextBlock Classes="label" Text="ADD TO THE KEYCHAIN" Margin="12,0,12,6" />
|
||||
|
||||
<Button Classes="row" Command="{Binding NewGeneratedKeyCommand}" Click="OnAddChosen"
|
||||
AutomationProperties.Name="Generate a key">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Generate a key" />
|
||||
<TextBlock Classes="detail" Text="A new key pair made here, so the private half never becomes a file." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Classes="row" Command="{Binding NewKeyCommand}" Click="OnAddChosen"
|
||||
AutomationProperties.Name="Add an SSH key">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Add an SSH key" />
|
||||
<TextBlock Classes="detail" Text="Paste in a key you already have." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Classes="row" Command="{Binding NewCredentialCommand}" Click="OnAddChosen"
|
||||
AutomationProperties.Name="Add a password">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Add a password" />
|
||||
<TextBlock Classes="detail" Text="One account, described once, for as many hosts as use it." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Classes="row" Command="{Binding NewObjectStoreCommand}" Click="OnAddChosen"
|
||||
AutomationProperties.Name="Add a bucket">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Add a bucket" />
|
||||
<TextBlock Classes="detail" Text="An S3-compatible bucket, to browse under Settings, Files." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
<Button Classes="row" Command="{Binding NewTagCommand}" Click="OnAddChosen"
|
||||
AutomationProperties.Name="Add a tag">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="2">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Add a tag" />
|
||||
<TextBlock Classes="detail" Text="A name to put on hosts, before there is anything to put it on." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- ============ ◆ the editor page ============ -->
|
||||
<!--
|
||||
The desktop's five keychain editors, as one page over the whole screen — the host editor's shape on this
|
||||
head, and PhoneShell stands its chrome down for it the same way (see PhoneShell.RefreshChrome). Only one
|
||||
of the five is ever open; each draws its own section and its own CANCEL and SAVE, and the ← in the
|
||||
header cancels whichever it is, as the system back gesture does.
|
||||
|
||||
Every label is a label above its box rather than a placeholder in it, which the audit asked of the
|
||||
host editor too: on a phone the box is full of what was typed long before anybody is done reading it.
|
||||
-->
|
||||
<Grid IsVisible="{Binding IsEditingAVaultItem}" RowDefinitions="Auto,*" Background="{StaticResource Canvas}">
|
||||
<Border Grid.Row="0" Height="56" Background="{StaticResource DeepChrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="8,0">
|
||||
<Button Grid.Column="0" Classes="icon" AutomationProperties.Name="Cancel" Click="OnCancelEditor">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
<TextBlock Grid.Column="1" Classes="heading" Margin="4,0" Text="Generate a key"
|
||||
IsVisible="{Binding IsGeneratingKey}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Margin="4,0" Text="SSH key"
|
||||
IsVisible="{Binding IsEditingKey}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Margin="4,0" Text="Password"
|
||||
IsVisible="{Binding IsEditingCredential}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Margin="4,0" Text="Bucket"
|
||||
IsVisible="{Binding IsEditingObjectStore}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Margin="4,0" Text="Tag"
|
||||
IsVisible="{Binding IsEditingTag}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel Margin="16,14,16,24" Spacing="8">
|
||||
|
||||
<!--
|
||||
Which vault a new item is filed into, hidden at one vault, which is where most people stay. The
|
||||
desktop's own "NEW ITEMS FILE TO", and the sentence under it for the reason that one has it.
|
||||
-->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding HasVaultChoice}" Margin="0,0,0,8">
|
||||
<TextBlock Classes="label" Text="NEW ITEMS FILE TO" />
|
||||
<ComboBox ItemsSource="{Binding TargetVaults}" SelectedItem="{Binding SelectedTargetVault}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||||
<TextBlock Classes="mono" FontSize="12.5" Text="{Binding Display}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Classes="detail" Text="An item filed into a shared vault is readable by everyone holding that vault's key." />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ generating ============ -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsGeneratingKey}">
|
||||
<TextBlock Classes="label" Text="KIND" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="chiptoggle" Classes.worn="{Binding GeneratesEd25519}" MinHeight="44"
|
||||
Command="{Binding ChooseKeyAlgorithmCommand}"
|
||||
CommandParameter="{x:Static ssh:SshKeyAlgorithm.Ed25519}">
|
||||
<TextBlock Classes="mono" FontSize="12.5" Text="Ed25519" />
|
||||
</Button>
|
||||
<Button Classes="chiptoggle" Classes.worn="{Binding GeneratesRsa}" MinHeight="44"
|
||||
Command="{Binding ChooseKeyAlgorithmCommand}"
|
||||
CommandParameter="{x:Static ssh:SshKeyAlgorithm.Rsa4096}">
|
||||
<TextBlock Classes="mono" FontSize="12.5" Text="RSA 4096" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<TextBlock Classes="detail" Text="Ed25519 is what every current OpenSSH prefers. RSA is for servers too old to accept it, and takes a few seconds to make." />
|
||||
|
||||
<TextBlock Classes="label" Text="NAME" Margin="0,8,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding GenerateComment}" />
|
||||
<TextBlock Classes="detail" Text="What the key is called here, and the comment written into it, so the line on a host says where it came from." />
|
||||
|
||||
<TextBlock Classes="body" Margin="0,8,0,0"
|
||||
Text="The key file itself has no passphrase. Your keychain passphrase is what protects it, and it never reaches the server in a form it can read. It lands in the key editor unsaved, to be named and kept." />
|
||||
|
||||
<Grid ColumnDefinitions="*,10,*" Margin="0,10,0,0">
|
||||
<Button Grid.Column="0" Classes="secondary" Content="CANCEL"
|
||||
Command="{Binding CancelGenerateKeyCommand}" />
|
||||
<Button Grid.Column="2" Classes="primary" Content="GENERATE"
|
||||
Command="{Binding GenerateKeyCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ a key ============ -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsEditingKey}">
|
||||
<TextBlock Classes="label" Text="NAME" />
|
||||
<TextBox Classes="field" Text="{Binding KeyEditorLabel}" />
|
||||
|
||||
<!--
|
||||
Not a password box, for the desktop's reason: the armour has to be visible to be pasted and
|
||||
checked, and the mistake this prevents is pasting the .pub file, which is refused by name.
|
||||
-->
|
||||
<TextBlock Classes="label" Text="PRIVATE KEY" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding KeyEditorPrivateKey}" Height="140"
|
||||
PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
AcceptsReturn="True" TextWrapping="NoWrap" VerticalContentAlignment="Top"
|
||||
Padding="12,10" FontSize="11" />
|
||||
|
||||
<TextBlock Classes="label" Text="PASSPHRASE, IF THE KEY HAS ONE" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field secret" Text="{Binding KeyEditorPassphrase}" />
|
||||
|
||||
<TextBlock Classes="label" Text="PUBLIC HALF (OPTIONAL)" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding KeyEditorPublicKey}" FontSize="11" />
|
||||
|
||||
<TextBlock Classes="label" Text="NOTES" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding KeyEditorNotes}" Height="72" AcceptsReturn="True"
|
||||
TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="12,10" />
|
||||
|
||||
<TextBlock Classes="body" Margin="0,6,0,0"
|
||||
Text="The key and its passphrase are encrypted here and never reach the server in a form it can read." />
|
||||
|
||||
<Grid ColumnDefinitions="*,10,*" Margin="0,10,0,0">
|
||||
<Button Grid.Column="0" Classes="secondary" Content="CANCEL" Command="{Binding CancelKeyEditCommand}" />
|
||||
<Button Grid.Column="2" Classes="primary" Content="SAVE" Command="{Binding SaveKeyCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ a password ============ -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsEditingCredential}">
|
||||
<TextBlock Classes="label" Text="NAME" />
|
||||
<TextBox Classes="field" Text="{Binding CredentialEditorLabel}" />
|
||||
|
||||
<TextBlock Classes="label" Text="USERNAME" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding CredentialEditorUsername}"
|
||||
PlaceholderText="blank: each host's own" />
|
||||
|
||||
<TextBlock Classes="label" Text="PASSWORD" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field secret" Text="{Binding CredentialEditorPassword}" />
|
||||
|
||||
<TextBlock Classes="label" Text="NOTES" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding CredentialEditorNotes}" Height="72" AcceptsReturn="True"
|
||||
TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="12,10" />
|
||||
|
||||
<TextBlock Classes="body" Margin="0,6,0,0"
|
||||
Text="Encrypted here and never sent to the server in a form it can read. Bind a host to it in the host's own editor, under how it signs in." />
|
||||
|
||||
<Grid ColumnDefinitions="*,10,*" Margin="0,10,0,0">
|
||||
<Button Grid.Column="0" Classes="secondary" Content="CANCEL"
|
||||
Command="{Binding CancelCredentialEditCommand}" />
|
||||
<Button Grid.Column="2" Classes="primary" Content="SAVE" Command="{Binding SaveCredentialCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ a bucket ============ -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsEditingObjectStore}">
|
||||
<TextBlock Classes="label" Text="NAME" />
|
||||
<TextBox Classes="field" Text="{Binding BucketEditorLabel}" />
|
||||
|
||||
<TextBlock Classes="label" Text="BUCKET" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding BucketEditorBucket}" />
|
||||
|
||||
<TextBlock Classes="label" Text="ACCESS KEY ID" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding BucketEditorAccessKeyId}" />
|
||||
|
||||
<TextBlock Classes="label" Text="SECRET ACCESS KEY" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field secret" Text="{Binding BucketEditorSecretAccessKey}" />
|
||||
|
||||
<TextBlock Classes="label" Text="REGION" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding BucketEditorRegion}" PlaceholderText="e.g. eu-west-1" />
|
||||
|
||||
<TextBlock Classes="label" Text="ENDPOINT" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding BucketEditorEndpoint}" PlaceholderText="blank: Amazon S3" />
|
||||
|
||||
<CheckBox IsChecked="{Binding BucketEditorUsePathStyle}" MinHeight="44">
|
||||
<TextBlock FontSize="13.5" Foreground="{StaticResource Text}" TextWrapping="Wrap"
|
||||
Text="Address the bucket as a path" />
|
||||
</CheckBox>
|
||||
<TextBlock Classes="detail"
|
||||
Text="Off for Amazon S3. On for most self-hosted services — MinIO and Ceph have no wildcard DNS, so the bucket cannot be a subdomain." />
|
||||
|
||||
<TextBlock Classes="label" Text="NOTES" Margin="0,6,0,0" />
|
||||
<TextBox Classes="field" Text="{Binding BucketEditorNotes}" Height="72" AcceptsReturn="True"
|
||||
TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="12,10" />
|
||||
|
||||
<Grid ColumnDefinitions="*,10,*" Margin="0,10,0,0">
|
||||
<Button Grid.Column="0" Classes="secondary" Content="CANCEL"
|
||||
Command="{Binding CancelObjectStoreEditCommand}" />
|
||||
<Button Grid.Column="2" Classes="primary" Content="SAVE" Command="{Binding SaveObjectStoreCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ a tag ============ -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsEditingTag}">
|
||||
<TextBlock Classes="label" Text="NAME" />
|
||||
<TextBox Classes="field" Text="{Binding TagEditorLabel}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBlock Classes="detail"
|
||||
Text="Renaming a tag changes it everywhere at once. No host is rewritten — each one names this tag rather than repeating its name." />
|
||||
|
||||
<Grid ColumnDefinitions="*,10,*" Margin="0,10,0,0">
|
||||
<Button Grid.Column="0" Classes="secondary" Content="CANCEL" Command="{Binding CancelTagEditCommand}" />
|
||||
<Button Grid.Column="2" Classes="primary" Content="SAVE" Command="{Binding SaveTagCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="detail" Margin="0,8,0,0" Text="{Binding Status}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
</Panel>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,10 +1,70 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
namespace DodoSSH.Client.Android.Views;
|
||||
|
||||
/// <summary>Design 04 — everything in the vault that is not a host.</summary>
|
||||
internal sealed partial class KeychainScreen : UserControl
|
||||
{
|
||||
public KeychainScreen() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels whichever of the keychain's five editors is open.
|
||||
/// </summary>
|
||||
/// <returns>Whether one was, and so whether there was anything to cancel.</returns>
|
||||
/// <remarks>
|
||||
/// One place for the question the editor page's ← and the system back gesture both ask. Only one editor is
|
||||
/// ever open — each one's command refuses while another is — so the order only decides which test runs
|
||||
/// first. Cancelling rather than hiding, as the host editor does, because cancelling is what empties the
|
||||
/// boxes, and two of them hold a private key and a password.
|
||||
/// </remarks>
|
||||
internal static bool TryCancelAnEditor(VaultViewModel vault)
|
||||
{
|
||||
if (vault.IsGeneratingKey)
|
||||
{
|
||||
vault.CancelGenerateKeyCommand.Execute(null);
|
||||
}
|
||||
else if (vault.IsEditingKey)
|
||||
{
|
||||
vault.CancelKeyEditCommand.Execute(null);
|
||||
}
|
||||
else if (vault.IsEditingCredential)
|
||||
{
|
||||
vault.CancelCredentialEditCommand.Execute(null);
|
||||
}
|
||||
else if (vault.IsEditingObjectStore)
|
||||
{
|
||||
vault.CancelObjectStoreEditCommand.Execute(null);
|
||||
}
|
||||
else if (vault.IsEditingTag)
|
||||
{
|
||||
vault.CancelTagEditCommand.Execute(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnCancelEditor(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is VaultViewModel vault)
|
||||
{
|
||||
TryCancelAnEditor(vault);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A kind was chosen on the add sheet: the sheet goes down as its editor opens.</summary>
|
||||
private void OnAddChosen(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is VaultViewModel vault)
|
||||
{
|
||||
vault.IsKeychainAddSheetOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
|
||||
<StackPanel Spacing="1" Margin="0,5">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="detail" FontSize="10" Text="{Binding Started}"
|
||||
<TextBlock Classes="detail code" FontSize="10" Text="{Binding Started}"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
|
||||
<!--
|
||||
@@ -104,7 +104,7 @@
|
||||
Text="{Binding Kind}" Foreground="{StaticResource Live}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="detail" FontSize="10" Text="{Binding HostLabel}"
|
||||
<TextBlock Classes="detail code" FontSize="10" Text="{Binding HostLabel}"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
@@ -148,7 +148,7 @@
|
||||
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
|
||||
<StackPanel Spacing="1" Margin="0,5">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="detail" FontSize="10" Text="{Binding At}"
|
||||
<TextBlock Classes="detail code" FontSize="10" Text="{Binding At}"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
|
||||
Text="{Binding Operation}" Foreground="{StaticResource Info}"
|
||||
|
||||
@@ -6,190 +6,267 @@
|
||||
Background="{StaticResource Canvas}">
|
||||
|
||||
<!--
|
||||
Design v2 — SETTINGS: the hub for everything the bottom bar has no room for.
|
||||
SETTINGS: the hub behind the bottom bar's third entry.
|
||||
|
||||
Three slots and ten destinations is the arithmetic the design solves by putting seven 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.
|
||||
It takes the shell as its data context rather than the vault, because nearly every row on it is a
|
||||
navigation command and nothing here reads an item.
|
||||
|
||||
Teams is the tenth and the design never drew it — see the row itself. That one is the design's count
|
||||
plus one rather than a rearrangement of it. The keychain below is a rearrangement, and the only one:
|
||||
the bottom bar lost a slot to gain this row.
|
||||
── the 2026-10-04 redesign ────────────────────────────────────────────────────────────────────────────
|
||||
The hub used to hold the product: the keychain, snippets, SFTP and S3 were rows here beside Preferences
|
||||
and LOCK, and the audit's M8 said so. The keychain and snippets now live under VAULTS, in the open vault's
|
||||
own switch, and what is left is arranged the way a phone's settings are — who you are, then grouped
|
||||
sections of rows on cards — rather than one flat list of places.
|
||||
|
||||
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.
|
||||
The account card says who is signed in and where, which nothing on this head said before: the server's
|
||||
address is the fact that decides whose keychain this is, and it was only ever typed once, on the first
|
||||
screen. It is not a button; there is no account page on the phone to open (signing out is on the lock
|
||||
screen — see PreferencesScreen's NOT HERE).
|
||||
|
||||
── from MORE to SETTINGS ──────────────────────────────────────────────────────────────────────────────
|
||||
The hub was called More because that was the honest name for a handful of things that did not fit. With
|
||||
the keychain on it as well it is the place everything that is not connecting to a machine lives, which
|
||||
has a name of its own and a glyph everyone already knows.
|
||||
Rows are cards of rows rather than cards each, because a destination is a place and not a thing, and a
|
||||
group of them reads as one section only when they share a surface. Each keeps 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.
|
||||
|
||||
The keychain is first, and above the rule, because it is the only row here that holds the user's own
|
||||
material rather than a screen about the application. Everything below it is somewhere to look at what
|
||||
has happened or how this phone behaves.
|
||||
Every icon is the embedded Material font's, as the bar's are — see NavButton, which says why the
|
||||
Unicode glyphs went (audit M7).
|
||||
-->
|
||||
|
||||
<UserControl.Styles>
|
||||
<!-- A section's rows. The card's own padding is the rows' business, so a row's press fill reaches its
|
||||
edges; the hairlines between rows are drawn by the rows' containers. -->
|
||||
<Style Selector="Border.settingsgroup">
|
||||
<Setter Property="Background" Value="{StaticResource Panel}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="Button.settingsrow">
|
||||
<Setter Property="MinHeight" Value="60" />
|
||||
<Setter Property="Padding" Value="14,8" />
|
||||
<Setter Property="CornerRadius" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="Border.rowrule">
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Margin" Value="62,0,0,0" />
|
||||
<Setter Property="Background" Value="{StaticResource BorderSubtle}" />
|
||||
</Style>
|
||||
<!-- The tile an icon sits on: a square of the accent's wash, so a row's mark is a colour as well as a
|
||||
shape. Amber for the one row that ends something rather than going somewhere. -->
|
||||
<Style Selector="Border.rowtile">
|
||||
<Setter Property="Width" Value="36" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Background" Value="{StaticResource AccentSoft}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="Border.rowtile > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
||||
<Setter Property="FontSize" Value="19" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.rowtitle">
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
<Setter Property="FontSize" Value="14.5" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.chevron">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextGhost}" />
|
||||
<Setter Property="FontSize" Value="20" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<Border Grid.Row="0" Height="56" Padding="16,0">
|
||||
<TextBlock Classes="heading" Text="Settings" />
|
||||
<Border Grid.Row="0" Height="60" Padding="20,0" Background="{StaticResource DeepChrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<TextBlock Classes="heading" FontSize="22" FontWeight="Bold" Text="Settings" />
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel Margin="10,0,10,20" Spacing="2">
|
||||
<StackPanel Margin="16,14,16,24" Spacing="8">
|
||||
|
||||
<!-- ============ who, and where ============ -->
|
||||
<Border Classes="settingsgroup" Padding="14">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<Border Grid.Column="0" Width="48" Height="48" CornerRadius="24"
|
||||
Background="{StaticResource AvatarGradient}">
|
||||
<TextBlock Text="{Binding AvatarInitials}" FontSize="17" FontWeight="Bold"
|
||||
Foreground="{StaticResource AccentInk}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock FontSize="16" FontWeight="Bold" Foreground="{StaticResource Text}"
|
||||
Text="{Binding AccountName}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock FontSize="12.5" Foreground="{StaticResource TextDim}" Text="{Binding Email}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding Email, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding ServerUrl}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ security ============ -->
|
||||
<TextBlock Classes="label" Text="SECURITY" Margin="6,14,0,2" />
|
||||
<Border Classes="settingsgroup">
|
||||
<StackPanel>
|
||||
<!--
|
||||
The fingerprint is a row to Preferences rather than a switch here. Turning it on registers a key
|
||||
in the phone's secure hardware and turning it off withdraws one — two operations with a prompt
|
||||
and a paragraph each, which live on that screen — so this row says which state the phone is in
|
||||
and goes there. ON and OFF are the two flags the shell already computes; a phone with no way to
|
||||
hold a key says neither and the screen behind explains.
|
||||
-->
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="Unlock with a fingerprint"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}">
|
||||
<Grid ColumnDefinitions="Auto,12,*,8,Auto,4,Auto">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="Unlock with a fingerprint" />
|
||||
<TextBlock Classes="detail" Text="The key stays in this phone's secure hardware." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="4" Classes="label" FontSize="10.5" Text="ON"
|
||||
Foreground="{StaticResource Live}" VerticalAlignment="Center"
|
||||
IsVisible="{Binding CanForgetDevice}" />
|
||||
<TextBlock Grid.Column="4" Classes="label" FontSize="10.5" Text="OFF"
|
||||
VerticalAlignment="Center" IsVisible="{Binding CanRegisterDevice}" />
|
||||
<TextBlock Grid.Column="6" Classes="icon chevron" Text="" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Border Classes="rowrule" />
|
||||
<!--
|
||||
Locking, here as well as in the vault's header. The header is on the VAULTS tab and stands down
|
||||
on this one, so without this row every screen under SETTINGS would be two taps from locking the
|
||||
keychain — the wrong direction for the one action whose whole value is being quick.
|
||||
-->
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="Lock the keychain now"
|
||||
Command="{Binding LockCommand}">
|
||||
<Grid ColumnDefinitions="Auto,12,*">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="Lock now" />
|
||||
<TextBlock Classes="detail" Text="Shells you have open keep running." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ============ vaults and history ============ -->
|
||||
<TextBlock Classes="label" Text="VAULTS AND HISTORY" Margin="6,14,0,2" />
|
||||
<Border Classes="settingsgroup">
|
||||
<StackPanel>
|
||||
<!--
|
||||
Vaults, which the v2 design had no row for. It is on the phone because a vault arrives without
|
||||
being asked for, and somebody it arrives for is at least as likely to be holding a phone. The
|
||||
vault name in the VAULTS tab's header leads here too.
|
||||
-->
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="Vaults"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
|
||||
<Grid ColumnDefinitions="Auto,12,*,8,Auto">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="Vaults and sharing" />
|
||||
<TextBlock Classes="detail" Text="Your vaults, who is in each one, and who holds its key." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="4" Classes="icon chevron" Text="" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Border Classes="rowrule" />
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="Logs"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Logs}">
|
||||
<Grid ColumnDefinitions="Auto,12,*,8,Auto">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="Activity log" />
|
||||
<TextBlock Classes="detail" Text="What has been connected to, and what has changed." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="4" Classes="icon chevron" Text="" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ============ files ============ -->
|
||||
<!--
|
||||
"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 row reading Vault would be the one place the
|
||||
product surface leaked the implementation's word.
|
||||
SFTP and S3 stay reachable from here, and the terminal now reaches a host's files directly — the
|
||||
FILES entry in a session's menu opens them on that session's host. A bucket has no session, so this
|
||||
is still the way to one. Both 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.
|
||||
-->
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Keychain}">
|
||||
<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="Keychain" />
|
||||
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||
Text="Keys, passwords, buckets and tags — everything in the vault that is not a host." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<TextBlock Classes="label" Text="FILES" Margin="6,14,0,2" />
|
||||
<Border Classes="settingsgroup">
|
||||
<StackPanel>
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="SFTP"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||||
<Grid ColumnDefinitions="Auto,12,*,8,Auto">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="SFTP" />
|
||||
<TextBlock Classes="detail" Text="Browse a host's files." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="4" Classes="icon chevron" Text="" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<Border Classes="rowrule" />
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="S3"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||||
<Grid ColumnDefinitions="Auto,12,*,8,Auto">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="S3" />
|
||||
<TextBlock Classes="detail" Text="Objects in an S3-compatible bucket from the keychain." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="4" Classes="icon chevron" Text="" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Height="1" Background="{StaticResource Border}" Margin="16,8" />
|
||||
|
||||
<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>
|
||||
|
||||
<!--
|
||||
Vaults, which the v2 design has no row for — it is a shipped screen the design had no slot for
|
||||
rather than a drawn one with nothing behind it. It is on the phone because a vault arrives
|
||||
without being asked for, and somebody it arrives for is at least as likely to be holding a phone.
|
||||
|
||||
◎ rather than a glyph of its own. The desktop rail already draws this destination with it, and
|
||||
two heads giving one destination two marks is how a user learns the wrong one.
|
||||
-->
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
|
||||
<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="Vaults" />
|
||||
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||
Text="Your vaults, who is in each one, and who holds its key." />
|
||||
</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="Unlocking with a fingerprint, and what this phone cannot do." />
|
||||
</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 HOSTS and stands down behind SETTINGS, so without this row every screen
|
||||
under here would 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>
|
||||
<!-- ============ this app ============ -->
|
||||
<!-- Preferences with tune rather than a gear: the gear is the tab this row is under (audit M7). -->
|
||||
<TextBlock Classes="label" Text="THIS APP" Margin="6,14,0,2" />
|
||||
<Border Classes="settingsgroup">
|
||||
<Button Classes="row settingsrow" AutomationProperties.Name="Preferences"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}">
|
||||
<Grid ColumnDefinitions="Auto,12,*,8,Auto,4,Auto">
|
||||
<Border Grid.Column="0" Classes="rowtile">
|
||||
<TextBlock Classes="icon" Text="" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="rowtitle" Text="Preferences and updates" />
|
||||
<TextBlock Classes="detail" Text="Fingerprint unlock, updates, and what this phone cannot do." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="4" Classes="mono" FontSize="11" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding Updates.CurrentVersion}" VerticalAlignment="Center"
|
||||
MaxWidth="110" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="6" Classes="icon chevron" Text="" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ 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. Forwarding is built and runs here as on the desktop — a host's
|
||||
The v2 design gives this hub an entry and a full screen behind it: local, remote and dynamic rules
|
||||
with toggles and byte counters. Forwarding is built and runs here as on the desktop — a host's
|
||||
forwards start with every terminal opened to it — but it is not a destination, so it has no row. A
|
||||
forward belongs to a host and runs with a session, and on this head the session's forwards are a
|
||||
sheet off the terminal's bar: the ⇄ button, drawn for a tab that has some, lists them with a switch
|
||||
@@ -202,13 +279,12 @@
|
||||
It is a paragraph here rather than a missing row for the reason the desktop keeps TEAMS in its rail:
|
||||
a place somebody looks for a feature should say where it went. See docs/design-import-gaps.md.
|
||||
-->
|
||||
<Border Classes="card" Margin="6,16,6,0">
|
||||
<Border Classes="card" Margin="0,14,0,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" />
|
||||
<TextBlock Classes="icon" Text="" Foreground="{StaticResource TextFaint}" FontSize="18" />
|
||||
<TextBlock FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource TextDim}"
|
||||
Text="Port forwarding" VerticalAlignment="Center" />
|
||||
<Border Classes="tag outline" VerticalAlignment="Center">
|
||||
<TextBlock Text="IN THE TERMINAL" />
|
||||
</Border>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Globalization;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Automation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
@@ -17,16 +20,31 @@ namespace DodoSSH.Client.Android.Views;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Written in C# rather than as a XAML control because it is a button with a glyph, a label and a rule about
|
||||
/// the accent bar, and a templated control for that would be more machinery than the thing it draws.
|
||||
/// Written in C# rather than as a XAML control because it is a button with an icon, a label and a rule about
|
||||
/// the indicator, and a templated control for that would be more machinery than the thing it draws.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The accent bar is on top, not underneath.</b> The v2 design distinguishes the current destination with
|
||||
/// colour and weight alone and draws no bar at all; the bar is kept, and kept at the top, for a reason the
|
||||
/// 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.
|
||||
/// ◆ <b>The indicator is a pill behind the icon, where it was a bar along the top edge.</b> The bar was put at
|
||||
/// the top because the bottom of the screen is under the system's gesture bar, and an indicator drawn there is
|
||||
/// one nobody sees; the pill answers the same worry from the other side, by sitting in the middle of the
|
||||
/// control rather than at either edge. It is the shape Android's own navigation bar has drawn since Material 3
|
||||
/// and the one the 2026-10-04 redesign asks for. The rule the bar existed to keep is kept: the current entry is
|
||||
/// still told apart three ways — the filled pill, the accent colour and the heavier label — because colour
|
||||
/// alone at this size is the one signal a user with a colour deficiency would be left with.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ◆ <b>An icon from the embedded Material font rather than a Unicode glyph.</b> The glyphs (▤ ⌗ ⚙) came from
|
||||
/// whichever symbol font the phone had, at whatever weight it drew them, and ⚙ meant both this tab and the
|
||||
/// Preferences row under it (audit M7). The icon font is the one the desktop's rail uses.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>A count, when there is one.</b> Connections carries the number of open sessions in a green badge, which
|
||||
/// was otherwise only said by the strip of shells above the bar. Zero draws nothing: a "0" is a number about
|
||||
/// nothing, and the badge being absent already says it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>It has a name.</b> Its content is a panel, and Avalonia's automation peer reported that panel's type name
|
||||
/// to TalkBack — "Avalonia.Controls.Panel", three times across the bar (audit H4). The label is the name.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Every colour here is resolved from <c>Theme/Palette.axaml</c> rather than written out. A nav bar that
|
||||
@@ -39,25 +57,28 @@ internal sealed class NavButton : Button
|
||||
public static readonly StyledProperty<string> LabelProperty =
|
||||
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>
|
||||
/// <summary>The icon drawn above the label: one codepoint of the embedded Material Icons font.</summary>
|
||||
public static readonly StyledProperty<string> GlyphProperty =
|
||||
AvaloniaProperty.Register<NavButton, string>(nameof(Glyph), string.Empty);
|
||||
|
||||
public static readonly StyledProperty<bool> IsCurrentProperty =
|
||||
AvaloniaProperty.Register<NavButton, bool>(nameof(IsCurrent));
|
||||
|
||||
private readonly Border indicator;
|
||||
/// <summary>A count drawn over the icon, or zero for none.</summary>
|
||||
public static readonly StyledProperty<int> CountProperty =
|
||||
AvaloniaProperty.Register<NavButton, int>(nameof(Count));
|
||||
|
||||
private readonly Border pill;
|
||||
private readonly TextBlock glyph;
|
||||
private readonly TextBlock text;
|
||||
private readonly Border badge;
|
||||
private readonly TextBlock badgeText;
|
||||
|
||||
// Resolved from the palette once the control is in the tree; see Repaint.
|
||||
private IBrush? currentBrush;
|
||||
private IBrush? currentIconBrush;
|
||||
private IBrush? idleBrush;
|
||||
private IBrush? pillBrush;
|
||||
|
||||
public NavButton()
|
||||
{
|
||||
@@ -70,35 +91,71 @@ internal sealed class NavButton : Button
|
||||
HorizontalContentAlignment = HorizontalAlignment.Stretch;
|
||||
VerticalContentAlignment = VerticalAlignment.Stretch;
|
||||
|
||||
indicator = new Border
|
||||
{
|
||||
Height = 2,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
|
||||
glyph = new TextBlock
|
||||
{
|
||||
FontSize = 15,
|
||||
FontSize = 21,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
AutomationProperties.SetAccessibilityView(glyph, AccessibilityView.Raw);
|
||||
|
||||
(badge, badgeText) = MakeBadge();
|
||||
|
||||
pill = new Border
|
||||
{
|
||||
Width = 64,
|
||||
Height = 32,
|
||||
CornerRadius = new CornerRadius(16),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Background = Brushes.Transparent,
|
||||
Child = glyph,
|
||||
};
|
||||
|
||||
text = new TextBlock
|
||||
{
|
||||
FontSize = 9,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeight.Medium,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
var stack = new StackPanel
|
||||
Content = new StackPanel
|
||||
{
|
||||
Spacing = 3,
|
||||
Spacing = 4,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children = { new Panel { Children = { pill, badge } }, text },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>The count's badge, over the pill's top-right corner.</summary>
|
||||
/// <remarks>
|
||||
/// Over the corner rather than beside the label, so it reads as belonging to the icon and costs the
|
||||
/// label no width.
|
||||
/// </remarks>
|
||||
private static (Border Badge, TextBlock Text) MakeBadge()
|
||||
{
|
||||
var count = new TextBlock
|
||||
{
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children = { glyph, text },
|
||||
};
|
||||
|
||||
Content = new Panel { Children = { indicator, stack } };
|
||||
var holder = new Border
|
||||
{
|
||||
MinWidth = 18,
|
||||
Height = 18,
|
||||
CornerRadius = new CornerRadius(9),
|
||||
Padding = new Thickness(5, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, -4, 8, 0),
|
||||
IsVisible = false,
|
||||
Child = count,
|
||||
};
|
||||
|
||||
return (holder, count);
|
||||
}
|
||||
|
||||
public string Label
|
||||
@@ -121,6 +178,13 @@ internal sealed class NavButton : Button
|
||||
set => SetValue(IsCurrentProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CountProperty" />
|
||||
public int Count
|
||||
{
|
||||
get => GetValue(CountProperty);
|
||||
set => SetValue(CountProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
@@ -129,6 +193,7 @@ internal sealed class NavButton : Button
|
||||
if (change.Property == LabelProperty)
|
||||
{
|
||||
text.Text = Label;
|
||||
AutomationProperties.SetName(this, Label);
|
||||
}
|
||||
else if (change.Property == GlyphProperty)
|
||||
{
|
||||
@@ -138,9 +203,10 @@ internal sealed class NavButton : Button
|
||||
{
|
||||
Repaint();
|
||||
}
|
||||
else if (change.Property == FontFamilyProperty)
|
||||
else if (change.Property == CountProperty)
|
||||
{
|
||||
text.FontFamily = FontFamily;
|
||||
badge.IsVisible = Count > 0;
|
||||
badgeText.Text = Count.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +217,17 @@ internal sealed class NavButton : Button
|
||||
|
||||
// Resolved here rather than in the constructor: application resources are not reachable while the
|
||||
// control is still being constructed, and a hard-coded stack would drift from Palette.axaml.
|
||||
if (Application.Current?.FindResource("MonoFont") is FontFamily mono)
|
||||
if (Application.Current?.FindResource("IconFont") is FontFamily icons)
|
||||
{
|
||||
text.FontFamily = mono;
|
||||
glyph.FontFamily = icons;
|
||||
}
|
||||
|
||||
currentBrush = Application.Current?.FindResource("AccentText") as IBrush;
|
||||
currentBrush = Application.Current?.FindResource("Text") as IBrush;
|
||||
currentIconBrush = Application.Current?.FindResource("AccentText") as IBrush;
|
||||
idleBrush = Application.Current?.FindResource("TextFaint") as IBrush;
|
||||
pillBrush = Application.Current?.FindResource("AccentSoft") as IBrush;
|
||||
badge.Background = Application.Current?.FindResource("Live") as IBrush;
|
||||
badgeText.Foreground = Application.Current?.FindResource("Canvas") 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.
|
||||
@@ -166,16 +236,21 @@ internal sealed class NavButton : Button
|
||||
|
||||
private void Repaint()
|
||||
{
|
||||
// Both the label and the bar, not just the bar — and the weight as well. See the remark on the type.
|
||||
// The pill, the colour and the weight together. See the remark on the type.
|
||||
var paint = IsCurrent ? currentBrush : idleBrush;
|
||||
var iconPaint = IsCurrent ? currentIconBrush : 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;
|
||||
if (iconPaint is not null)
|
||||
{
|
||||
glyph.Foreground = iconPaint;
|
||||
}
|
||||
|
||||
text.FontWeight = IsCurrent ? FontWeight.Bold : FontWeight.Medium;
|
||||
pill.Background = IsCurrent && pillBrush is not null ? pillBrush : Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
@@ -73,17 +73,28 @@
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
◆ Material icons rather than Unicode glyphs, as the desktop's own rail draws them. The glyphs came from
|
||||
whatever symbol font the phone happened to have, at whatever weight and optical size it drew them — the
|
||||
heavy ❯ beside the hairline ◳ the 2026-10-02 audit pointed at (M7) — and two of them meant two things:
|
||||
⚙ was both the Settings tab and Preferences, ⚿ both the keychain and locking it. The icon font is the
|
||||
one the desktop embeds, so one destination now has one mark on both heads.
|
||||
-->
|
||||
<Style Selector="Button.railentry TextBlock.railglyph">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource IconFont}" />
|
||||
<Setter Property="FontSize" Value="19" />
|
||||
<!-- A ligature codepoint is noise to a screen reader; the entry's name is on the button. -->
|
||||
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
|
||||
<Setter Property="Width" Value="24" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- A destination's name is a word rather than an identifier, so it takes the sans face — see the remark
|
||||
on TextBlock.body in Theme/Phone.axaml. -->
|
||||
<Style Selector="Button.railentry TextBlock.raillabel">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
||||
<Setter Property="FontSize" Value="12.5" />
|
||||
<Setter Property="FontSize" Value="13.5" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
</Style>
|
||||
@@ -120,11 +131,11 @@
|
||||
<DockPanel LastChildFill="False">
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Margin="8,0,8,10">
|
||||
<Button Classes="railentry" Classes.active="{Binding IsPreferencesShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Preferences" Classes.active="{Binding IsPreferencesShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="⚙" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Preferences" />
|
||||
</Grid>
|
||||
</Button>
|
||||
@@ -134,11 +145,11 @@
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="8,10,8,0" Spacing="2">
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsHostsShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Hosts" Classes.active="{Binding IsHostsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="▤" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Hosts" />
|
||||
<TextBlock Grid.Column="2" Classes="railcount" Text="{Binding Vault.Hosts.Count}" />
|
||||
</Grid>
|
||||
@@ -155,67 +166,67 @@
|
||||
surface and is that count, drawn as the sessions themselves — a number beside this word would be
|
||||
the same fact said twice, three inches apart.
|
||||
-->
|
||||
<Button Classes="railentry" Classes.active="{Binding IsTerminalSurface}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Connections" Classes.active="{Binding IsTerminalSurface}"
|
||||
Command="{Binding ShowTerminalCommand}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="⌗" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Connections" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Border Height="1" Background="{StaticResource Border}" Margin="4,8" />
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsKeychainShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Keychain" Classes.active="{Binding IsKeychainShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Keychain}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="⚿" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Keychain" />
|
||||
<TextBlock Grid.Column="2" Classes="railcount" Text="{Binding Vault.Keys.Count}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsSnippetsShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Snippets" Classes.active="{Binding IsSnippetsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Snippets}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="❯" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Snippets" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsTransfersShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="SFTP" Classes.active="{Binding IsTransfersShowing}"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="⇅" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="SFTP" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsBucketsShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="S3" Classes.active="{Binding IsBucketsShowing}"
|
||||
Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="◳" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="S3" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsLogsShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Logs" Classes.active="{Binding IsLogsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Logs}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="≡" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Logs" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="railentry" Classes.active="{Binding IsVaultsShowing}"
|
||||
<Button Classes="railentry" AutomationProperties.Name="Vaults" Classes.active="{Binding IsVaultsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="◎" />
|
||||
<TextBlock Grid.Column="0" Classes="railglyph" Text="" />
|
||||
<TextBlock Grid.Column="1" Classes="raillabel" Text="Vaults" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
@@ -24,10 +24,11 @@
|
||||
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
|
||||
|
||||
── a terminal gets the screen ─────────────────────────────────────────────────────────────────────────
|
||||
Three of the four rows below stand down while a shell is showing: the header, the shells strip and the
|
||||
bottom bar itself. The strip asks IsShowingPages directly; the other two go through flags the control
|
||||
computes, and the bar's is the one that differs — it stays up on Connections with nothing running, which
|
||||
is the terminal surface drawing a page rather than a shell. See PhoneShell.RefreshChrome.
|
||||
The rows around the screen stand down while a shell is showing: the header and the bottom bar. Both go
|
||||
through flags the control computes, and the bar's is the one that differs — it stays up on Connections
|
||||
with nothing running, which is the terminal surface drawing a page rather than a shell. See
|
||||
PhoneShell.RefreshChrome. (A third row, the strip of open shells, was removed by the 2026-10-04 redesign;
|
||||
the count on Connections took its place — see the remark where it was.)
|
||||
|
||||
The arithmetic is why. Header 56, strip 46, bar 64, and the terminal's own two rows on top of that: at
|
||||
360dp the shell was framed by about a third of the display, all of it about somewhere the user was not.
|
||||
@@ -78,7 +79,7 @@
|
||||
<views:PhoneRail Grid.Column="0"
|
||||
IsVisible="{Binding $parent[views:PhoneShell].ShowsRail}" />
|
||||
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!--
|
||||
The header: which vault, and whether it is synced.
|
||||
@@ -106,34 +107,53 @@
|
||||
header in HostsScreen, which the header stands down for, both move with it for the same reason;
|
||||
see the remark on each.
|
||||
-->
|
||||
<StackPanel>
|
||||
<Border Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1" Padding="14,0" Height="56">
|
||||
BorderThickness="0,0,0,1" Padding="10,0,6,0" Height="60">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!--
|
||||
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
|
||||
fact about the vault, and the design gives it the accent as a solid tile — which is also what
|
||||
the launcher icon draws, so the two agree.
|
||||
-->
|
||||
<Border Grid.Column="0" Width="26" Height="26" CornerRadius="8"
|
||||
Background="{StaticResource Accent}" VerticalAlignment="Center">
|
||||
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
◆ The vault's name is a button now, and it opens the vaults.
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="heading" Margin="10,0,8,0" FontSize="16"
|
||||
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
||||
The 2026-10-04 redesign draws the name with a chevron, as the switch it is on every phone app
|
||||
that holds more than one of something. What it leads to is VAULTS, which is where the vaults
|
||||
this phone shows are chosen — so the chevron is a promise the screen behind it keeps, rather
|
||||
than a dropdown this head has not got.
|
||||
|
||||
The mark stays filled, and takes the gradient Button.primary has: it is the one thing on this
|
||||
header that is not a fact about the vault, and the launcher icon draws it the same way.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="row" MinHeight="48" Padding="4,0,8,0" CornerRadius="12"
|
||||
HorizontalAlignment="Left"
|
||||
AutomationProperties.Name="{Binding Vault.VaultName, StringFormat='Vault {0}. Choose vaults'}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<Border Width="32" Height="32" CornerRadius="10" Background="{StaticResource AccentGradient}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="12" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw" />
|
||||
</Border>
|
||||
<TextBlock Classes="heading" FontSize="18" FontWeight="Bold" MaxWidth="170"
|
||||
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="icon" Text="" FontSize="20" Foreground="{StaticResource TextFaint}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
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"
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center"
|
||||
Margin="0,0,6,0">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="8" Height="8"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
|
||||
<TextBlock FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextDim}"
|
||||
Text="{Binding SyncLabel}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
@@ -145,16 +165,49 @@
|
||||
the bar it raises takes this header's place — so the pencil is in that bar, beside the count of
|
||||
what it would edit. See HostActionBar.axaml, which is drawn in the Panel below this one.
|
||||
|
||||
What is left in this row is the vault's name, the sync light and LOCK: three facts about the
|
||||
keychain, and none of them about a row.
|
||||
What is left in this row is the vault's name, the sync light and the lock: three facts about
|
||||
the keychain, and none of them about a row. The lock is an icon since the redesign, with its
|
||||
name on it for a screen reader rather than the word LOCK in nine-pixel capitals.
|
||||
-->
|
||||
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
||||
ToolTip.Tip="Lock the keychain">
|
||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
<Button Grid.Column="3" Classes="icon" Command="{Binding LockCommand}"
|
||||
AutomationProperties.Name="Lock the keychain" ToolTip.Tip="Lock the keychain">
|
||||
<TextBlock Classes="icon" Text="" FontSize="21" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ HOSTS · KEYCHAIN · SNIPPETS: the vault's three kinds of content, as one switch.
|
||||
|
||||
The redesign's answer to the audit's M8, that SETTINGS held the product. The keychain and the
|
||||
snippets were rows on the hub, a screen away from the hosts they are used with; they are the open
|
||||
vault's contents as much as the hosts are, so they now sit beside them under the VAULTS tab, and the
|
||||
hub keeps what is about this phone rather than about a vault.
|
||||
|
||||
Narrow surfaces only. A wide one has the rail, which lists all three already — see
|
||||
PhoneShell.ShowsVaultTabs. The chips are RadioButtons for the reason the keychain's own are: the
|
||||
three are exclusive and a screen reader should say so, and Mode=OneWay so the switch moves when the
|
||||
screen does and never ahead of it.
|
||||
-->
|
||||
<Border IsVisible="{Binding $parent[views:PhoneShell].ShowsVaultTabs}" Padding="16,12,16,4">
|
||||
<Border Classes="segmented">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
<RadioButton Grid.Column="0" Classes="segment" GroupName="vaulttab" Content="Hosts"
|
||||
IsChecked="{Binding IsHostsShowing, Mode=OneWay}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
|
||||
<RadioButton Grid.Column="1" Classes="segment" GroupName="vaulttab" Content="Keychain"
|
||||
IsChecked="{Binding IsKeychainShowing, Mode=OneWay}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Keychain}" />
|
||||
<RadioButton Grid.Column="2" Classes="segment" GroupName="vaulttab" Content="Snippets"
|
||||
IsChecked="{Binding IsSnippetsShowing, Mode=OneWay}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Snippets}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<!-- ============ ◆ the contextual action bar ============ -->
|
||||
@@ -277,67 +330,14 @@
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
The shells strip. The design puts it above the nav on every screen, and that is the single most
|
||||
desktop-shaped idea in the product — it is what makes a terminal a surface you switch to rather than
|
||||
a screen you navigate away from. Kept, because losing it would make an open shell invisible from the
|
||||
host list, which is where you are when you open the next one.
|
||||
◆ THE SHELLS STRIP IS GONE, AND THE BADGE ON CONNECTIONS IS WHAT REPLACED IT.
|
||||
|
||||
v2 draws the sessions as pills rather than as a labelled row, and drops the word SHELLS: with a
|
||||
rounded chip carrying a live dot and a name, the label was spending nine characters of a 360dp row
|
||||
saying what the row already looks like.
|
||||
|
||||
On every screen except the one it names. The terminal draws these same sessions in its own bar, and
|
||||
two rows of the same pills — one of them 46 pixels of it — is the arrangement this surface exists to
|
||||
stop. Wrapped rather than given a second condition, because the strip's own visibility is about
|
||||
whether there are any tabs and this one is about which surface is up.
|
||||
|
||||
◆ And it stands down under the host editor, which is a page rather than a card since the pencil moved
|
||||
into the action bar. A strip of open shells above a form is the same two-rows-of-chrome problem the
|
||||
terminal has, on a screen where the form is the whole point of being there. See
|
||||
PhoneShell.ShowsShellStrip, which is where that "and" is made, Avalonia's bindings having none.
|
||||
It sat above the bar on every page — the single most desktop-shaped idea in the product, kept so that
|
||||
an open shell was never invisible from the host list. The 2026-10-04 redesign keeps that promise with
|
||||
less: Connections carries the number of open sessions in a green badge, and one tap on it is the
|
||||
sessions themselves, in the terminal's own bar. That is 46 pixels of every page given back to the list,
|
||||
and one row of the same pills where there were two on the way into a shell.
|
||||
-->
|
||||
<Panel Grid.Row="2" IsVisible="{Binding $parent[views:PhoneShell].ShowsShellStrip}">
|
||||
<!-- DeepChrome rather than Sidebar, since v5, joining the header and the bar above and below it:
|
||||
the strip is chrome the same way they are — a frame around the screen rather than a pane of
|
||||
it — and Sidebar is what a card sits on, which this row is not. -->
|
||||
<Border IsVisible="{Binding HasTabs}" Background="{StaticResource DeepChrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||
<Button Classes="row" MinHeight="34" Padding="13,0" CornerRadius="10"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<!--
|
||||
Green only while there is a shell behind the tab. It used to be lit unconditionally,
|
||||
which was true when a tab could not exist without a session; one can now —
|
||||
connecting opens the tab first — and a dot that was green before anything had
|
||||
answered would be the one thing on this strip claiming something untrue.
|
||||
|
||||
Amber while it is being made, which is the other half of that correction. Not being
|
||||
green stopped the dot lying, but it left a tab still dialling drawn exactly like a
|
||||
tab whose shell has exited — the two states on this strip with the least in common,
|
||||
one worth waiting for and one over. See Phone.axaml.
|
||||
-->
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}"
|
||||
Classes.connecting="{Binding IsConnecting}" Width="6" Height="6"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- ============ navigation ============ -->
|
||||
<!--
|
||||
@@ -353,20 +353,26 @@
|
||||
◆ It is no longer a single question — a wide surface takes the rail instead — so it reads one flag
|
||||
the control computes rather than a condition here. See PhoneShell.ShowsBottomBar.
|
||||
|
||||
── three rather than four ─────────────────────────────────────────────────────────────────────────
|
||||
The keychain moved behind SETTINGS. A bottom bar is for the places a session moves between, and keys,
|
||||
credentials and tags are not that — they are managed occasionally and then left alone, which is the
|
||||
shape of everything else already behind the hub. What is left is the two halves of using this
|
||||
application, and the drawer holding the rest.
|
||||
── VAULTS · CONNECTIONS · SETTINGS ────────────────────────────────────────────────────────────────
|
||||
Three entries still, and the first is the open vault rather than its host list: hosts, the keychain
|
||||
and snippets are a switch at the top of it. The keychain had moved behind SETTINGS on the argument that
|
||||
a bottom bar is for the places a session moves between; the redesign keeps the bar at three and puts the
|
||||
vault's own contents back one tap from anywhere, which is what the audit's M8 asked for. 72 tall, for
|
||||
the pill and a 12-pixel label under it — see NavButton.
|
||||
-->
|
||||
<!-- DeepChrome rather than Chrome, since v5 — see the remark on the vault header, above, which this
|
||||
bar is the foot of the same frame the header is the top of. -->
|
||||
<Border Grid.Row="3" IsVisible="{Binding $parent[views:PhoneShell].ShowsBottomBar}"
|
||||
<Border Grid.Row="2" IsVisible="{Binding $parent[views:PhoneShell].ShowsBottomBar}"
|
||||
Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" Height="64">
|
||||
BorderThickness="0,1,0,0" Height="72">
|
||||
<Grid ColumnDefinitions="*,*,*">
|
||||
|
||||
<views:NavButton Grid.Column="0" Label="Hosts" Glyph="▤" IsCurrent="{Binding IsHostsShowing}"
|
||||
<!--
|
||||
IsVaultSurface rather than IsHostsShowing, for the reason SETTINGS reads IsMoreSurface: the tab
|
||||
stands for three screens, and it stays lit on whichever of them is up. A tap goes to the hosts,
|
||||
which is where the application opens and what the vault's switch starts on.
|
||||
-->
|
||||
<views:NavButton Grid.Column="0" Label="Vaults" Glyph="" IsCurrent="{Binding IsVaultSurface}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
|
||||
|
||||
@@ -386,9 +392,11 @@
|
||||
shell up the bar is gone and nothing here is read at all. It was left unbound while the bar was
|
||||
collapsed for the whole of the terminal surface, which made a lit state unreachable; a screen the
|
||||
bar can now sit under and not point at is the entry looking broken instead.
|
||||
|
||||
◆ The count is the open tabs, and it is what the shells strip used to say. See the remark above.
|
||||
-->
|
||||
<views:NavButton Grid.Column="1" Label="Connections" Glyph="⌗"
|
||||
IsCurrent="{Binding IsTerminalSurface}"
|
||||
<views:NavButton Grid.Column="1" Label="Connections" Glyph=""
|
||||
IsCurrent="{Binding IsTerminalSurface}" Count="{Binding Tabs.Count}"
|
||||
Command="{Binding ShowTerminalCommand}" />
|
||||
|
||||
<!--
|
||||
@@ -396,18 +404,32 @@
|
||||
and a bar that went dark the moment you opened one of them would only ever light two of its
|
||||
three entries.
|
||||
|
||||
"Settings" and a gear, where it was "More" and a hamburger. The hub held a handful of things
|
||||
that did not fit and could not be called anything better than More; with the keychain, teams and
|
||||
preferences all on it, it is the place everything that is not connecting to something lives —
|
||||
which is what a gear has meant on a phone since before this application existed.
|
||||
"Settings" and a gear, where it was "More" and a hamburger. With the vault's contents moved under
|
||||
VAULTS, what is behind it is about this phone and this account — security, the terminal's
|
||||
behaviour, which vaults are shown, the logs — which is what a gear has meant on a phone since
|
||||
before this application existed.
|
||||
-->
|
||||
<views:NavButton Grid.Column="2" Label="Settings" Glyph="⚙" IsCurrent="{Binding IsMoreSurface}"
|
||||
<views:NavButton Grid.Column="2" Label="Settings" Glyph="" IsCurrent="{Binding IsMoreSurface}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ THE REST OF THE SCRIM, over the two rows of chrome a sheet on the hosts screen cannot reach.
|
||||
|
||||
The hosts screen's sheets are drawn inside the screen's own row, so their scrim dimmed the list and
|
||||
left the header — with the lock on it — and the bottom bar bright and live above and below every
|
||||
sheet (audit M12). These two finish the job in the same colour, declared last so they draw over
|
||||
the header and the bar, and a tap on either does what a tap on the sheet's own scrim does: lowers
|
||||
the sheet. See PhoneShell.ShowsSheetScrim and OnChromeScrimTapped.
|
||||
-->
|
||||
<Button Grid.Row="0" Classes="scrim" IsVisible="{Binding $parent[views:PhoneShell].ShowsSheetScrim}"
|
||||
Click="OnChromeScrimTapped" AutomationProperties.Name="Close the sheet" />
|
||||
<Button Grid.Row="2" Classes="scrim" IsVisible="{Binding $parent[views:PhoneShell].ShowsSheetScrim}"
|
||||
Click="OnChromeScrimTapped" AutomationProperties.Name="Close the sheet" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -43,6 +43,13 @@ internal sealed partial class PhoneShell : UserControl
|
||||
/// <summary>The software keyboard, while this control is attached. Null on a platform without one.</summary>
|
||||
private IInputPane? keyboard;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the software keyboard is up, read from the pane's state rather than from the inset: before
|
||||
/// Android 15 the window itself shrinks for the keyboard and the inset reads zero. See
|
||||
/// <see cref="ApplyKeyboardInset(double)"/>.
|
||||
/// </summary>
|
||||
private bool keyboardOpen;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the lock screen currently showing is the one the application launched into.
|
||||
/// </summary>
|
||||
@@ -89,6 +96,7 @@ internal sealed partial class PhoneShell : UserControl
|
||||
if (shell is not null)
|
||||
{
|
||||
shell.PropertyChanged -= OnShellChanged;
|
||||
shell.Transfers.PropertyChanged -= OnVaultChanged;
|
||||
}
|
||||
|
||||
shell = DataContext as MainWindowViewModel;
|
||||
@@ -96,6 +104,11 @@ internal sealed partial class PhoneShell : UserControl
|
||||
if (shell is not null)
|
||||
{
|
||||
shell.PropertyChanged += OnShellChanged;
|
||||
|
||||
// The files screen's actions sheet is the one sheet outside the hosts screen that the chrome
|
||||
// scrim covers for, so its openness is heard here too. Transfers is built once with the shell
|
||||
// and never replaced, unlike the vault.
|
||||
shell.Transfers.PropertyChanged += OnVaultChanged;
|
||||
ApplyScreenshotPolicy(shell.State);
|
||||
|
||||
thisLockIsTheLaunch = true;
|
||||
@@ -170,9 +183,13 @@ internal sealed partial class PhoneShell : UserControl
|
||||
public static readonly StyledProperty<bool> ShowsHostSelectionBarProperty =
|
||||
AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsHostSelectionBar));
|
||||
|
||||
/// <summary>Whether the strip of open shells above the bottom bar is drawn.</summary>
|
||||
public static readonly StyledProperty<bool> ShowsShellStripProperty =
|
||||
AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsShellStrip));
|
||||
/// <summary>Whether the HOSTS · KEYCHAIN · SNIPPETS switch is drawn under the vault's header.</summary>
|
||||
/// <remarks>
|
||||
/// Narrow surfaces only: a wide one has the rail, which lists the three already, and a switch repeating
|
||||
/// three of the rail's entries an inch to the right of them would be the same choice offered twice.
|
||||
/// </remarks>
|
||||
public static readonly StyledProperty<bool> ShowsVaultTabsProperty =
|
||||
AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsVaultTabs));
|
||||
|
||||
/// <inheritdoc cref="IsWideProperty" />
|
||||
public bool IsWide
|
||||
@@ -209,11 +226,22 @@ internal sealed partial class PhoneShell : UserControl
|
||||
private set => SetValue(ShowsHostSelectionBarProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ShowsShellStripProperty" />
|
||||
public bool ShowsShellStrip
|
||||
/// <summary>Whether the header and the bottom bar are dimmed under a sheet the hosts screen raised.</summary>
|
||||
public static readonly StyledProperty<bool> ShowsSheetScrimProperty =
|
||||
AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsSheetScrim));
|
||||
|
||||
/// <inheritdoc cref="ShowsSheetScrimProperty" />
|
||||
public bool ShowsSheetScrim
|
||||
{
|
||||
get => GetValue(ShowsShellStripProperty);
|
||||
private set => SetValue(ShowsShellStripProperty, value);
|
||||
get => GetValue(ShowsSheetScrimProperty);
|
||||
private set => SetValue(ShowsSheetScrimProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ShowsVaultTabsProperty" />
|
||||
public bool ShowsVaultTabs
|
||||
{
|
||||
get => GetValue(ShowsVaultTabsProperty);
|
||||
private set => SetValue(ShowsVaultTabsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -243,6 +271,13 @@ internal sealed partial class PhoneShell : UserControl
|
||||
/// Losing them on the keychain would be losing the only LOCK button on the surface.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ◆ <b>Narrow, it is the VAULTS tab's header now, on all three of its screens.</b> The keychain and
|
||||
/// snippets moved from behind SETTINGS to the vault's own switch (see
|
||||
/// <see cref="MainWindowViewModel.IsVaultSurface"/>), so the header and the switch under it are drawn
|
||||
/// over hosts, keychain and snippets alike, and those two screens' own headers stand down on a narrow
|
||||
/// surface to make room.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ◆ <b>The header is now a swap rather than a switch, and the editor takes the whole screen.</b> Two
|
||||
/// more flags and two more inputs, both of them the vault's rather than the shell's — see
|
||||
/// <see cref="vault"/>. While hosts are ticked the header stands down and
|
||||
@@ -272,9 +307,11 @@ internal sealed partial class PhoneShell : UserControl
|
||||
var connectPage = shell is { IsTerminalSurface: true, HasTabs: false };
|
||||
|
||||
// The editor is a page of its own now, so nothing else is drawn around it — not the vault header,
|
||||
// not the shells strip, and not the way off the screen. Its own header carries the back arrow, which
|
||||
// not the vault's switch, and not the way off the screen. Its own header carries the back arrow, which
|
||||
// is the one control it needs and the one the system gesture already maps to.
|
||||
var editing = vault?.IsEditing == true;
|
||||
// The keychain's editor page counts too: it is the host editor's shape and wants the same screen.
|
||||
var editing = vault?.IsEditing == true
|
||||
|| (vault?.IsEditingAVaultItem == true && shell?.IsKeychainShowing == true);
|
||||
|
||||
// Only on the hosts screen. The ticks survive a trip to the keychain — the set is not cleared by
|
||||
// navigating — and a bar counting hosts over the transfers screen would be chrome about a list that
|
||||
@@ -293,10 +330,17 @@ internal sealed partial class PhoneShell : UserControl
|
||||
|
||||
IsWide = wide;
|
||||
ShowsRail = wide && (pages || connectPage) && !editing;
|
||||
ShowsBottomBar = !wide && (pages || connectPage) && !editing;
|
||||
ShowsShellStrip = pages && !editing;
|
||||
// ◆ And not while the software keyboard is up. The body shrinks to stay clear of the keyboard, so the
|
||||
// bar rode up on top of it and took 72 pixels from whatever was being typed into — the connect sheet,
|
||||
// most often (audit M12). Every Android app with a bottom bar puts it away for the keyboard.
|
||||
ShowsBottomBar = !wide && (pages || connectPage) && !editing && !keyboardOpen;
|
||||
ShowsHostSelectionBar = pages && choosing && !editing;
|
||||
ShowsVaultHeader = pages && !editing && !choosing && (wide || shell?.IsMoreSurface != true);
|
||||
ShowsVaultHeader = pages && !editing && !choosing && (wide || shell?.IsVaultSurface == true);
|
||||
ShowsVaultTabs = ShowsVaultHeader && !wide;
|
||||
ShowsSheetScrim = pages
|
||||
&& ((vault is not null && HasASheetUp(vault))
|
||||
|| (shell is { Transfers.IsEntryMenuOpen: true }
|
||||
&& (shell.IsTransfersShowing || shell.IsBucketsShowing)));
|
||||
}
|
||||
|
||||
private void OnShellChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@@ -452,10 +496,18 @@ internal sealed partial class PhoneShell : UserControl
|
||||
DispatcherPriority.Background);
|
||||
|
||||
private void OnKeyboardChanged(object? sender, InputPaneStateEventArgs e)
|
||||
=> ApplyKeyboardInset(e.NewState is InputPaneState.Open ? e.EndRect.Height : 0);
|
||||
{
|
||||
keyboardOpen = e.NewState is InputPaneState.Open;
|
||||
ApplyKeyboardInset(keyboardOpen ? e.EndRect.Height : 0);
|
||||
RefreshChrome();
|
||||
}
|
||||
|
||||
private void ApplyKeyboardInset(IInputPane pane)
|
||||
=> ApplyKeyboardInset(pane.State is InputPaneState.Open ? pane.OccludedRect.Height : 0);
|
||||
{
|
||||
keyboardOpen = pane.State is InputPaneState.Open;
|
||||
ApplyKeyboardInset(keyboardOpen ? pane.OccludedRect.Height : 0);
|
||||
RefreshChrome();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the phone's whole interface clear of the software keyboard.
|
||||
@@ -568,7 +620,7 @@ internal sealed partial class PhoneShell : UserControl
|
||||
/// application opens on, which is what every other Android app does.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>v3 adds one guard above the switch rather than another case inside it.</b> The switch's first case
|
||||
/// <b>v3 adds one guard above the switch rather than another case inside it.</b> The switch's second case
|
||||
/// is the membership test of <see cref="MainWindowViewModel.IsMoreSurface"/> minus <c>More</c> itself,
|
||||
/// and has to stay in step with it — a screen added to the hub and not to that case would trap the user
|
||||
/// on it. An editor is not a screen and has no entry there. It is also strictly nearer: the add sheet
|
||||
@@ -608,6 +660,14 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// The files screen's actions sheet, raised from its selection bar.
|
||||
if (current.Transfers.IsEntryMenuOpen)
|
||||
{
|
||||
current.Transfers.IsEntryMenuOpen = false;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryCloseAnOpenEditor(current))
|
||||
{
|
||||
e.Handled = true;
|
||||
@@ -621,28 +681,43 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGoUp(current))
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Moves one level up the screens, for the back gesture.</summary>
|
||||
/// <returns>Whether it moved, and so whether back has been spent.</returns>
|
||||
/// <remarks>See <see cref="OnBackRequested"/>, whose last step this is.</remarks>
|
||||
private bool TryGoUp(MainWindowViewModel current)
|
||||
{
|
||||
switch (current.Screen)
|
||||
{
|
||||
case ShellScreen.Snippets or ShellScreen.Logs or ShellScreen.Transfers
|
||||
or ShellScreen.Buckets or ShellScreen.Preferences or ShellScreen.Vaults
|
||||
or ShellScreen.Keychain:
|
||||
// ◆ The vault's other two screens go back to its hosts rather than to the hub, since the redesign
|
||||
// moved them under VAULTS: the switch at the top of the vault is where they were reached from, and
|
||||
// HOSTS is where that switch starts.
|
||||
case ShellScreen.Keychain or ShellScreen.Snippets:
|
||||
current.ShowScreenCommand.Execute(ShellScreen.Hosts);
|
||||
return true;
|
||||
|
||||
case ShellScreen.Logs or ShellScreen.Transfers
|
||||
or ShellScreen.Buckets or ShellScreen.Preferences or ShellScreen.Vaults:
|
||||
|
||||
// ◆ The hub, or Hosts where there is no hub. A wide surface draws the rail instead and
|
||||
// never draws SETTINGS, so backing out to it would land on a screen with no way off it but
|
||||
// a second back — and the arrows in these screens' own headers are hidden there for the
|
||||
// same reason. Home is where back goes when the thing you came from is not on the surface.
|
||||
current.ShowScreenCommand.Execute(IsWide ? ShellScreen.Hosts : ShellScreen.More);
|
||||
e.Handled = true;
|
||||
break;
|
||||
return true;
|
||||
|
||||
case ShellScreen.More:
|
||||
current.ShowScreenCommand.Execute(ShellScreen.Hosts);
|
||||
e.Handled = true;
|
||||
break;
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Hosts, and anything the phone does not draw. Left unhandled, so back leaves the app.
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,6 +765,25 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Whether one of the hosts screen's four sheets is up.</summary>
|
||||
/// <remarks>The same four <see cref="TryLowerASheet"/> lowers, in the same terms.</remarks>
|
||||
private static bool HasASheetUp(VaultViewModel vault) =>
|
||||
vault.IsHostActionSheetOpen || vault.IsAskingForConnectPassword || vault.IsAddSheetOpen
|
||||
|| vault.GroupSheet is not null || vault.IsKeychainAddSheetOpen;
|
||||
|
||||
/// <summary>A tap on the scrim over the header or the bar: lowers the sheet, as its own scrim does.</summary>
|
||||
private void OnChromeScrimTapped(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (shell is { Transfers.IsEntryMenuOpen: true })
|
||||
{
|
||||
shell.Transfers.IsEntryMenuOpen = false;
|
||||
}
|
||||
else if (shell?.Vault is { } open)
|
||||
{
|
||||
TryLowerASheet(open);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lowers whatever the hosts screen has raised over its list, topmost first.
|
||||
/// </summary>
|
||||
@@ -755,6 +849,12 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return true;
|
||||
}
|
||||
|
||||
if (vault.IsKeychainAddSheetOpen)
|
||||
{
|
||||
vault.CloseKeychainAddSheetCommand.Execute(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (vault.GroupSheet is not null)
|
||||
{
|
||||
vault.CloseGroupSheetCommand.Execute(null);
|
||||
@@ -792,6 +892,11 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return true;
|
||||
}
|
||||
|
||||
if (KeychainScreen.TryCancelAnEditor(vault))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (vault.IsEditingGroup)
|
||||
{
|
||||
vault.CancelGroupEditCommand.Execute(null);
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<!-- The lock screen draws the same ring beside its own fingerprint button. -->
|
||||
<Ellipse Width="16" Height="16" Stroke="{StaticResource Accent}" StrokeThickness="1.5"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Unlock with a fingerprint" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Unlock with a fingerprint" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="body"
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="12.5" FontWeight="SemiBold" Text="{Binding Label}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Preview}"
|
||||
<TextBlock Classes="detail code" FontSize="10.5" Text="{Binding Preview}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Classes="label" FontSize="9" Text="▶ RUNS" Margin="10,0,0,0"
|
||||
@@ -247,7 +247,7 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="mono" FontSize="12.5" Text="{Binding Command}"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="detail" FontSize="10" Text="{Binding Host}"
|
||||
<TextBlock Grid.Column="1" Classes="detail code" FontSize="10" Text="{Binding Host}"
|
||||
Margin="10,0,0,0" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
@@ -27,18 +27,14 @@
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<!-- ============ header ============ -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="56" Margin="8,0">
|
||||
<!--
|
||||
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
|
||||
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
|
||||
somewhere the layout has removed. See PhoneShell.IsWide.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
|
||||
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}" />
|
||||
<!--
|
||||
◆ Wide surfaces only, since the 2026-10-04 redesign, for the reason the keychain's header is: on a narrow
|
||||
one this screen is the SNIPPETS third of the open vault's switch, under the vault's own header. The back
|
||||
arrow went with it, and the + moved to the floating button below — the one shape every add on this head
|
||||
now takes (audit M10), where a header + here meant "new snippet" and the terminal's meant "new session".
|
||||
-->
|
||||
<Grid Grid.Row="0" Height="56" Margin="12,0" IsVisible="{Binding $parent[views:PhoneShell].IsWide}">
|
||||
<TextBlock Classes="heading" Text="Snippets" Margin="4,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- ============ filter ============ -->
|
||||
@@ -103,9 +99,9 @@
|
||||
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"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="SAVE"
|
||||
Command="{Binding SaveCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -119,27 +115,41 @@
|
||||
<TextBlock Classes="body" IsVisible="{Binding !HasVisible}" Margin="24,20" VerticalAlignment="Top"
|
||||
Text="{Binding EmptyMessage}" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Visible}" SelectedItem="{Binding Selected}"
|
||||
<!-- The foot padding is the floating button's: the last card scrolls clear of it. -->
|
||||
<ListBox ItemsSource="{Binding Visible}" SelectedItem="{Binding Selected}" Padding="0,4,0,88"
|
||||
Background="Transparent" BorderThickness="0" IsVisible="{Binding HasVisible}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<!-- A name for TalkBack rather than the row view model's type name (audit H4). -->
|
||||
<Setter Property="AutomationProperties.Name" Value="{ReflectionBinding Label}" />
|
||||
</Style>
|
||||
<!--
|
||||
On the card, as the keychain does it and for the keychain's reason: Border.card paints an opaque
|
||||
surface over the item, so a fill behind it would never show. This list used to be lit by Fluent's
|
||||
own selection blue showing round the card's edges; the theme no longer draws that (audit M3), and
|
||||
the chosen snippet is the one the bar under the list acts on, so it has to look chosen.
|
||||
-->
|
||||
<Style Selector="ListBoxItem:selected Border.card">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TerminalTabAccent}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SnippetRowViewModel">
|
||||
<Border Classes="card" Margin="12,5">
|
||||
<StackPanel Spacing="9">
|
||||
<!--
|
||||
◆ The redesign's snippet card: the name in the sans face, the command under it in the mono, and
|
||||
what a tap on INSERT will do in a badge on the right — RUNS for one that presses Enter, INSERTS
|
||||
for one that waits at the prompt to be read. Both are said, rather than only the first: a card
|
||||
with no badge was the safe one, and the safe one is the one worth naming too.
|
||||
-->
|
||||
<Border Classes="card" Margin="16,4" Padding="14,12">
|
||||
<Grid ColumnDefinitions="*,10,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="5">
|
||||
<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>
|
||||
<TextBlock FontSize="15" FontWeight="SemiBold" Foreground="{StaticResource Text}"
|
||||
Text="{Binding Label}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
|
||||
<Border Classes="tag outline"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
@@ -156,22 +166,39 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- The command, on the surface every block of monospace in this design is drawn on.
|
||||
No radius override any more: it diverged from Border.output's own 14 under v2 for a
|
||||
smaller card-nested block, and now that the style's own value has moved to 12 there is
|
||||
no gap left worth a second number for — it takes the class's, like every other output
|
||||
block on this head. -->
|
||||
<Border Classes="output" Padding="11,9">
|
||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding Preview}"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
<!-- The command, one line of mono under the name. It was a block of its own on the terminal's
|
||||
surface; the redesign keeps the card to two lines so four of them fit where two did. -->
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Preview}"
|
||||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ◆ The badge that says whether this one waits to be read. Amber for the one that does not. -->
|
||||
<Border Grid.Column="2" Classes="tag" IsVisible="{Binding RunsOnInsert}"
|
||||
Background="{StaticResource WarnWash}">
|
||||
<TextBlock Text="RUNS" Foreground="{StaticResource Warn}" />
|
||||
</Border>
|
||||
<Border Grid.Column="2" Classes="tag outline" IsVisible="{Binding !RunsOnInsert}">
|
||||
<TextBlock Text="INSERTS" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Panel>
|
||||
|
||||
<!-- ============ add ============ -->
|
||||
<!--
|
||||
◆ The floating button, where the header's + was: every add on this head is this one shape now — hosts,
|
||||
keychain, snippets — and it is where a thumb already is. Not while the editor is open, which is the
|
||||
add it would raise.
|
||||
-->
|
||||
<Button Grid.Row="3" Classes="fab" Margin="0,0,18,18" HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
AutomationProperties.Name="New snippet" IsVisible="{Binding !IsEditing}"
|
||||
Command="{Binding NewCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="26" />
|
||||
</Button>
|
||||
|
||||
<!-- ============ 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
|
||||
@@ -242,9 +269,9 @@
|
||||
<TextBlock Classes="body"
|
||||
Text="The snippet is re-encrypted with the other vault's key, so everybody who holds that key can read this command and insert it — and nobody else can. Nothing else about it changes." />
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="MOVE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="MOVE"
|
||||
Command="{Binding ConfirmMoveCommand}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelMoveCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
@@ -178,8 +178,10 @@
|
||||
the 44 that matters is the horizontal one, since nothing in a row of this shape is hard to hit
|
||||
above or below.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="icon" Content="←" Height="34" Margin="4,0,0,0"
|
||||
Command="{Binding ShowScreenCommand}" CommandParameter="{Binding Screen}" />
|
||||
<Button Grid.Column="0" Classes="icon" Height="34" Margin="4,0,0,0" AutomationProperties.Name="Back"
|
||||
Command="{Binding ShowScreenCommand}" CommandParameter="{Binding Screen}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="20" />
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
v2 draws these as pills rather than as a segmented strip, so the row is transparent and each
|
||||
@@ -206,7 +208,7 @@
|
||||
BorderThickness="1" CornerRadius="10" Height="30">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Classes="row" MinHeight="28" Padding="11,0" CornerRadius="10"
|
||||
VerticalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center" AutomationProperties.Name="{Binding Label}"
|
||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
@@ -239,13 +241,13 @@
|
||||
to look like.
|
||||
-->
|
||||
<Button Classes="row" MinHeight="28" Width="44" Padding="0" CornerRadius="0,10,10,0"
|
||||
AutomationProperties.Name="{Binding Label, StringFormat='Close {0}'}"
|
||||
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1,0,0,0"
|
||||
Command="{Binding $parent[views:TerminalScreen].((vm:MainWindowViewModel)DataContext).CloseTabCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="×" Foreground="{StaticResource TextFaint}" FontSize="14"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock Classes="icon" Text="" Foreground="{StaticResource TextFaint}"
|
||||
FontSize="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -301,9 +303,12 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="3" Classes="icon" Content="+" Height="34" Margin="0,0,4,0"
|
||||
<Button Grid.Column="3" Classes="icon" Height="34" Margin="0,0,4,0"
|
||||
AutomationProperties.Name="Connect, or open this host's files"
|
||||
Click="OnSheetRequested"
|
||||
Command="{Binding OpenConnectSheetCommand}" />
|
||||
Command="{Binding OpenConnectSheetCommand}">
|
||||
<TextBlock Classes="icon" Text="" FontSize="22" />
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
The wide screen's side panel, shown and hidden — the quick-access keyboard beside the terminal
|
||||
@@ -418,7 +423,7 @@
|
||||
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||||
Text="{Binding HostLabel}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||||
<TextBlock Classes="detail code" FontSize="10.5" Text="{Binding Address}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Classes="detail" FontSize="10"
|
||||
@@ -760,10 +765,32 @@
|
||||
|
||||
<TextBlock Classes="title" Text="CONNECT" FontSize="13" />
|
||||
|
||||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding ShowScreenCommand}"
|
||||
<!--
|
||||
◆ This shell's own host's files, first: the redesign draws a session's terminal and its files as
|
||||
two views of one thing, and this is the nearest this application comes. It opens SFTP to the host
|
||||
the selected tab is on — the desktop sidebar's "Open SFTP", through the same command — so the
|
||||
listing lands on the right machine without choosing it again from a picker. It is a second
|
||||
connection, which is why it is offered rather than switched to; see
|
||||
MainWindowViewModel.SelectFilesHostAsync.
|
||||
-->
|
||||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding OpenOtherSurfaceCommand}"
|
||||
IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
AutomationProperties.Name="Files on this host">
|
||||
<Grid ColumnDefinitions="Auto,14,*">
|
||||
<TextBlock Grid.Column="0" Classes="icon" Text="" FontSize="22"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<StackPanel Grid.Column="2" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Text="Files on this host" />
|
||||
<TextBlock Classes="detail code" Text="{Binding SelectedTab.Address, FallbackValue=''}"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Connect" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="Another shell, on this host or any other in the keychain." />
|
||||
</StackPanel>
|
||||
@@ -779,7 +806,7 @@
|
||||
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect via SFTP" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Connect via SFTP" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="Browse a host's files." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -787,7 +814,7 @@
|
||||
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect via S3" />
|
||||
<TextBlock FontSize="14.5" FontWeight="SemiBold" Foreground="{StaticResource Text}" Text="Connect via S3" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="Objects in an S3-compatible bucket from the keychain." />
|
||||
</StackPanel>
|
||||
@@ -1006,7 +1033,7 @@
|
||||
<TextBlock Grid.Column="0" Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||||
Text="{Binding Label}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="1" Classes="detail" FontSize="10.5" Text="{Binding Sequence}"
|
||||
<TextBlock Grid.Column="1" Classes="detail code" FontSize="10.5" Text="{Binding Sequence}"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="2" Classes="icon" Height="40" Width="40"
|
||||
AutomationProperties.Name="Delete this key" Command="{Binding DeleteCommand}">
|
||||
|
||||
@@ -113,9 +113,9 @@
|
||||
Text="Its key is made on this phone and nobody else has it. Add people to it once it exists, then share the key with them." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="CREATE"
|
||||
<Button Grid.Column="2" Classes="primary" Height="44" Content="CREATE"
|
||||
Command="{Binding CreateVaultCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelNewVaultCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
Reference in new issue
Block a user