Public Access
Six defects, all found by running this head on an emulator against a local API rather than by reading it. They are in one commit because two of the files carry several of them and splitting those would mean hand-editing patches; each is argued at its own site. ── the keypad, which is most of this ─────────────────────────────────────────────────────────────────── It was eleven 30-by-44 keys in a horizontal ScrollViewer, and it was wrong twice over. A pan and a press begin with the same few pixels of travel, so a thumb coming down at an angle — which is how a thumb comes down — was read as the start of a pan and the press was never delivered; the row slid slightly instead. And 30 tall is well under the 48 Android asks for, so the presses that did arrive were hard to aim. The arrows, the keys pressed most and in sequence, were the ones parked past the right-hand edge. Termux, Termius, JuiceSSH, ConnectBot and Blink all ship this row and not one of them scrolls it: they divide the width evenly and add a second row when the set does not fit. That is not convention for its own sake — a key whose position is fixed can be hit without looking, which is the whole point of a modifier row used while reading the output above it. So: two UniformGrid rows, every key an equal share of the width, nothing scrollable, 40 tall. Every key kept. Arrows on the lower row where the thumb rests, reordered to ← ↓ ↑ → so the pairs that undo each other are neighbours. It costs about 51 pixels of terminal, four or five lines, and that is the right trade: the row that was cheaper did not work. The keys also gain a pressed state, which they had none of. They are Focusable="False" so that using one does not take the keyboard off the terminal, and a button that never takes focus never shows the adorner Fluent would otherwise give it — leaving no feedback at all on a control whose only job is to be pressed. Their looks move from assignments in the code-behind to Button.hotkey, which is what lets a style say :pressed, and the Ctrl latch becomes a class rather than two brushes written by hand. ── and five smaller ones ──────────────────────────────────────────────────────────────────────────────── The keyboard's inset was never cleared when the app was resumed without the keyboard it was applied for. Android takes the keyboard away as the activity stops and Avalonia's input pane raises no StateChanged, the pane going with the window rather than closing in front of one — so the interface came back holding a whole keyboard's height of empty canvas. 838 of 2340 pixels on a Pixel 5, on every screen, until something opened and closed the keyboard. MainActivity now raises PhoneEnvironment.Resumed and PhoneShell re-asks the pane. It asks rather than assuming zero: a permission prompt or the biometric sheet can hand the activity back with the keyboard genuinely still up, and flattening the inset there would strand the focused box under a real one. TextBlock.detail never wrapped, where .body beside it always has. A block that neither wraps nor trims lays out at its full single-line width and the parent clips it, with no ellipsis to say anything is missing — so the add sheet read "A machine to connect to, filed under a group if you" and stopped. Which of the fifty-odd .detail blocks overflows is a function of width and font scale rather than of the string, so this is a style rather than an attribute on the handful visibly wrong on one phone. The nine that genuinely want one line now say NoWrap beside their TextTrimming, which is the pairing that was always meant. The two port boxes are NumericUpDowns and were the only unthemed controls on this head — Fluent's border and fill, next to a TextBox.field sharing their row. App.axaml has styled NumericUpDown for the desktop all along; this head never had one. Three buttons classed "icon accent" were painting Fluent's own Button.accent, #6275AC, a blue in no palette here. The desktop overrides that style and this head did not, so the same class read correctly there and not here. The host key sheet said "First contact with 10.0.2.2 · 2222 . Nothing in this keychain…" — Avalonia puts a space between adjacent Runs and the last one began with the full stop. A gap before a full stop, on the one dialogue whose whole job is to be read carefully before it is answered. ── checks ────────────────────────────────────────────────────────────────────────────────────────────── manual-checks gains 10.4a and 11.10f. 10.4 already covered dismissing the keyboard with back or the chevron but not leaving the app with it up, which is the path the inset bug shipped through; 10.4a covers both directions, since a fix that flattened the inset on every resume would pass one half and fail the other. 11.10f guards the keypad against a ScrollViewer being put back around it.
640 lines
38 KiB
XML
640 lines
38 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
xmlns:views="using:DodoSSH.Client.Android.Views"
|
|
x:Class="DodoSSH.Client.Android.Views.FilesScreen"
|
|
x:DataType="vm:TransfersViewModel"
|
|
Background="{StaticResource Canvas}">
|
|
|
|
<!--
|
|
Designs 06 and 08 — SFTP and S3, which are one screen.
|
|
|
|
Below the picker the two are the same thing: a bucket and an SFTP host are both an IRemoteFileStore, and
|
|
the pane, the queue and every transfer were written once. What differs is which picker is offered and
|
|
what the button says — OPEN for a bucket, because S3 is request-per-operation and nothing stays
|
|
connected. Drawing them twice would be two copies of a screen obliged to stay identical.
|
|
|
|
**One pane, not two.** The design's LOCAL / REMOTE toggle is not drawn, and this is the decision
|
|
docs/android-port.md took before any of this was built: Android has scoped storage, so there is no
|
|
browsable local filesystem to put in the other half. TransfersViewModel's local pane — LocalPath,
|
|
LocalRoots, LocalEntries — is desktop-only and is left alone here rather than shown empty.
|
|
|
|
◆ **ADD FILES is the way in, and it is the system document picker rather than an UPLOAD button.** There
|
|
is nothing local to select from, so the gesture cannot be "choose on the left, press the arrow": it is
|
|
"point at a document wherever it lives, and it goes to the directory showing". What Android hands back
|
|
is a `content://` URI, so `DocumentStaging` copies it into this application's cache and queues the copy —
|
|
the queue needs a path, a length and a seek, and a document URI promises none of the three. See that
|
|
class for why the copy is a requirement rather than a shortcut, and `QueueStagedUploads` for when it is
|
|
deleted again.
|
|
|
|
◆ **SAVE FILE is the way out, and it is the save picker rather than a DOWNLOAD button.** `QueueDownloads`
|
|
writes to `Path.Combine(LocalPath, name)`, and `LocalPath` on Android is the application's own private
|
|
directory — a download that way would report success and leave the file where the person who asked for
|
|
it cannot open it. So this head does not use it: `QueueDeliveredDownload` runs the transfer into the
|
|
cache and hands the finished bytes to the document `ACTION_CREATE_DOCUMENT` made.
|
|
|
|
The destination is chosen *before* the transfer, which is a decision with a visible cost — the picker
|
|
creates the document when it is dismissed, so a download that then fails leaves an empty file where it
|
|
was pointed. The alternative is a picker raised minutes later, over whatever the person moved on to and
|
|
frequently while this application is backgrounded, where Android will not show one at all.
|
|
|
|
So what ships is browsing a remote and moving files both ways, plus the two remote-side operations that
|
|
need nothing local — opening a directory and deleting. The queue is drawn under the actions, because
|
|
this head can now fill it.
|
|
|
|
◆ **The host key prompts are here too.** File transfer is a second, separate authenticated connection
|
|
and it makes its own trust decision — the host records a second login. So this screen carries its own
|
|
unknown-key prompt and its own changed-key refusal, over TransfersViewModel's own PendingHostKey, and
|
|
the refusal has no continue button here either.
|
|
-->
|
|
|
|
<!--
|
|
Nine rows since the disconnect strip arrived; it is row 7, between the listing and the action bar, so
|
|
that what it says is next to the buttons it replaces the point of. Auto and empty except while the
|
|
session has been lost, so an ordinary browse pays nothing for it.
|
|
-->
|
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*,Auto,Auto">
|
|
|
|
<!-- ============ header ============ -->
|
|
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Height="56" Margin="8,0">
|
|
<!--
|
|
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
|
|
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
|
|
somewhere the layout has removed. See PhoneShell.IsWide.
|
|
-->
|
|
<Button Grid.Column="0" Classes="icon" Content="←"
|
|
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
|
|
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
|
|
|
<TextBlock Grid.Column="1" Classes="heading" Text="SFTP" Margin="4,0"
|
|
IsVisible="{Binding ShowsHostPicker}" />
|
|
<TextBlock Grid.Column="1" Classes="heading" Text="S3" Margin="4,0"
|
|
IsVisible="{Binding ShowsBucketPicker}" />
|
|
|
|
<!-- What is actually open, named. On a bucket this reads s3://name. -->
|
|
<Border Grid.Column="2" Classes="tag" Margin="8,0" HorizontalAlignment="Left"
|
|
IsVisible="{Binding IsConnected}">
|
|
<TextBlock Text="{Binding ConnectedTo}" TextTrimming="CharacterEllipsis" />
|
|
</Border>
|
|
|
|
<Button Grid.Column="3" Classes="icon" Content="↻" Command="{Binding RefreshRemoteCommand}"
|
|
IsVisible="{Binding IsConnected}" />
|
|
</Grid>
|
|
|
|
<!-- ============ opening one ============ -->
|
|
<!--
|
|
ShowsRemoteInvitation rather than !IsConnected, which stopped being the same question when a
|
|
connection could be lost rather than only closed: a session that went away leaves its listing on
|
|
screen, and this card springing back over it would hide the one thing the strip below is offering to
|
|
put the user back into. See TransfersViewModel.ShowsRemoteContents.
|
|
-->
|
|
<Border Grid.Row="1" Classes="card" Margin="12,0,12,8" IsVisible="{Binding ShowsRemoteInvitation}">
|
|
<StackPanel Spacing="10">
|
|
|
|
<ComboBox HorizontalAlignment="Stretch" MinHeight="44" IsVisible="{Binding ShowsHostPicker}"
|
|
ItemsSource="{Binding Hosts}" SelectedItem="{Binding SelectedHost}"
|
|
PlaceholderText="choose a host">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:HostRowViewModel">
|
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
|
|
<ComboBox HorizontalAlignment="Stretch" MinHeight="44" IsVisible="{Binding ShowsBucketChoice}"
|
|
ItemsSource="{Binding Buckets}" SelectedItem="{Binding SelectedBucket}"
|
|
PlaceholderText="choose a bucket">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
|
<StackPanel Spacing="1">
|
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
|
<TextBlock Classes="detail" Text="{Binding Description}" />
|
|
</StackPanel>
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
|
|
<!--
|
|
A picker with nothing in it says nothing, and here it cannot say much more: this head's keychain
|
|
screen reads and deletes and edits nothing, so there is no bucket editor to send anybody to. The
|
|
sentence is therefore the whole of what this can offer, and it names the machine that can — which
|
|
is better than an empty control that reads as a screen still loading. The desktop's counterpart
|
|
has a button, because over there the editor is two clicks away.
|
|
-->
|
|
<TextBlock Classes="detail" TextWrapping="Wrap" IsVisible="{Binding ShowsNoBuckets}"
|
|
Text="No buckets yet. A bucket is a keychain item — its name, keys and endpoint — and this phone can read the keychain but not add to it. Add one on the desktop and it syncs here." />
|
|
|
|
<!--
|
|
Its own box, deliberately not the hosts screen's. This is a separate authentication, so a password
|
|
typed to open a terminal has not been offered here — and quietly reusing it would make a one-time
|
|
password appear to work twice.
|
|
-->
|
|
<TextBox Classes="field secret" IsVisible="{Binding SelectedHostAsksForAPassword}"
|
|
Text="{Binding TypedPassword}" PlaceholderText="password" />
|
|
|
|
<!--
|
|
Gone rather than disabled when there is no bucket to open, because a disabled OPEN under an empty
|
|
picker is the same dead end with a grey button on it. The sentence above is what the card carries
|
|
in that state.
|
|
-->
|
|
<Button Classes="primary" Content="{Binding ConnectLabel}" Command="{Binding ConnectCommand}"
|
|
IsVisible="{Binding !ShowsNoBuckets}" IsEnabled="{Binding !IsBusy}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!-- ============ ◆ the host key decisions ============ -->
|
|
<!--
|
|
The unknown key: the fingerprint in full, wrapping rather than clipping, and two ways out.
|
|
-->
|
|
<Border Grid.Row="2" Margin="12,0,12,8" IsVisible="{Binding HasPendingHostKey}"
|
|
Background="{StaticResource WarnWash}" BorderBrush="{StaticResource WarnSoft}"
|
|
BorderThickness="1" CornerRadius="12" Padding="14">
|
|
<StackPanel Spacing="9">
|
|
<TextBlock Classes="label" Foreground="{StaticResource Warn}" Text="UNKNOWN HOST KEY" />
|
|
<TextBlock Classes="body">
|
|
<Run Text="First contact with" />
|
|
<Run Text="{Binding PendingHostKey.Host}" Foreground="{StaticResource Text}" />
|
|
<Run Text="·" />
|
|
<Run Text="{Binding PendingHostKey.Port}" />
|
|
<Run Text=". Nothing in this keychain has approved this key." />
|
|
</TextBlock>
|
|
<TextBlock Classes="label" Foreground="{StaticResource TextFaint}"
|
|
Text="{Binding PendingHostKey.Algorithm}" />
|
|
<!-- ◆ In full, and wrapping rather than clipping. See the .fingerprint style. -->
|
|
<SelectableTextBlock Classes="fingerprint" Text="{Binding PendingHostKey.Fingerprint}" />
|
|
<Grid ColumnDefinitions="*,8,*">
|
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="TRUST AND OPEN"
|
|
Command="{Binding TrustHostKeyCommand}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
|
Command="{Binding RejectHostKeyCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ The changed key, and it has no continue button. Presenting this as dismissible is the one design
|
|
mistake that matters here — the same rule the hosts screen's sheet follows.
|
|
-->
|
|
<Border Grid.Row="2" Margin="12,0,12,8" IsVisible="{Binding HasHostKeyMismatch}"
|
|
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
|
BorderThickness="1" CornerRadius="12" Padding="14">
|
|
<StackPanel Spacing="9">
|
|
<TextBlock Classes="label" Foreground="{StaticResource Danger}" Text="HOST KEY CHANGED — REFUSED" />
|
|
<TextBlock Classes="body" Foreground="{StaticResource DangerText}" Text="{Binding HostKeyMismatch}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!-- ============ ◆ typing where to go ============ -->
|
|
<!--
|
|
The breadcrumb below reaches only what is under the directory already listed, so crossing a host's
|
|
filesystem was a tap and a round trip a level and a path somebody already knew could not be entered at
|
|
all. This is the way in for one that is: absolute, relative to the directory showing, or starting at
|
|
~. See SftpPath.Resolve.
|
|
|
|
GO is a real button rather than only the keyboard's own action key, and that is a phone decision: a
|
|
software keyboard's return key is whatever the IME decided to draw, this application does not set the
|
|
IME action, and a path box whose only commit was a key that may say "newline" would be a box nothing
|
|
could be submitted from. The Enter binding stays for the hardware keyboards this head does meet.
|
|
-->
|
|
<Grid Grid.Row="3" ColumnDefinitions="*,8,Auto" Margin="14,0,14,6"
|
|
IsVisible="{Binding ShowsRemoteContents}">
|
|
<TextBox Grid.Column="0" Classes="field" Text="{Binding RemotePathDraft}"
|
|
PlaceholderText="/path/to/somewhere">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding GoToTypedRemotePathCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Padding="16,0" Content="GO"
|
|
Command="{Binding GoToTypedRemotePathCommand}" />
|
|
</Grid>
|
|
|
|
<!--
|
|
◆ How many are gathered up, and the one gesture that gathers or releases them all. Only while
|
|
something is — an always-present row would spend 30 pixels of a listing on a count of zero.
|
|
-->
|
|
<Grid Grid.Row="3" ColumnDefinitions="Auto,*" Margin="14,0,14,6" VerticalAlignment="Bottom"
|
|
IsVisible="{Binding HasCheckedRemote}">
|
|
<Button Grid.Column="0" Classes="secondary" MinHeight="36" Padding="12,0"
|
|
Command="{Binding ToggleAllRemoteCommand}">
|
|
<TextBlock Classes="label" FontSize="9" Text="ALL / NONE" />
|
|
</Button>
|
|
<TextBlock Grid.Column="1" Classes="mono" FontSize="12" Margin="10,0,0,0"
|
|
VerticalAlignment="Center" Foreground="{StaticResource AccentText}"
|
|
Text="{Binding CheckedRemoteLabel}" />
|
|
</Grid>
|
|
|
|
<!-- ============ where we are ============ -->
|
|
<ScrollViewer Grid.Row="4" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
|
IsVisible="{Binding ShowsRemoteContents}" Margin="0,0,0,4">
|
|
<StackPanel Orientation="Horizontal" Spacing="4" Margin="14,0" VerticalAlignment="Center">
|
|
<Button Classes="row" MinHeight="36" Padding="9,0" CornerRadius="10"
|
|
Command="{Binding RemoteUpCommand}">
|
|
<TextBlock Classes="mono" FontSize="12" Text="↑" Foreground="{StaticResource AccentText}" />
|
|
</Button>
|
|
<ItemsControl ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="2" /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:CrumbViewModel">
|
|
<Button Classes="row" MinHeight="36" Padding="7,0" CornerRadius="10"
|
|
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
|
CommandParameter="{Binding Path}">
|
|
<TextBlock Classes="detail" FontSize="11" Text="{Binding Name}" />
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
|
|
<!-- ============ pins ============ -->
|
|
<!--
|
|
◆ The phone's answer to the desktop's QUICK ACCESS sidebar — the same PinnedPathList this host was
|
|
connected with, drawn where this head actually browses files rather than beside a terminal it has no
|
|
strip for. See TransfersViewModel.ConnectedPinnedPaths for why this is a snapshot taken at connect
|
|
rather than a live follow of the host row: a pin added or removed mid-session shows up here on the
|
|
next connect, not while this one is still open.
|
|
|
|
Its own row rather than folded into the breadcrumb above: the two scroll independently and mean
|
|
different things — the breadcrumb is where this pane is, the chips are places it can jump to — and a
|
|
shared row would make ↑ look like one more pin among several.
|
|
|
|
Gone rather than empty when nothing is pinned or nothing is connected, which HasConnectedPins already
|
|
answers: an empty scrolling strip under the breadcrumb would read as a loading row rather than as
|
|
"this host has nothing pinned".
|
|
-->
|
|
<ScrollViewer Grid.Row="5" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
|
IsVisible="{Binding HasConnectedPins}" Margin="0,0,0,4">
|
|
<ItemsControl ItemsSource="{Binding ConnectedPinnedPaths}">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" Margin="14,0" /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="x:String">
|
|
<!--
|
|
Classes="row" at its default 44dp MinHeight rather than the breadcrumb's own 36 — these chips
|
|
are the destination, not the trail behind it, and a target worth a deliberate tap gets the
|
|
full touch floor this head holds everything else to. Not trimmed: a chip scrolls sideways with
|
|
the row rather than being squeezed to fit it, so the full path is always what a tap commits to.
|
|
-->
|
|
<Button Classes="row" MinHeight="44" Padding="11,0" CornerRadius="10"
|
|
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
|
CommandParameter="{Binding}">
|
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding}" />
|
|
</Button>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</ScrollViewer>
|
|
|
|
<!-- ============ the listing ============ -->
|
|
<Panel Grid.Row="6">
|
|
|
|
<TextBlock Classes="body" IsVisible="{Binding ShowsRemoteInvitation}" Margin="24,12"
|
|
VerticalAlignment="Top" Text="{Binding Status}" />
|
|
|
|
<!--
|
|
Tapped on the list rather than a Command on the row, and that is not a preference.
|
|
|
|
A Button as the ItemTemplate root swallows the press: the ListBoxItem never sees it, SelectedItem
|
|
stays null, and every command that reads SelectedRemoteEntry — opening a directory, deleting —
|
|
quietly does nothing. Measured, not assumed. So the row is a Border, selection works as it does on
|
|
every other list in this head, and the one phone-specific behaviour is here: a single tap on a
|
|
directory opens it, because a phone has no double-tap-to-open and no Enter.
|
|
|
|
Not driven from OnSelectedRemoteEntryChanged, which would have been the tidier-looking place: that
|
|
setter is in the shared view model, and the desktop pane deliberately opens on DoubleTapped — making
|
|
selection navigate would send a single desktop click walking into a directory.
|
|
-->
|
|
<ListBox ItemsSource="{Binding RemoteEntries}" SelectedItem="{Binding SelectedRemoteEntry}"
|
|
Background="Transparent" BorderThickness="0" IsVisible="{Binding ShowsRemoteContents}"
|
|
Tapped="OnRemoteEntryTapped">
|
|
<ListBox.Styles>
|
|
<Style Selector="ListBoxItem">
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="MinHeight" Value="0" />
|
|
<Setter Property="Margin" Value="10,1" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
</Style>
|
|
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
</Style>
|
|
</ListBox.Styles>
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
|
<Border MinHeight="52" Padding="10,7">
|
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
|
|
<!--
|
|
◆ The tick, which on this head is the only multi-selection there can be: a phone has no
|
|
ctrl-click and no shift-click. It is not the list's selection — that stays "the row I am
|
|
looking at", which RENAME, MOVE and PERMS act on one at a time — and this is "the rows I
|
|
have gathered up", which SAVE and DELETE take. See RemoteEntryRowViewModel.IsChecked.
|
|
|
|
Its own 40-pixel column so a thumb aimed at it does not open the directory beside it: the
|
|
row's own tap navigates, which is this head's one behaviour the desktop does not share.
|
|
-->
|
|
<CheckBox Grid.Column="0" Width="40" MinWidth="0" VerticalAlignment="Center"
|
|
IsChecked="{Binding IsChecked}" Click="OnRemoteRowChecked" />
|
|
|
|
<TextBlock Grid.Column="1" Width="16" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" FontSize="12" Text="▸"
|
|
Foreground="{StaticResource AccentText}" IsVisible="{Binding IsNavigable}" />
|
|
<TextBlock Grid.Column="1" Width="16" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" FontSize="12" Text="·"
|
|
Foreground="{StaticResource TextFaint}" IsVisible="{Binding IsFile}" />
|
|
|
|
<StackPanel Grid.Column="2" Margin="11,0,0,0" Spacing="2" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" FontSize="12.5" Text="{Binding Name}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<StackPanel Orientation="Horizontal" Spacing="7">
|
|
<TextBlock Classes="detail" Text="{Binding Size}"
|
|
IsVisible="{Binding Size, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
<!-- Real POSIX bits from the server, not a plausible-looking mode. -->
|
|
<TextBlock Classes="detail" Text="{Binding Permissions}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<TextBlock Grid.Column="3" Classes="detail" Text="{Binding Modified}"
|
|
VerticalAlignment="Center" />
|
|
</Grid>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
</Panel>
|
|
|
|
<!-- ============ what the screen says once the session has stopped ============ -->
|
|
<!--
|
|
The phone's half of the desktop's SessionEndedBar, and it binds this screen's own view model rather
|
|
than the shell's — this control's DataContext is TransfersViewModel, where the desktop's strip sits in
|
|
MainWindow beside a status bar and can ask which surface it is on. The facts are the same two:
|
|
TransfersViewModel.LostConnection and the reconnect that goes back to the directory showing.
|
|
|
|
A phone needs it more than a desk does, and for the reason its terminal does: a mobile link drops on
|
|
its own and the screen goes off, so this session is discovered later with a listing on it and no
|
|
account of why nothing works. The listing stays deliberately — see ShowsRemoteContents.
|
|
-->
|
|
<Border Grid.Row="7" IsVisible="{Binding IsConnectionLost}" Background="{StaticResource DeepChrome}"
|
|
BorderBrush="{StaticResource Danger}" BorderThickness="0,1,0,0" Padding="12,10">
|
|
<StackPanel Spacing="8">
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
|
<Ellipse Classes="dot" Classes.dropped="True" Width="7" Height="7" VerticalAlignment="Center" />
|
|
<TextBlock Classes="label" FontSize="9" Text="DISCONNECTED" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Two lines maximum, for the reason the terminal's strip caps its own: a transport error can hand
|
|
over whatever the operating system called it, and a strip that grew to fit one would eat the
|
|
listing above it on a screen that has none to spare.
|
|
-->
|
|
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource TextDim}"
|
|
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
|
|
Text="{Binding LostConnection}" />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<Button Classes="row" MinHeight="44" Padding="14,0" Command="{Binding ReconnectCommand}">
|
|
<TextBlock Classes="label" FontSize="9" Text="RECONNECT" />
|
|
</Button>
|
|
<Button Classes="row" MinHeight="44" Padding="14,0" Command="{Binding DisconnectCommand}">
|
|
<TextBlock Classes="label" FontSize="9" Text="CLOSE" />
|
|
</Button>
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!-- ============ what to do with the chosen entry ============ -->
|
|
<!-- DeepChrome rather than Chrome, since v5 — see the remark on PhoneShell's vault header. -->
|
|
<Border Grid.Row="8" IsVisible="{Binding IsConnected}" Background="{StaticResource DeepChrome}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="12,10">
|
|
<StackPanel Spacing="9">
|
|
|
|
<!-- ◆ Counted, and it replaces the actions in place rather than stacking over the row it names. -->
|
|
<Border IsVisible="{Binding IsConfirmingRemoteDeletion}" Background="{StaticResource DangerWash}"
|
|
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="12"
|
|
Padding="13,11">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
|
Text="{Binding PendingRemoteDeletion.Question}" />
|
|
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
|
Text="{Binding PendingRemoteDeletion.Consequence}" />
|
|
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
|
<Button Grid.Column="0" Classes="danger" Height="44" Content="DELETE"
|
|
Command="{Binding ConfirmDeleteRemoteCommand}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP"
|
|
Command="{Binding CancelDeleteRemoteCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ Renaming and moving, in the place the deletion question already occupies and never at the same
|
|
time as it. One form for both — see RemoteMoveRequest for why the two questions share a shape and
|
|
differ only in their words — and no danger colours on it, because a rename is undone by renaming
|
|
back and the deletion above is the one thing here that nothing can walk back.
|
|
-->
|
|
<Border IsVisible="{Binding IsMovingRemote}" Background="{StaticResource Raised}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
|
|
Padding="13,11">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
|
Text="{Binding PendingRemoteMove.Question}" />
|
|
<TextBlock Classes="detail" TextWrapping="Wrap" Text="{Binding PendingRemoteMove.FullPath}" />
|
|
<TextBox Classes="field" Text="{Binding RemoteMoveTarget}"
|
|
PlaceholderText="{Binding PendingRemoteMove.FieldHint}">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding ConfirmMoveRemoteCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<TextBlock Classes="body" Text="{Binding PendingRemoteMove.Note}" />
|
|
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
|
<Button Grid.Column="0" Classes="primary" Height="44"
|
|
Content="{Binding PendingRemoteMove.ConfirmLabel}"
|
|
Command="{Binding ConfirmMoveRemoteCommand}" IsEnabled="{Binding !IsBusy}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
|
Command="{Binding CancelMoveRemoteCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ The mode, as nine boxes and the number they add up to — in the place the other two questions
|
|
already occupy and never at the same time as either. See RemotePermissionsRequest for why it is
|
|
boxes rather than a field to type 755 into, and TransfersScreen.axaml for the desktop's own copy
|
|
of the same form.
|
|
|
|
Compact rather than a second layout: the three columns are the mode's own three triples, and at
|
|
360dp they are exactly what fits. The octal is printed underneath as it changes, which is what
|
|
lets somebody who came here to type chmod check the boxes they just ticked.
|
|
-->
|
|
<Border IsVisible="{Binding IsChangingRemotePermissions}" Background="{StaticResource Raised}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
|
|
Padding="13,11">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
|
Text="{Binding PendingRemotePermissions.Question}" />
|
|
<TextBlock Classes="detail" TextWrapping="Wrap"
|
|
Text="{Binding PendingRemotePermissions.FullPath}" />
|
|
|
|
<Grid ColumnDefinitions="Auto,*,*,*" RowDefinitions="Auto,44,44,44" Margin="0,2">
|
|
<TextBlock Grid.Row="0" Grid.Column="1" Classes="detail" Text="read"
|
|
HorizontalAlignment="Center" />
|
|
<TextBlock Grid.Row="0" Grid.Column="2" Classes="detail" Text="write"
|
|
HorizontalAlignment="Center" />
|
|
<TextBlock Grid.Row="0" Grid.Column="3" Classes="detail"
|
|
Text="{Binding PendingRemotePermissions.ExecuteLabel}"
|
|
HorizontalAlignment="Center" />
|
|
|
|
<TextBlock Grid.Row="1" Grid.Column="0" Classes="mono" FontSize="12" Text="owner"
|
|
VerticalAlignment="Center" Margin="0,0,10,0" />
|
|
<CheckBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.OwnerRead}" />
|
|
<CheckBox Grid.Row="1" Grid.Column="2" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.OwnerWrite}" />
|
|
<CheckBox Grid.Row="1" Grid.Column="3" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.OwnerExecute}" />
|
|
|
|
<TextBlock Grid.Row="2" Grid.Column="0" Classes="mono" FontSize="12" Text="group"
|
|
VerticalAlignment="Center" Margin="0,0,10,0" />
|
|
<CheckBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.GroupRead}" />
|
|
<CheckBox Grid.Row="2" Grid.Column="2" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.GroupWrite}" />
|
|
<CheckBox Grid.Row="2" Grid.Column="3" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.GroupExecute}" />
|
|
|
|
<TextBlock Grid.Row="3" Grid.Column="0" Classes="mono" FontSize="12" Text="others"
|
|
VerticalAlignment="Center" Margin="0,0,10,0" />
|
|
<CheckBox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.OthersRead}" />
|
|
<CheckBox Grid.Row="3" Grid.Column="2" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.OthersWrite}" />
|
|
<CheckBox Grid.Row="3" Grid.Column="3" HorizontalAlignment="Center" MinWidth="0"
|
|
IsChecked="{Binding PendingRemotePermissions.OthersExecute}" />
|
|
</Grid>
|
|
|
|
<TextBlock Classes="mono" FontSize="13" Foreground="{StaticResource AccentText}"
|
|
Text="{Binding PendingRemotePermissions.Preview}" />
|
|
|
|
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="APPLY"
|
|
Command="{Binding ConfirmRemotePermissionsCommand}"
|
|
IsEnabled="{Binding PendingRemotePermissions.HasChanges}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
|
Command="{Binding CancelRemotePermissionsCommand}" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ The queue, above the actions and only when it has something in it. Bounded and scrolling rather
|
|
than growing: five files queued must not push the buttons off the bottom of the screen, which on a
|
|
phone is how a screen becomes unusable rather than merely tall.
|
|
-->
|
|
<ScrollViewer MaxHeight="164" IsVisible="{Binding HasTransfers}"
|
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
|
<ItemsControl ItemsSource="{Binding Transfers}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:TransferRowViewModel">
|
|
<Grid ColumnDefinitions="14,*,Auto" Margin="0,3">
|
|
<TextBlock Grid.Column="0" Classes="mono" FontSize="12" Text="{Binding Arrow}"
|
|
VerticalAlignment="Center" Foreground="{StaticResource AccentText}" />
|
|
|
|
<StackPanel Grid.Column="1" Margin="8,0" Spacing="3">
|
|
<TextBlock Classes="mono" FontSize="12" Text="{Binding Name}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<ProgressBar Height="3" Minimum="0" Maximum="100" Value="{Binding Percent}"
|
|
Foreground="{StaticResource Accent}"
|
|
Background="{StaticResource Raised}" />
|
|
<TextBlock Classes="detail" Text="{Binding Progress}"
|
|
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
One button per row, never two: whichever of the three applies to the state it is in.
|
|
A phone row has space for a name, a bar and one 44-pixel target, and the three are
|
|
mutually exclusive by construction — IsRunning and CanRetry cannot both hold.
|
|
-->
|
|
<StackPanel Grid.Column="2" VerticalAlignment="Center">
|
|
<Button Classes="secondary" MinHeight="36" Padding="10,0" Content="STOP"
|
|
IsVisible="{Binding IsRunning}"
|
|
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).CancelTransferCommand}"
|
|
CommandParameter="{Binding}" />
|
|
<Button Classes="secondary" MinHeight="36" Padding="10,0" Content="{Binding RetryLabel}"
|
|
IsVisible="{Binding CanRetry}"
|
|
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).RetryTransferCommand}"
|
|
CommandParameter="{Binding}" />
|
|
</StackPanel>
|
|
</Grid>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</ScrollViewer>
|
|
|
|
<Button Classes="secondary" Height="40" Content="CLEAR FINISHED" HorizontalAlignment="Stretch"
|
|
IsVisible="{Binding HasTransfers}" Command="{Binding ClearCompletedCommand}" />
|
|
|
|
<!--
|
|
◆ The two directions, on their own row above the pair below — because DELETE is the button on this
|
|
screen that nothing can undo, and it must not sit at a thumb's width from the ones somebody presses
|
|
often. ADD FILES is the primary of the two: it is the one that needs no selection, and the one this
|
|
screen exists for on a phone.
|
|
|
|
SAVE FILE takes the selected row rather than several, and that asymmetry is the platform's: the
|
|
save picker names one destination. CanDownload is the desktop's own flag for the same question —
|
|
a file is selected and something is connected — and is reused rather than restated here.
|
|
-->
|
|
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
|
<Button Grid.Column="0" Classes="primary" Height="44" Content="ADD FILES" Click="OnAddFiles" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="SAVE FILE" Click="OnSaveFile"
|
|
IsEnabled="{Binding CanDownload}" />
|
|
</Grid>
|
|
|
|
<!--
|
|
◆ The two that put a file right rather than taking it away, above the row DELETE is on and away
|
|
from it. Both open a form in place of these buttons — see the card above — because a phone has
|
|
nowhere to put a text field except where something else already was.
|
|
-->
|
|
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
|
<Button Grid.Column="0" Classes="secondary" Height="44" Content="RENAME"
|
|
Command="{Binding RenameRemoteCommand}" IsEnabled="{Binding CanRenameRemote}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="MOVE"
|
|
Command="{Binding MoveRemoteCommand}" IsEnabled="{Binding CanMoveRemote}" />
|
|
</Grid>
|
|
|
|
<!--
|
|
◆ PERMS beside DELETE rather than on the row above, which is full: it is the fourth thing that can
|
|
be done to a chosen row and the third that puts one right. Dark on a bucket, where an object has an
|
|
access policy rather than nine bits — see CanChangeRemotePermissions.
|
|
-->
|
|
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
|
<Button Grid.Column="0" Classes="secondary" Height="44" Content="PERMS"
|
|
Command="{Binding ChangeRemotePermissionsCommand}"
|
|
IsEnabled="{Binding CanChangeRemotePermissions}" />
|
|
<Button Grid.Column="2" Classes="secondary" Height="44" Content="DELETE"
|
|
Command="{Binding DeleteRemoteCommand}" IsEnabled="{Binding CanDeleteRemote}" />
|
|
</Grid>
|
|
|
|
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding !IsAskingAboutRemoteEntry}">
|
|
<Button Grid.Column="0" Classes="secondary" Height="44" Content="CLOSE"
|
|
Command="{Binding DisconnectCommand}" />
|
|
<Panel Grid.Column="2" />
|
|
</Grid>
|
|
|
|
<TextBlock Classes="body" IsVisible="{Binding !IsAskingAboutRemoteEntry}"
|
|
Text="ADD FILES picks documents to send. SAVE FILE asks where the selected file should be kept — the file is created there when you choose it, so a transfer that fails leaves it empty." />
|
|
|
|
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
|
Text="{Binding Status}" />
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|