Public Access
Four asks in one pass over the desktop head, and two of them are furniture moving rather than anything new. THE STRIP IS THE WINDOW'S NOW, not the terminal's. Vaults, SFTP and S3 sit at its head and cannot be closed, and open terminals follow them. SFTP and S3 left the nav rail to get there, which is the one semantic change: they are the two destinations you stay in while something runs, and a rail entry is for somewhere you go and come back from. So the rail belongs to the Vaults tab and collapses with it, which also means SFTP, S3 and a terminal each get the full 1016 rather than the 826 a page gets. The tab is expressed as "a page, and not one of the two the strip took" rather than as a fourth ShellSurface. Both are still ShellScreen members and have to be — that is what they are on the phone, where they are two rows in a hub — so a surface each would have been a second way to say a thing Screen already says. IsTransfersShowing and IsBucketsShowing light the other two tabs unchanged. What is new is one field: the page Vaults comes back to, because it is the one tab with sub-navigation and therefore the one that can return to the wrong place. That is not the hidden field ShellSurface argues against — that one would be a second copy of "which page"; this is a tab remembering its own. THE HOSTS SCREEN IS A GRID, and the 268-pixel sidebar is gone. That column was choosing among forty machines and editing one of them at two-thirds width, and it was narrow so the editor beneath it could be a column at all. Cards took the first job at full width; a 304-pixel drawer took the second and collapses when nothing is selected. Pressing a group card narrows the grid; SHOW ALL is the way back. The group editor moved into the drawer as well, which finally makes IsEditingGroup mean the same thing on both heads — it was the phone's alone, because the desktop's editor was a bar that was always on screen. AreHostsExpanded and ToggleHosts went with the control that used them. They folded the whole list away under one heading, an affordance that existed because the column was narrow. Folding a single group is a different thing and is still here. THE TYPE SCALE IS A POINT LARGER and the text ramp is white. The base size was never stated anywhere — a bare TextBlock took TextElement's default of 12 — so raising the scale meant naming it, on Window and on UserControl. The second selector is not redundancy: the layout harness hosts a UserControl in a window it builds itself, and without it the suite would measure every screen a point smaller than it ships, silently. A selector on TextBlock would have been the obvious way and is wrong, because a style setter beats an inherited value and would collapse every deliberate step back to one number. #E3E7F4 is a blue-tinted white on blue-black surfaces, which costs contrast twice — once for being darker than white and once for sharing a hue with what it is drawn on. Pure white is 18.3:1 against the canvas where that was 15.5:1. Every step below moved with the top, so the intervals the design chose are kept and TextDim clears 9:1 against 6.4:1. The palette is shared, so the phone has both changes too. TWO DEFECTS THE HARNESS STRUCTURALLY CANNOT SEE, found by rendering the screen rather than by measuring it, and both now covered. The tile was 232 and was first written as 248, from arithmetic that left out the scrolling stack's own margins. Every layout test passed — the harness asks whether a control is inside the window, never how many fit on a line — so the grid quietly became one column wide at exactly the minimum this application guarantees, which is the shape cards exist to avoid. TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen counts columns instead, and fails at 248. And a card's text ran past its own border, because a horizontal StackPanel measures children with infinite width: a TextBlock inside one never learns it is short of room, so TextTrimming never fires. Both card rows are grids with a star column that gives way and an Auto column that does not — a hostname with its tail cut is still the machine you were looking for, where a badge or the word naming an auth method is not. The keychain header changed shape for the same class of reason. It was Auto,Auto,*,Auto with the buttons last, so the slack column was the only thing absorbing a change of width and five buttons fell off the right edge the moment the type grew. That is how GENERATE lost the word KEY once already. The summary sits in the star column and trims now, so the buttons always get their width. HostSidebarTests became HostGridTests and moved to the grid with the gestures it drives. docs/design-import-gaps.md gains a v3 section naming the five toolbar controls in the design with nothing behind them — a view-mode switch, a tag filter, a calendar, a share control and Serial — and manual-checks.md and the README follow the controls that moved.
548 lines
30 KiB
XML
548 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">
|
||
|
||
<!--
|
||
The screen names whichever remote it is offering, because since v2 it is reached as two
|
||
destinations rather than one: SFTP and S3 are separate tabs in the strip — they were rail entries
|
||
until v3 — and this control draws both. A single "FILES" heading over a bucket picker would name
|
||
neither of them.
|
||
-->
|
||
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="12" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||
Margin="0,0,12,0" IsVisible="{Binding ShowsHostPicker}" />
|
||
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="12" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||
Margin="0,0,12,0" IsVisible="{Binding ShowsBucketPicker}" />
|
||
|
||
<!--
|
||
The HOST / BUCKET pair that used to sit here is gone: which sort of remote this screen offers is
|
||
now the tab you chose, and a toggle that silently moved you to the other one would leave the lit
|
||
tab naming a screen you are no longer on. What sets it is ShowFiles on the shell, which is what
|
||
the tab calls.
|
||
-->
|
||
|
||
<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="12"
|
||
Foreground="{StaticResource Text}" />
|
||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10"
|
||
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="12"
|
||
Foreground="{StaticResource Text}" />
|
||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="10"
|
||
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>
|
||
|
||
<!--
|
||
Column 6, not 5, which is where it used to be — sharing a cell with the connected chip, so the two
|
||
drew over each other for as long as anything was open. That was survivable while the status was
|
||
mostly read before connecting; it is not now, because refusing to switch between SFTP and S3 while
|
||
a session is live reports itself here, which is precisely when the chip is on screen.
|
||
-->
|
||
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="11.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="11"
|
||
Foreground="{StaticResource TextDim}" />
|
||
</Button>
|
||
<TextBlock Classes="mono" Text="›" FontSize="11"
|
||
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="9.5" Margin="12,0,8,0" />
|
||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
|
||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.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="12"
|
||
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="10.5"
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="10.5"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
</Grid>
|
||
</DataTemplate>
|
||
</ListBox.ItemTemplate>
|
||
</ListBox>
|
||
|
||
<TextBlock Grid.Row="3" Classes="hint" FontSize="12" 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="14" TextWrapping="Wrap"
|
||
Text="{Binding PendingRemoteDeletion.Question}" />
|
||
<SelectableTextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource Danger}"
|
||
Text="{Binding PendingRemoteDeletion.FullPath}" />
|
||
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" 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="11"
|
||
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="11"
|
||
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="9.5" Margin="12,0,8,0" />
|
||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
|
||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
|
||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.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="12"
|
||
Margin="12,0,8,0" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Size}" FontSize="10.5"
|
||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Modified}" FontSize="10.5"
|
||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||
<TextBlock Grid.Column="4" Classes="mono perms" Classes.loose="{Binding IsWorldWritable}"
|
||
Text="{Binding Permissions}" FontSize="10.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="12" 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="12" 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="12"
|
||
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="10.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="11.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="12"
|
||
Foreground="{StaticResource Accent}" VerticalAlignment="Center" />
|
||
<StackPanel Grid.Column="1" Margin="0,0,8,0">
|
||
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="11.5"
|
||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Classes="mono" Text="{Binding Path}" FontSize="10"
|
||
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="10.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="12" 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>
|