Public Access
Merge branch 'claude/sftp-s3-connection-ui-b0730f'
This commit is contained in:
@@ -14,6 +14,21 @@
|
||||
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.
|
||||
|
||||
── v3 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
◆ THE CONNECT BAR IS GONE, and everything it held is inside the right-hand pane now.
|
||||
|
||||
It was a 44-pixel strip across the top of the screen holding a heading, a picker, a password box and a
|
||||
button — chrome spanning both panes to configure one of them, and drawn at full width whether or not
|
||||
anything was ever going to be connected. Underneath it sat a pane that was empty for exactly the same
|
||||
reason, saying so in a sentence nobody had to be told twice.
|
||||
|
||||
So the pane says it instead. Disconnected, the right-hand half is an invitation where the listing would
|
||||
be: what this screen is for, what pressing the button will cost, and the button. Connected, the
|
||||
invitation is replaced by the listing and the connection states itself in a strip above it, beside the
|
||||
control that closes it — which is the arrangement the two facts were always about. The heading the bar
|
||||
carried is not reprinted anywhere: the tab in the strip says SFTP or S3, and it says it whether or not
|
||||
this screen is showing.
|
||||
|
||||
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.
|
||||
-->
|
||||
@@ -71,105 +86,10 @@
|
||||
</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>
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<!-- ============ The two panes ============ -->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,64,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,64,*">
|
||||
|
||||
<!-- ==== This machine ==== -->
|
||||
<!--
|
||||
@@ -292,13 +212,26 @@
|
||||
</Border>
|
||||
|
||||
<!-- ==== The host ==== -->
|
||||
<Grid Grid.Column="2" x:Name="RemotePane" RowDefinitions="Auto,Auto,Auto,Auto,*"
|
||||
<!--
|
||||
Six rows rather than five: the connection's own strip is the new one, and it is inside this pane
|
||||
rather than above both because it is about this pane and nothing else.
|
||||
-->
|
||||
<Grid Grid.Column="2" x:Name="RemotePane" RowDefinitions="Auto,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" />
|
||||
<!--
|
||||
Which kind of remote this pane is for, in the place the local pane names itself. It is the
|
||||
only thing left saying so on the screen itself — the bar that used to print SFTP or S3 across
|
||||
the top is gone — and the pair is worth keeping apart, because HOST and BUCKET is the
|
||||
difference between a directory tree and a flat namespace with inferred folders in it.
|
||||
-->
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="HOST" VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowsHostPicker}" />
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="BUCKET" VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowsBucketPicker}" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="ghost" Content="UP" Command="{Binding RemoteUpCommand}"
|
||||
IsEnabled="{Binding IsConnected}" />
|
||||
@@ -310,6 +243,32 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ WHAT IS OPEN, AND WHAT CLOSES IT. Only while something is.
|
||||
|
||||
A row of its own rather than three more cells in the header above, and the reason is arithmetic
|
||||
rather than taste: this pane is 381 pixels wide at the window's minimum, UP, REFRESH and DELETE
|
||||
take most of that, and an account-at-host chip beside a DISCONNECT would have pushed one of them
|
||||
off the edge. The layout suite would have caught it — which is the point of stating the number
|
||||
here, so the next thing added to either row is measured against it rather than tried.
|
||||
|
||||
Two things and a gap, and the gap is the point: the status line was tried here and does not fit.
|
||||
What is left after a 170-pixel address and a DISCONNECT is about eighty pixels, which turns every
|
||||
sentence into its first word and an ellipsis. It is at the foot of the screen instead — see the
|
||||
queue's own strip, which has the width for one.
|
||||
-->
|
||||
<Border Grid.Row="1" Padding="12,6" Background="{StaticResource Raised}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding IsConnected}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0" Classes="chip accent" MaxWidth="170">
|
||||
<TextBlock Text="{Binding ConnectedTo}" TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
<Button Grid.Column="2" Classes="ghost" Content="DISCONNECT"
|
||||
Command="{Binding DisconnectCommand}" />
|
||||
</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
|
||||
@@ -319,7 +278,7 @@
|
||||
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}"
|
||||
<Border Grid.Row="2" Padding="12,10" Background="{StaticResource DangerWash}"
|
||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding IsConfirmingRemoteDeletion}">
|
||||
<StackPanel Spacing="7">
|
||||
@@ -338,7 +297,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Margin="12,6,12,4">
|
||||
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="12,6,12,4">
|
||||
<ItemsControl Grid.Column="0" ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
@@ -373,14 +332,14 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="2,*,84,110,92" Margin="0,2,12,4">
|
||||
<Grid Grid.Row="4" 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}"
|
||||
<ListBox Grid.Row="5" x:Name="RemoteList" ItemsSource="{Binding RemoteEntries}"
|
||||
SelectedItem="{Binding SelectedRemoteEntry}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
||||
@@ -401,16 +360,143 @@
|
||||
</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>
|
||||
<!--
|
||||
A directory with nothing in it. It no longer has to say anything about not being connected: that
|
||||
state is the invitation below, which covers this pane whole.
|
||||
-->
|
||||
<TextBlock Grid.Row="5" Classes="hint" FontSize="12" Margin="24" MaxWidth="300"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
|
||||
Text="Nothing in this directory." IsVisible="{Binding !HasRemoteEntries}" />
|
||||
|
||||
<!--
|
||||
============ THE INVITATION, which is what this screen opens on ============
|
||||
|
||||
Everything the connect bar used to hold, in the half of the screen it was always about. It covers
|
||||
the pane from below the header to the bottom, opaquely: the breadcrumb, the column headings and
|
||||
the empty listing are all still laid out underneath, and all three are furniture for a listing
|
||||
that does not exist yet.
|
||||
|
||||
Two steps, not one. The first is an invitation with a button — a name for what this screen does, a
|
||||
sentence about what it costs, and one thing to press — and the picker is the second. A combo box
|
||||
sitting open in the middle of an empty pane would be a form with no question above it; the reason
|
||||
the pane is empty is the question, and that is what the first step says.
|
||||
|
||||
◆ The two steps also keep this inside its budget, which is the pane's height less whatever the
|
||||
queue has taken — 268 pixels with three transfers on it. The sentence and the button go away when
|
||||
the picker arrives, so the tall shape is the form rather than form-plus-prose, and neither shape
|
||||
reaches the floor. Anything added here has to hold that; the layout suite measures both.
|
||||
-->
|
||||
<Border Grid.Row="1" Grid.RowSpan="5" Background="{StaticResource Canvas}"
|
||||
IsVisible="{Binding !IsConnected}">
|
||||
<StackPanel Spacing="10" Margin="20" MaxWidth="320"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
|
||||
<!--
|
||||
The mark the hosts screen puts on a group, at the size an empty state can carry one. This
|
||||
application ships no icon set — see the note on colour at the top of this file — so a glyph in
|
||||
a rounded square is what an icon is here, and ▤ is already the one that means "a place things
|
||||
are kept".
|
||||
-->
|
||||
<Border Width="44" Height="44" CornerRadius="12" HorizontalAlignment="Center"
|
||||
Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1">
|
||||
<TextBlock Text="▤" FontSize="18" Foreground="{StaticResource TextDim}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||||
Text="Connect to a host" IsVisible="{Binding ShowsHostPicker}" />
|
||||
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||||
Text="Open a bucket" IsVisible="{Binding ShowsBucketPicker}" />
|
||||
|
||||
<!--
|
||||
The sentence, and the button under it. Both go away once the picker is up — by then they have
|
||||
been read or they have not — which is why they are one collapsing group rather than two
|
||||
elements each answering the same question.
|
||||
|
||||
The sentence is the one the empty pane used to carry, and it is kept rather than dropped
|
||||
because it is the one thing about this screen that surprises people: browsing files is a
|
||||
second login, and it shows up in the host's auth log as one. The bucket's counterpart says
|
||||
the opposite thing for the opposite reason — there is no session to open at all, so nothing
|
||||
is dialled until a listing is asked for.
|
||||
-->
|
||||
<StackPanel Spacing="10" IsVisible="{Binding !IsChoosingRemote}">
|
||||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||||
IsVisible="{Binding ShowsHostPicker}"
|
||||
Text="Browsing a host's files opens its own SFTP connection, so the host records a second login — it is not the channel a terminal uses." />
|
||||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||||
IsVisible="{Binding ShowsBucketPicker}"
|
||||
Text="A bucket has nothing to connect to: the keys and the endpoint are tested by the first listing, not by opening a session." />
|
||||
<Button Classes="accent" HorizontalAlignment="Center" Content="SELECT HOST"
|
||||
IsVisible="{Binding ShowsHostPicker}"
|
||||
Command="{Binding BeginChoosingRemoteCommand}" />
|
||||
<Button Classes="accent" HorizontalAlignment="Center" Content="SELECT BUCKET"
|
||||
IsVisible="{Binding ShowsBucketPicker}"
|
||||
Command="{Binding BeginChoosingRemoteCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- ==== The picker, once it has been asked for ==== -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsChoosingRemote}">
|
||||
|
||||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Hosts}"
|
||||
SelectedItem="{Binding SelectedHost}"
|
||||
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 HorizontalAlignment="Stretch" ItemsSource="{Binding Buckets}"
|
||||
SelectedItem="{Binding SelectedBucket}"
|
||||
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 PasswordChar="•" Text="{Binding TypedPassword}" PlaceholderText="password"
|
||||
IsVisible="{Binding SelectedHostAsksForAPassword}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
|
||||
<Button Classes="accent" Content="{Binding ConnectLabel}"
|
||||
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL"
|
||||
Command="{Binding CancelChoosingRemoteCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
The status line, in the state where the connected strip above is not on screen. Wrapped rather
|
||||
than trimmed, unlike its counterpart up there: what lands here is a refusal with a reason in
|
||||
it — a server's own sentence about why a connection did not open — and an ellipsis through the
|
||||
middle of that is a message that has been shown without being said.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11.5" TextAlignment="Center" Text="{Binding Status}" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
Two highlights rather than one, because refusing is worth showing. Something dragged over a
|
||||
@@ -418,11 +504,11 @@
|
||||
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"
|
||||
<Border Grid.Row="0" Grid.RowSpan="6" IsHitTestVisible="False"
|
||||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||||
BorderThickness="2" IsVisible="{Binding IsRemoteDropTarget}" />
|
||||
|
||||
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
|
||||
<Border Grid.Row="0" Grid.RowSpan="6" IsHitTestVisible="False"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
|
||||
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="12"
|
||||
@@ -435,7 +521,7 @@
|
||||
</Grid>
|
||||
|
||||
<!-- ============ The queue ============ -->
|
||||
<Border Grid.Row="2" Background="{StaticResource Sidebar}" BorderBrush="{StaticResource Border}"
|
||||
<Border Grid.Row="1" Background="{StaticResource Sidebar}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" MaxHeight="196">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
@@ -445,6 +531,22 @@
|
||||
<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" />
|
||||
|
||||
<!--
|
||||
◆ THE STATUS LINE, in the state where the remote pane has no room for one.
|
||||
|
||||
It is here rather than beside DISCONNECT because this row spans the window and that one spans
|
||||
half of it: what the screen has to say about a session is a sentence, and a sentence needs the
|
||||
width. Only while something is open — the other half of the time it is inside the invitation
|
||||
in the remote pane, next to the button that provoked it, which is where a refusal has to be.
|
||||
|
||||
Right-aligned in a free column, so it reads as this row's other end rather than as a third
|
||||
clause of the sentence to its left.
|
||||
-->
|
||||
<TextBlock Grid.Column="2" Classes="hint" FontSize="11.5" Margin="16,0,0,0"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"
|
||||
Text="{Binding Status}" IsVisible="{Binding IsConnected}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -511,7 +613,7 @@
|
||||
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}"
|
||||
<Border Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource Canvas}"
|
||||
IsVisible="{Binding HasPendingHostKey}">
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
@@ -528,7 +630,7 @@
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="0" Grid.RowSpan="3" Background="{StaticResource Canvas}"
|
||||
<Border Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource Canvas}"
|
||||
IsVisible="{Binding HasHostKeyMismatch}">
|
||||
<Border Classes="card" BorderBrush="{StaticResource DangerSoft}">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
@@ -357,6 +357,30 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
/// <summary>Whether the picker is showing buckets.</summary>
|
||||
internal bool ShowsBucketPicker => Remote is RemoteKind.Bucket;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the picker is open, as opposed to the invitation that offers it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The desktop's connect bar is gone — a strip of controls across the top of a screen that is not
|
||||
/// connected to anything, asking a question the empty right-hand pane was already asking silently. What
|
||||
/// replaced it is the pane itself: an invitation where the listing would be, and this flag is the step
|
||||
/// between "connect to a host" and the picker that does it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Two steps rather than a picker sitting open, because the pane is the whole answer to "why is this
|
||||
/// half of the screen empty" and a combo box does not say that. The phone does not use this: its screen
|
||||
/// is one pane at a time, so the picker <em>is</em> what it shows before a connection exists.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It is cleared by everything that changes what the picker would be picking — connecting, disconnecting,
|
||||
/// switching between SFTP and S3, and losing the vault — so an open form is never left over a pane that
|
||||
/// has moved on. See the property-changed hooks at the foot of this file.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private bool isChoosingRemote;
|
||||
|
||||
/// <summary>
|
||||
/// What the button that opens the remote says.
|
||||
/// </summary>
|
||||
@@ -375,8 +399,15 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
[ObservableProperty]
|
||||
private string typedPassword = string.Empty;
|
||||
|
||||
/// <remarks>
|
||||
/// It opens saying what the state is rather than what to do about it, and that is a v3 change the
|
||||
/// desktop forced. "Choose a host and connect to browse its files" was this line for two versions, and
|
||||
/// it was the only thing on the screen saying so — the connect bar it sat in had a picker and a button
|
||||
/// and no prose at all. The invitation that replaced the bar says it in a heading, a sentence and a
|
||||
/// button, so a status line repeating it underneath was the same instruction three times.
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private string status = "Choose a host and connect to browse its files.";
|
||||
private string status = "Nothing is open yet.";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isBusy;
|
||||
@@ -563,6 +594,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
SelectedHost = null;
|
||||
SelectedBucket = null;
|
||||
TypedPassword = string.Empty;
|
||||
|
||||
// The picker with it. Its two lists have just been emptied, so leaving it open would show a form
|
||||
// offering a choice between nothing.
|
||||
IsChoosingRemote = false;
|
||||
}
|
||||
|
||||
// ShowRemoteCommand was here, and it went with the toggle that invoked it. Which kind of remote this
|
||||
@@ -570,6 +605,25 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
// MainWindowViewModel.ShowFiles, which sets Remote directly because it has a refusal to make first.
|
||||
// Keeping the command would have left one nothing could invoke.
|
||||
|
||||
/// <summary>Opens the picker, from the invitation in the empty remote pane.</summary>
|
||||
[RelayCommand]
|
||||
private void BeginChoosingRemote() => IsChoosingRemote = true;
|
||||
|
||||
/// <summary>
|
||||
/// Puts the picker away without connecting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The typed password goes with it. It is a secret nobody asked to keep, and leaving it in the box would
|
||||
/// mean the next person to open the picker — for a different host, possibly — starts with somebody
|
||||
/// else's password already typed in.
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void CancelChoosingRemote()
|
||||
{
|
||||
IsChoosingRemote = false;
|
||||
TypedPassword = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Opens the chosen remote, whichever kind it is.</summary>
|
||||
[RelayCommand]
|
||||
private Task ConnectAsync(CancellationToken cancellationToken) =>
|
||||
@@ -1390,6 +1444,11 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
OnPropertyChanged(nameof(ShowsBucketPicker));
|
||||
OnPropertyChanged(nameof(ConnectLabel));
|
||||
OnPropertyChanged(nameof(SelectedHostAsksForAPassword));
|
||||
|
||||
// Arriving at the other destination shows its invitation rather than a picker somebody left open on
|
||||
// this one — and the picker it would have left open is the wrong one, since the two kinds have
|
||||
// different lists behind them.
|
||||
IsChoosingRemote = false;
|
||||
}
|
||||
|
||||
partial void OnIsConnectedChanged(bool value)
|
||||
@@ -1397,6 +1456,11 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
OnPropertyChanged(nameof(CanDownload));
|
||||
OnPropertyChanged(nameof(CanUpload));
|
||||
OnPropertyChanged(nameof(CanDeleteRemote));
|
||||
|
||||
// Both ways, and the disconnecting half is the one worth stating: closing a session puts the pane
|
||||
// back to its invitation rather than to the form, so what the pane shows after a disconnect is the
|
||||
// same thing it showed before anything was ever connected.
|
||||
IsChoosingRemote = false;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
|
||||
Reference in New Issue
Block a user