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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user