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
542 lines
30 KiB
XML
542 lines
30 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||
x:Class="DodoSSH.Client.App.Views.TransfersScreen"
|
||
x:DataType="vm:TransfersViewModel">
|
||
|
||
<!--
|
||
File transfer: this machine on the left, the host on the right, and the queue underneath.
|
||
|
||
Two things about this screen are worth knowing before reading the markup. It has its own CONNECT
|
||
button, because SSH.NET cannot open an SFTP subsystem on a transport that is already carrying a shell —
|
||
so browsing a host's files is a second authenticated connection rather than a second channel, and
|
||
pretending otherwise would hide a second login from the person whose audit log it appears in. And the
|
||
panes are symmetrical apart from one column: PERMS is remote-only, because a POSIX mode is not a fact
|
||
about a file on the machine this client is developed on.
|
||
|
||
What the design has and this does not: `sftp over bastion-eu`, which needs jump hosts the connection
|
||
layer has not got. See docs/design-import-gaps.md.
|
||
-->
|
||
|
||
<UserControl.Styles>
|
||
<!--
|
||
A directory is marked by colour rather than by an icon: this application ships no icon set, and the
|
||
palette already reserves blue for "a directory, a distinct scope" — see App.axaml, where it is
|
||
described as deliberately rare. This is the one place it is spent.
|
||
|
||
Two further colours come from the mode, and they are split across the two columns on purpose: NAME says
|
||
what a row is, PERMS says what is notable about how it is set. So an executable is green in NAME —
|
||
"live, yours, something that runs" — while a file anyone may write to is amber in PERMS, over the
|
||
characters that actually say so. The two never compete for one TextBlock, which is what lets a
|
||
world-writable executable show both facts instead of one winning an argument.
|
||
|
||
Both are files only; see SftpEntry, which will not read a mode off a symbolic link or a directory.
|
||
Rendering `-rwxrwxrwx` in two colours at once is not something this list can do, so amber over the whole
|
||
string is the compromise: the eye lands on the column, and the string itself is the detail.
|
||
-->
|
||
<Style Selector="TextBlock.entry">
|
||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||
</Style>
|
||
<!--
|
||
Two different hues, and after v2 that takes saying. These marks encode two independent facts in one
|
||
column, so they have to be told apart at a glance — and they used to be, for free, because the accent
|
||
was green and Info was blue. v2 made the accent blue too, which put #5B8CFF beside #7FB0FF: the same
|
||
hue two steps apart, which is a shade rather than a distinction.
|
||
|
||
So an executable is Live green now. It is the one use of that colour that is not about a session, and
|
||
it earns it on the same grounds — it is a fact about the file rather than something to press, and it
|
||
is the colour this marker already was before the accent moved out from under it.
|
||
-->
|
||
<Style Selector="TextBlock.entry.dir">
|
||
<Setter Property="Foreground" Value="{StaticResource Info}" />
|
||
</Style>
|
||
<Style Selector="TextBlock.entry.exec">
|
||
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
||
</Style>
|
||
|
||
<!--
|
||
Faint by default, as this column has always been: a mode is there so its absence would be noticed. It
|
||
steps up to amber only when it has something to say, which is the whole reason the default is quiet.
|
||
-->
|
||
<Style Selector="TextBlock.perms">
|
||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||
</Style>
|
||
<!--
|
||
Warn rather than WarnText, which is the muted amber a warning card writes its sentences in. At 9.5px
|
||
against TextFaint that one is a shade, not a signal, and a marker nobody notices is the same as no
|
||
marker at all.
|
||
-->
|
||
<Style Selector="TextBlock.perms.loose">
|
||
<Setter Property="Foreground" Value="{StaticResource Warn}" />
|
||
</Style>
|
||
</UserControl.Styles>
|
||
|
||
<Grid RowDefinitions="44,*,Auto">
|
||
|
||
<!-- ============ The host, and the connection ============ -->
|
||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||
<Grid ColumnDefinitions="Auto,Auto,180,Auto,Auto,Auto,*" VerticalAlignment="Center">
|
||
|
||
<TextBlock Grid.Column="0" Classes="mono" Text="FILES" FontSize="11" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||
Margin="0,0,12,0" />
|
||
|
||
<!--
|
||
Which sort of remote. Two buttons rather than one picker holding hosts and buckets together, and
|
||
the reason is that the two are not interchangeable: a host brings a password box, a host key
|
||
prompt and a mismatch refusal with it, and a bucket has no equivalent of any of them. One picker
|
||
would mean half this bar appearing and disappearing with the selection.
|
||
-->
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="2" Margin="0,0,8,0"
|
||
IsVisible="{Binding !IsConnected}">
|
||
<Button Classes="flat cat" Content="HOST" Classes.active="{Binding ShowsHostPicker}"
|
||
Command="{Binding ShowRemoteCommand}"
|
||
CommandParameter="{x:Static vm:RemoteKind.Host}" />
|
||
<Button Classes="flat cat" Content="BUCKET" Classes.active="{Binding ShowsBucketPicker}"
|
||
Command="{Binding ShowRemoteCommand}"
|
||
CommandParameter="{x:Static vm:RemoteKind.Bucket}" />
|
||
</StackPanel>
|
||
|
||
<ComboBox Grid.Column="2" ItemsSource="{Binding Hosts}"
|
||
SelectedItem="{Binding SelectedHost}"
|
||
IsEnabled="{Binding !IsConnected}"
|
||
IsVisible="{Binding ShowsHostPicker}"
|
||
PlaceholderText="choose a host">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||
<StackPanel>
|
||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11"
|
||
Foreground="{StaticResource Text}" />
|
||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="9"
|
||
Foreground="{StaticResource TextFaint}" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<ComboBox Grid.Column="2" ItemsSource="{Binding Buckets}"
|
||
SelectedItem="{Binding SelectedBucket}"
|
||
IsEnabled="{Binding !IsConnected}"
|
||
IsVisible="{Binding ShowsBucketPicker}"
|
||
PlaceholderText="choose a bucket">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
||
<StackPanel>
|
||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="11"
|
||
Foreground="{StaticResource Text}" />
|
||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="9"
|
||
Foreground="{StaticResource TextFaint}" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<!--
|
||
Only for a host bound to nothing, exactly as the hosts screen's box is — and it is a different box
|
||
holding a different value. This connection authenticates separately, so a password typed to open a
|
||
terminal was never offered here.
|
||
-->
|
||
<TextBox Grid.Column="3" Width="150" Margin="6,0,0,0" PasswordChar="•"
|
||
Text="{Binding TypedPassword}" PlaceholderText="password"
|
||
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||
IsEnabled="{Binding !IsConnected}" />
|
||
|
||
<Button Grid.Column="4" Classes="accent" Content="{Binding ConnectLabel}" Margin="6,0,0,0"
|
||
Command="{Binding ConnectCommand}"
|
||
IsVisible="{Binding !IsConnected}"
|
||
IsEnabled="{Binding !IsBusy}" />
|
||
|
||
<Button Grid.Column="4" Classes="ghost" Content="DISCONNECT" Margin="6,0,0,0"
|
||
Command="{Binding DisconnectCommand}"
|
||
IsVisible="{Binding IsConnected}" />
|
||
|
||
<Border Grid.Column="5" Classes="chip accent" Margin="8,0,0,0"
|
||
IsVisible="{Binding IsConnected}">
|
||
<TextBlock Text="{Binding ConnectedTo}" />
|
||
</Border>
|
||
|
||
<TextBlock Grid.Column="5" Classes="hint" Text="{Binding Status}" FontSize="10.5"
|
||
Margin="12,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||
TextWrapping="NoWrap" />
|
||
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!-- ============ The two panes ============ -->
|
||
<Grid Grid.Row="1" ColumnDefinitions="*,64,*">
|
||
|
||
<!-- ==== This machine ==== -->
|
||
<!--
|
||
AllowDrop on the pane rather than on the list, because an empty directory lays its ListBox out at
|
||
zero height behind the empty-state sentence — a handler on the list would have nothing to hit.
|
||
This side takes remote rows only; see TransfersScreen.axaml.cs.
|
||
-->
|
||
<Grid Grid.Column="0" x:Name="LocalPane" RowDefinitions="Auto,Auto,Auto,*" DragDrop.AllowDrop="True">
|
||
|
||
<Border Grid.Row="0" Padding="12,7" BorderBrush="{StaticResource BorderSubtle}"
|
||
BorderThickness="0,0,0,1">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="label" Text="THIS MACHINE" VerticalAlignment="Center" />
|
||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||
<!--
|
||
The drives, because the breadcrumb cannot reach them: above C:\ is a list rather than a
|
||
directory. Without this the pane is stuck on whichever drive the user profile is on.
|
||
-->
|
||
<ItemsControl ItemsSource="{Binding LocalRoots}" VerticalAlignment="Center">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||
<Button Classes="ghost" Content="{Binding Name}"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoLocalCommand}"
|
||
CommandParameter="{Binding Path}" />
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
<Button Classes="ghost" Content="UP" Command="{Binding LocalUpCommand}" />
|
||
<Button Classes="ghost" Content="REFRESH" Command="{Binding RefreshLocalCommand}" />
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<ItemsControl Grid.Row="1" ItemsSource="{Binding LocalTrail}" Margin="12,6,12,4">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<WrapPanel Orientation="Horizontal" />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||
<StackPanel Orientation="Horizontal">
|
||
<Button Classes="flat" Padding="3,1"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoLocalCommand}"
|
||
CommandParameter="{Binding Path}">
|
||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10"
|
||
Foreground="{StaticResource TextDim}" />
|
||
</Button>
|
||
<TextBlock Classes="mono" Text="›" FontSize="10"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Grid Grid.Row="2" ColumnDefinitions="2,*,84,110" Margin="0,2,12,4">
|
||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" Margin="12,0,8,0" />
|
||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="8.5" />
|
||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="8.5" />
|
||
</Grid>
|
||
|
||
<ListBox Grid.Row="3" x:Name="LocalList" ItemsSource="{Binding LocalEntries}"
|
||
SelectedItem="{Binding SelectedLocalEntry}">
|
||
<ListBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:LocalEntryRowViewModel">
|
||
<Grid ColumnDefinitions="2,*,84,110" Margin="0,5,12,5">
|
||
<Border Grid.Column="0" Classes="rowmark" />
|
||
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||
Text="{Binding Name}" FontSize="11"
|
||
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="9.5"
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="9.5"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ListBox.ItemTemplate>
|
||
</ListBox>
|
||
|
||
<TextBlock Grid.Row="3" Classes="hint" FontSize="11" Margin="24" MaxWidth="260"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
|
||
Text="Nothing in this folder. Use the trail above to go somewhere else."
|
||
IsVisible="{Binding !HasLocalEntries}" />
|
||
|
||
<!--
|
||
The drop highlight, over the whole pane and last so it is on top.
|
||
|
||
IsHitTestVisible="False" is not optional. An overlay that takes part in hit testing swallows the
|
||
DragOver events underneath it the moment it appears — so the pointer leaves, the highlight never
|
||
clears, and the drop lands nowhere.
|
||
-->
|
||
<Border Grid.Row="0" Grid.RowSpan="4" IsHitTestVisible="False"
|
||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||
BorderThickness="2" IsVisible="{Binding IsLocalDropTarget}" />
|
||
|
||
</Grid>
|
||
|
||
<!-- ==== The two directions ==== -->
|
||
<Border Grid.Column="1" Background="{StaticResource Sidebar}"
|
||
BorderBrush="{StaticResource Border}" BorderThickness="1,0">
|
||
<StackPanel VerticalAlignment="Center" Spacing="10" Margin="6">
|
||
<!--
|
||
Pointing at the pane the file is going to, which is the only reading that survives the panes
|
||
being side by side: the left-hand pane is this machine, so an upload points right.
|
||
-->
|
||
<Button Classes="ghost" Content="→" HorizontalAlignment="Stretch"
|
||
HorizontalContentAlignment="Center"
|
||
Command="{Binding UploadCommand}" IsEnabled="{Binding CanUpload}"
|
||
ToolTip.Tip="Upload the selected file to the directory showing on the host" />
|
||
<Button Classes="ghost" Content="←" HorizontalAlignment="Stretch"
|
||
HorizontalContentAlignment="Center"
|
||
Command="{Binding DownloadCommand}" IsEnabled="{Binding CanDownload}"
|
||
ToolTip.Tip="Download the selected file into the folder showing on this machine" />
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!-- ==== The host ==== -->
|
||
<Grid Grid.Column="2" x:Name="RemotePane" RowDefinitions="Auto,Auto,Auto,Auto,*"
|
||
DragDrop.AllowDrop="True">
|
||
|
||
<Border Grid.Row="0" Padding="12,7" BorderBrush="{StaticResource BorderSubtle}"
|
||
BorderThickness="0,0,0,1">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="label" Text="HOST" VerticalAlignment="Center" />
|
||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||
<Button Classes="ghost" Content="UP" Command="{Binding RemoteUpCommand}"
|
||
IsEnabled="{Binding IsConnected}" />
|
||
<Button Classes="ghost" Content="REFRESH" Command="{Binding RefreshRemoteCommand}"
|
||
IsEnabled="{Binding IsConnected}" />
|
||
<Button Classes="danger" Content="DELETE" Command="{Binding DeleteRemoteCommand}"
|
||
IsEnabled="{Binding CanDeleteRemote}" />
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!--
|
||
The question DELETE asks. Under the button rather than over the pane, so the row it is about is
|
||
still on screen and still selected while it is being answered — and it names the full path rather
|
||
than the file, because a name is the half that does not identify anything.
|
||
|
||
This is the strongest warning on any of these screens, and deliberately: everything else this
|
||
application deletes is a tombstone against a copy the server still has, and a file on somebody's
|
||
host is bytes with nothing behind them.
|
||
-->
|
||
<Border Grid.Row="1" Padding="12,10" Background="{StaticResource DangerWash}"
|
||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||
IsVisible="{Binding IsConfirmingRemoteDeletion}">
|
||
<StackPanel Spacing="7">
|
||
<TextBlock Classes="heading" FontSize="13" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteDeletion.Question}" />
|
||
<SelectableTextBlock Classes="mono" FontSize="10.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource Danger}"
|
||
Text="{Binding PendingRemoteDeletion.FullPath}" />
|
||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="11" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteDeletion.Consequence}" />
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<Button Classes="danger" Content="DELETE ON THE HOST"
|
||
Command="{Binding ConfirmDeleteRemoteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelDeleteRemoteCommand}" />
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Margin="12,6,12,4">
|
||
<ItemsControl Grid.Column="0" ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate>
|
||
<WrapPanel Orientation="Horizontal" />
|
||
</ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:CrumbViewModel">
|
||
<StackPanel Orientation="Horizontal">
|
||
<TextBlock Classes="mono" Text="/" FontSize="10"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
<Button Classes="flat" Padding="3,1"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
||
CommandParameter="{Binding Path}">
|
||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10"
|
||
Foreground="{StaticResource TextDim}" />
|
||
</Button>
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<!--
|
||
Making a directory sits here, beside the path it would be made in, rather than with the queue's
|
||
controls. It exists because the queue refuses to overwrite: without somewhere else to put a file,
|
||
"that name is already taken" is a dead end.
|
||
-->
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||
IsVisible="{Binding IsConnected}">
|
||
<TextBox Width="140" Text="{Binding NewRemoteFolder}" PlaceholderText="new directory" />
|
||
<Button Classes="ghost" Content="MKDIR" Command="{Binding CreateRemoteFolderCommand}" />
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<Grid Grid.Row="3" ColumnDefinitions="2,*,84,110,92" Margin="0,2,12,4">
|
||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="8.5" Margin="12,0,8,0" />
|
||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="8.5" />
|
||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="8.5" />
|
||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="8.5" />
|
||
</Grid>
|
||
|
||
<ListBox Grid.Row="4" x:Name="RemoteList" ItemsSource="{Binding RemoteEntries}"
|
||
SelectedItem="{Binding SelectedRemoteEntry}">
|
||
<ListBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
||
<Grid ColumnDefinitions="2,*,84,110,92" Margin="0,5,12,5">
|
||
<Border Grid.Column="0" Classes="rowmark" />
|
||
<TextBlock Grid.Column="1" Classes="mono entry" Classes.dir="{Binding IsNavigable}"
|
||
Classes.exec="{Binding IsExecutable}"
|
||
Text="{Binding Name}" FontSize="11"
|
||
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="9.5"
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="9.5"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="4" Classes="mono perms" Classes.loose="{Binding IsWorldWritable}"
|
||
Text="{Binding Permissions}" FontSize="9.5" VerticalAlignment="Center" />
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ListBox.ItemTemplate>
|
||
</ListBox>
|
||
|
||
<StackPanel Grid.Row="4" Spacing="10" Margin="24" MaxWidth="300"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||
IsVisible="{Binding !HasRemoteEntries}">
|
||
<TextBlock Classes="hint" FontSize="11" TextAlignment="Center"
|
||
Text="Connect to a host to browse its files. This opens its own SFTP connection, so the host records a second login — it is not the same channel as a terminal."
|
||
IsVisible="{Binding !IsConnected}" />
|
||
<TextBlock Classes="hint" FontSize="11" TextAlignment="Center"
|
||
Text="Nothing in this directory."
|
||
IsVisible="{Binding IsConnected}" />
|
||
</StackPanel>
|
||
|
||
<!--
|
||
Two highlights rather than one, because refusing is worth showing. Something dragged over a
|
||
disconnected pane has to say so under the pointer — a pane that lights up nowhere reads as a
|
||
window that has stopped answering, and the answer arriving after the drop is the answer arriving
|
||
too late. See the local pane for why neither may hit-test.
|
||
-->
|
||
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
|
||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||
BorderThickness="2" IsVisible="{Binding IsRemoteDropTarget}" />
|
||
|
||
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
|
||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
|
||
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="11"
|
||
Foreground="{StaticResource Danger}"
|
||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||
</Border>
|
||
|
||
</Grid>
|
||
|
||
</Grid>
|
||
|
||
<!-- ============ The queue ============ -->
|
||
<Border Grid.Row="2" Background="{StaticResource Sidebar}" BorderBrush="{StaticResource Border}"
|
||
BorderThickness="0,1,0,0" MaxHeight="196">
|
||
<Grid RowDefinitions="Auto,*">
|
||
|
||
<Border Grid.Row="0" Padding="12,7">
|
||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
||
<TextBlock Grid.Column="0" Classes="label" Text="TRANSFERS" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="1" Classes="mono" FontSize="9.5" Margin="10,0,0,0"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||
Text="one at a time · nothing lands at its final name until it is complete" />
|
||
</Grid>
|
||
</Border>
|
||
|
||
<ScrollViewer Grid.Row="1">
|
||
<StackPanel>
|
||
<TextBlock Classes="hint" FontSize="10.5" Margin="12,4,12,14"
|
||
IsVisible="{Binding !HasTransfers}"
|
||
Text="Nothing queued. Choose a file in either pane and press the arrow pointing the way you want it to go." />
|
||
|
||
<ItemsControl ItemsSource="{Binding Transfers}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:TransferRowViewModel">
|
||
<Grid ColumnDefinitions="16,150,*,190,Auto" Margin="12,4">
|
||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding Arrow}" FontSize="11"
|
||
Foreground="{StaticResource Accent}" VerticalAlignment="Center" />
|
||
<StackPanel Grid.Column="1" Margin="0,0,8,0">
|
||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="10.5"
|
||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Classes="mono" Text="{Binding Path}" FontSize="9"
|
||
Foreground="{StaticResource TextFaint}"
|
||
TextTrimming="CharacterEllipsis" />
|
||
</StackPanel>
|
||
|
||
<ProgressBar Grid.Column="2" Height="4" Minimum="0" Maximum="100"
|
||
Value="{Binding Percent}" VerticalAlignment="Center"
|
||
Foreground="{StaticResource Accent}"
|
||
Background="{StaticResource Raised}" />
|
||
|
||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Progress}" FontSize="9.5"
|
||
Margin="10,0" VerticalAlignment="Center"
|
||
TextTrimming="CharacterEllipsis"
|
||
Foreground="{StaticResource TextDim}" />
|
||
|
||
<StackPanel Grid.Column="4" Orientation="Horizontal" Spacing="6">
|
||
<Border Classes="chip">
|
||
<TextBlock Text="{Binding StateLabel}" />
|
||
</Border>
|
||
<Button Classes="ghost" Content="STOP" IsVisible="{Binding IsRunning}"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).CancelTransferCommand}"
|
||
CommandParameter="{Binding}" />
|
||
<Button Classes="ghost" Content="{Binding RetryLabel}" IsVisible="{Binding CanRetry}"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).RetryTransferCommand}"
|
||
CommandParameter="{Binding}" />
|
||
<Button Classes="danger" Content="DISCARD" IsVisible="{Binding IsFinished}"
|
||
Command="{Binding $parent[ItemsControl].((vm:TransfersViewModel)DataContext).DiscardTransferCommand}"
|
||
CommandParameter="{Binding}" />
|
||
</StackPanel>
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Button Classes="ghost" Content="CLEAR FINISHED" Margin="12,6,12,12"
|
||
HorizontalAlignment="Left" Command="{Binding ClearCompletedCommand}"
|
||
IsVisible="{Binding HasTransfers}" />
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!--
|
||
First contact and a changed key, over the whole screen. The same two refusals a terminal makes, and
|
||
they arrive here on their own because this is a separate connection — a host trusted for a shell is
|
||
trusted for this too, but a host nobody has connected to at all is met here first.
|
||
-->
|
||
<Border Grid.Row="0" Grid.RowSpan="3" Background="{StaticResource Canvas}"
|
||
IsVisible="{Binding HasPendingHostKey}">
|
||
<Border Classes="card">
|
||
<StackPanel Spacing="12">
|
||
<TextBlock Classes="heading" Text="Check this host's fingerprint" />
|
||
<TextBlock Classes="hint"
|
||
Text="This host has not been seen before. Compare the fingerprint with what the operator published. Trusting it here also trusts it for terminals, and on your other machines." />
|
||
<SelectableTextBlock Classes="mono" FontSize="11" Foreground="{StaticResource Text}"
|
||
TextWrapping="Wrap" Text="{Binding PendingHostKey.Fingerprint}" />
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<Button Classes="accent" Content="TRUST AND CONNECT" Command="{Binding TrustHostKeyCommand}" />
|
||
<Button Classes="ghost" Content="CANCEL" Command="{Binding RejectHostKeyCommand}" />
|
||
</StackPanel>
|
||
</StackPanel>
|
||
</Border>
|
||
</Border>
|
||
|
||
<Border Grid.Row="0" Grid.RowSpan="3" Background="{StaticResource Canvas}"
|
||
IsVisible="{Binding HasHostKeyMismatch}">
|
||
<Border Classes="card" BorderBrush="{StaticResource DangerSoft}">
|
||
<StackPanel Spacing="12">
|
||
<TextBlock Classes="heading" Text="The host key has changed" Foreground="{StaticResource Danger}" />
|
||
<TextBlock Classes="hint" Text="{Binding HostKeyMismatch}" />
|
||
<TextBlock Classes="hint"
|
||
Text="Nothing was connected, and there is no way to continue from here. If the server was legitimately rebuilt, edit the host on the Hosts screen and choose Forget host key." />
|
||
<Button Classes="ghost" Content="CLOSE" Command="{Binding RejectHostKeyCommand}"
|
||
HorizontalAlignment="Left" />
|
||
</StackPanel>
|
||
</Border>
|
||
</Border>
|
||
|
||
</Grid>
|
||
|
||
</UserControl>
|