Public Access
The Android v2 design is what this head draws now: four destinations in a bottom bar — Hosts, Terminal, Keychain, More — with snippets, SFTP, S3, logs and preferences one tap deeper behind the last. The first design's four had nothing behind them, which is what made a hub worth building. The palette moved from green-black to blue-black, and it moved in the shared project because that is where it lives and the desktop v2 specifies the same seventeen tokens. One colour changed meaning rather than value, and it is the only semantic change in the file. Green used to *be* the accent, so Ellipse.dot.live filled with Accent and "the thing to press" and "a shell is open on this host" were the same colour by construction. v2 makes the accent blue and keeps a green for status alone, which finally separates them: Live is that green and nothing merely interactive may use it. The accent is also two colours now — Accent fills, AccentText writes — because a row of chips in the fill colour is a row of things that all look like the primary action. A palette is not one file, which is the part worth knowing before the next one. Nine hex literals lived outside it: the nav bar's own label colours, the accessory keys and their Ctrl-latched state, two scrims, the window background Android paints before Avalonia has a frame, and the launcher vector. The two C# sites now resolve from the dictionary by name rather than restating it. The renderer's page cannot — it is served to a WebView over a loopback socket — so terminal.css and terminal.js keep hand-copied values and say so at both sites. ShellScreen gained More and Buckets, appended rather than slotted in. SFTP and S3 are one screen over one TransfersViewModel differing only in which picker they offer, and the kind is set by the button that navigates rather than on arrival — doing it in OnScreenChanged made every arrival at Transfers force the picker back to hosts, including the desktop's own rail arriving at a screen with a bucket already open. It refuses to change kind while a session is live, because there is one session behind both destinations and switching under it would title a screen S3 while it listed an SFTP host. What the design draws and this does not, on the usual grounds. The FORWARDING screen: nothing here forwards anything, so every toggle would be a control with no effect — it is a paragraph on the hub naming the absence, for the reason the desktop keeps TEAMS in its rail. The terminal's `23 ms · fwd 5432`. An ED25519 badge and a SHA256 line on keychain cards, which need an algorithm field and a fingerprint the item type does not have. An `agent` chip, for an agent that does not exist. Snippet run history and exit codes. The Logs FOLLOW pill, which claims a live tail over records that are written once at close and read when the screen opens, and the severity filter, which has nothing to count — that chip row is spent on the real choice, which of the two logs. S3 bucket totals and lifecycle. And the + on HOSTS, which would open a host editor this head has not got. SFTP is browse, open and delete. Both transfer commands work, and what they work against is the local pane: QueueDownloads writes to Path.Combine(LocalPath, name), and LocalPath starts at SpecialFolder.UserProfile, which on Android is the application's own private directory. A download would have reported success and left the file where the person who asked for it cannot open it, which is worse than not offering it — a refusal is visible and a file in /data/user/0/ is not. The queue is not drawn either, since nothing here can put anything in it. Both return with the document picker. The foreground service still counts zero transfers, and the reason moved rather than went away. Four defects worth naming, because three of them are the kind that compile. A Button as a ListBox ItemTemplate swallows the pointer press before the list sees it, so the files listing selected nothing and every command reading the selection did nothing — the row is a Border now and the phone-only single-tap-to-open is a Tapped handler, which also keeps a desktop single click from walking into directories. Avalonia type selectors are exact, so TextBlock.fingerprint never matched SelectableTextBlock and every fingerprint on this head rendered proportional and unwrapped: that was breaking the never-truncated rule on the host-key sheet already. The new two-level hierarchy had no handler for the system back gesture, so back left the application from a log screen. And the tab's close cross had shrunk to a 30x32 target flush against the select target, which is the one control here that ends a shell with no confirmation and no undo. Fingerprint unlock is raised on arriving at the lock screen rather than waiting for its button, which is still there. Only at launch: a lock the user asked for is not answered with an immediate request to unlock, which makes LOCK look inert and trains the reflex of authenticating at a prompt nobody asked for. And once, because a declined gesture leaves the passphrase box exactly where it was and a prompt that came back after being dismissed would be a modal you cannot get out of to type into it. Two fixes fall on the desktop. Its file listing coloured directories with Info and executables with Accent, which was blue against green and is now two steps of one blue; an executable is Live now. And a bucket's folders were drawn with a 0001-01-01 timestamp, because a prefix has no modification time — blank now, for the reason a directory's size is blank. Verified by the whole suite: 1309 tests over nineteen projects, none failing, including the layout suite that stands up real Avalonia and parses every desktop screen. Both heads build. Not verified on a device — nothing in this head ever has been; see docs/android-port.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZE3u99BNt6LzgTC5jhbz2
168 lines
8.6 KiB
XML
168 lines
8.6 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
xmlns:views="using:DodoSSH.Client.Android.Views"
|
|
x:Class="DodoSSH.Client.Android.Views.SnippetsScreen"
|
|
x:DataType="vm:SnippetsViewModel"
|
|
Background="{StaticResource Canvas}">
|
|
|
|
<!--
|
|
Design 05 — SNIPPETS, under MORE.
|
|
|
|
Two departures from the mock-up, both because of what a snippet actually is here.
|
|
|
|
The design puts a ▶ Run button on every card. There are two operations rather than one — type the
|
|
command at the prompt, and type it *and* press Enter — and which of them a snippet allows is a property
|
|
of the snippet, set by a checkbox in its editor. ◆ That checkbox is the whole safety property: a
|
|
snippet that does not run on insert leaves the command sitting at the prompt for a human to read before
|
|
committing to it. Collapsing the two into one button called Run would throw that away, so the actions
|
|
are a bar over the bottom of the list, they name the terminal they will type into, and RUN only appears
|
|
for a snippet that is marked as running.
|
|
|
|
And the design's per-card footer — the hosts it ran on, the time, `exit 0` — is not drawn. Nothing here
|
|
records where a snippet was run, when, or what it returned; a snippet is a saved string, and the shell
|
|
it is typed into never reports back. See docs/design-import-gaps.md.
|
|
-->
|
|
|
|
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
|
|
|
<!-- ============ header ============ -->
|
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="56" Margin="8,0">
|
|
<Button Grid.Column="0" Classes="icon" Content="←"
|
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
|
<TextBlock Grid.Column="1" Classes="heading" Text="Snippets" Margin="4,0" />
|
|
<Button Grid.Column="2" Classes="icon accent" Content="+" Command="{Binding NewCommand}" />
|
|
</Grid>
|
|
|
|
<!-- ============ filter ============ -->
|
|
<!-- Matches the command text as well as the name: the workflow is "the one that vacuums", not its label. -->
|
|
<TextBox Grid.Row="1" Classes="field" Margin="14,2,14,8" Text="{Binding Filter}"
|
|
PlaceholderText="search snippets" IsVisible="{Binding HasSnippets}" />
|
|
|
|
<TextBlock Grid.Row="2" Classes="detail" Margin="18,0,18,6" TextWrapping="Wrap"
|
|
Text="{Binding Status}" />
|
|
|
|
<!-- ============ the editor ============ -->
|
|
<!--
|
|
In place, above the list, rather than as a screen of its own — the same decision the keychain's
|
|
delete confirmation makes and for the same reason: a form stacked over the list hides what it is
|
|
about.
|
|
-->
|
|
<Border Grid.Row="3" Classes="card" Margin="12,0" IsVisible="{Binding IsEditing}"
|
|
VerticalAlignment="Top">
|
|
<ScrollViewer>
|
|
<StackPanel Spacing="10">
|
|
<TextBlock Classes="label" Text="NEW SNIPPET" IsVisible="{Binding IsCreating}" />
|
|
<TextBlock Classes="label" Text="EDIT SNIPPET" IsVisible="{Binding !IsCreating}" />
|
|
|
|
<TextBox Classes="field" Text="{Binding EditorLabel}" PlaceholderText="name" />
|
|
|
|
<TextBox Classes="field" Text="{Binding EditorCommand}" PlaceholderText="command"
|
|
Height="120" AcceptsReturn="True" TextWrapping="Wrap"
|
|
VerticalContentAlignment="Top" Padding="14,10" />
|
|
|
|
<TextBox Classes="field" Text="{Binding EditorNotes}" PlaceholderText="notes" />
|
|
|
|
<!--
|
|
◆ The checkbox the whole screen is arranged around, with the paragraph that says why leaving it
|
|
off is the safe default. It is not a preference — it is the difference between a command a human
|
|
reads before committing to it and one that has already run.
|
|
-->
|
|
<CheckBox IsChecked="{Binding EditorRunsOnInsert}" MinHeight="44">
|
|
<TextBlock Classes="mono" FontSize="11.5" Text="Press Enter after inserting this"
|
|
TextWrapping="Wrap" />
|
|
</CheckBox>
|
|
<TextBlock Classes="body"
|
|
Text="Left off, inserting types the command at the prompt and stops, so it can be read and edited before it runs. Nothing here can tell whether the terminal is even at a prompt, which is why that is the default." />
|
|
|
|
<Grid ColumnDefinitions="*,8,*">
|
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
|
Command="{Binding SaveCommand}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
|
Command="{Binding CancelCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
<!-- ============ the list ============ -->
|
|
<Panel Grid.Row="3" IsVisible="{Binding !IsEditing}">
|
|
|
|
<!-- Written by the view model, and it says what a snippet is for rather than that there are none. -->
|
|
<TextBlock Classes="body" IsVisible="{Binding !HasVisible}" Margin="24,20" VerticalAlignment="Top"
|
|
Text="{Binding EmptyMessage}" />
|
|
|
|
<ListBox ItemsSource="{Binding Visible}" SelectedItem="{Binding Selected}"
|
|
Background="Transparent" BorderThickness="0" IsVisible="{Binding HasVisible}">
|
|
<ListBox.Styles>
|
|
<Style Selector="ListBoxItem">
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="MinHeight" Value="0" />
|
|
</Style>
|
|
</ListBox.Styles>
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:SnippetRowViewModel">
|
|
<Border Classes="card" Margin="12,5">
|
|
<StackPanel Spacing="9">
|
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
|
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Label}"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
|
|
<!-- ◆ The chip that says this one does not wait to be read. -->
|
|
<Border Classes="tag" IsVisible="{Binding RunsOnInsert}"
|
|
Background="{StaticResource WarnWash}">
|
|
<TextBlock Text="RUNS" Foreground="{StaticResource Warn}" />
|
|
</Border>
|
|
|
|
<Border Classes="tag outline"
|
|
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
|
<TextBlock Text="{Binding Badge}" />
|
|
</Border>
|
|
</StackPanel>
|
|
|
|
<!-- The command, on the surface every block of monospace in this design is drawn on. -->
|
|
<Border Classes="output" CornerRadius="9" Padding="11,9">
|
|
<TextBlock Classes="mono" FontSize="11" Text="{Binding Preview}"
|
|
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
|
</Border>
|
|
</StackPanel>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
</Panel>
|
|
|
|
<!-- ============ what to do with the chosen one ============ -->
|
|
<!--
|
|
Raised over the list, as the hosts screen raises its connect bar, and for the same reason: there is no
|
|
second column to put it in. Both buttons name the terminal — "TYPE INTO pg-primary" — because on a
|
|
phone the tab strip may be scrolled away and "whatever is in the terminal receives this" is not a
|
|
sentence anybody should have to guess the subject of.
|
|
-->
|
|
<Border Grid.Row="4" IsVisible="{Binding HasSelection}" Background="{StaticResource Chrome}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="14,12"
|
|
IsEnabled="{Binding !IsEditing}">
|
|
<StackPanel Spacing="9">
|
|
|
|
<Button Classes="primary" Content="{Binding InsertLabel}" Command="{Binding InsertCommand}"
|
|
IsEnabled="{Binding CanInsert}" />
|
|
|
|
<!-- Only for a snippet marked as running. For every other one there is nothing this could mean. -->
|
|
<Button Classes="secondary" Height="44" Content="{Binding RunLabel}"
|
|
Command="{Binding RunCommand}" IsVisible="{Binding SelectionRuns}"
|
|
IsEnabled="{Binding CanInsert}" />
|
|
|
|
<Grid ColumnDefinitions="*,8,*">
|
|
<Button Grid.Column="0" Classes="secondary" Height="44" Content="EDIT"
|
|
Command="{Binding EditCommand}" />
|
|
<Button Grid.Column="2" Classes="danger" Height="44" Content="DELETE"
|
|
Command="{Binding DeleteCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|