Public Access
The authentication picker has listed saved credentials since they existed, but making one meant leaving a half-typed host for the keychain screen and coming back to find it gone. On the phone it was worse than a detour: that head has no credential editor at all, so it could bind a host to a credential and never produce one. + NEW CREDENTIAL opens a card under the picker — name, optional username, password, notes — and ADD writes it and binds the host in one step. A button beside the picker rather than an entry inside it. Every row of that list is a binding a host can have, and "make a new one" is an action: as an entry it would sit in the box afterwards describing a state no host can be in, and cancelling the form would leave the picker showing it. It carries its own five fields rather than reusing the keychain editor's, and that is the load-bearing part. IsEditingCredential is what AVaultEditorIsInTheWay asks about, so sharing it would have made the whole Vault screen refuse to open an editor while this card sat open on the Hosts screen, with a status line naming a form the user cannot see on a screen they are not looking at — the exact failure that guard was split in two to end. A test pins it. It writes to the keychain immediately, unlike every other field in this editor, because a credential is a shared item with an id and a host can only name an id that exists. The consequence is honest rather than hidden and the hint says so: a credential added this way outlives a cancelled host edit. What was still being typed does not — every path that closes the host editor clears the form, and one of those fields is a password. The binding is written before the reload rather than after it. RefreshOpenEditors rebuilds this picker and then restores it from the editor's own selection, so setting it first is what survives the pass, and by the time it is read ReloadCredentialsAsync has put the matching entry in the list to land on. A name already taken is duplicated, not reused, and that is a deliberate parting from the new-tag box six lines further down which offers the existing tag instead. Two tags called "staging" are one intention spelled twice; two credentials called "root" are two different passwords, and quietly binding the host to whichever was there already would authenticate it as an account nobody chose. A duplicate label in the picker is the smaller problem. Into editingHostVaultId, so the credential lands wherever the host is being sealed and everybody who can read the host can read what it authenticates with. Stricter than the tag path — which files into the active vault and is recorded as a gap in docs/design-import-gaps.md — and it can be, because this picker lists credentials from every readable vault rather than one. Five flow tests cover the bind-through-reload path, the cancel semantics on both the saved credential and the abandoned one, the cross-screen guard, the duplicate name and the empty-password refusal. The layout test is separate and necessary: the card is collapsed until somebody presses the button, so a harness driven by the default state draws none of it, and TheHostDrawerFitsWithTheHostEditorOpen would have gone on passing over a card that blew the column. 1,854 tests, none failing.
1241 lines
72 KiB
XML
1241 lines
72 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.HostsScreen"
|
||
x:DataType="vm:VaultViewModel"
|
||
Background="{StaticResource Canvas}">
|
||
|
||
<!--
|
||
Design 01 — HOSTS, the phone's home.
|
||
|
||
The desktop's sidebar and its right-hand connect column cannot both be on screen at 360dp, so the list
|
||
fills the screen and everything else is raised over it or put in its place.
|
||
|
||
Groups are rows in the same list rather than a nesting container, which is what SidebarRows already
|
||
hands both heads. The desktop shows group headings only when groups exist and so does this, for free.
|
||
|
||
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||
The rows became cards: rounded, inset from the edge, and filled when chosen rather than marked with a
|
||
strip.
|
||
|
||
── v3 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||
The design's floating + arrives, three versions after it was first asked for. It was refused twice on
|
||
the grounds that it would open an editor this head had not got — hosts were created on the desktop and
|
||
synced down — and that was true until the editors below existed. It opens a sheet rather than a form,
|
||
because "add" on this screen has been two operations since groups existed: a host, or a group to file
|
||
hosts under.
|
||
|
||
── v4: a tap connects, and a long press raises a bar. ───────────────────────────────────────────────
|
||
Choosing a row used to raise a connect bar over the bottom of the list — a password box, CONNECT, EDIT,
|
||
MOVE and DELETE, five controls in the way of the one thing a tap on a machine's name means. So the
|
||
gestures split: a tap connected, and a long press raised the bar.
|
||
|
||
── ◆ v5: THE BAR IS GONE, AND A LONG PRESS CHOOSES HOSTS. ───────────────────────────────────────────
|
||
The bar was the right idea in the wrong place. It sat over the bottom of the list, so it covered rows; it
|
||
had room for five things and never a sixth; and every one of them was about exactly one machine, which
|
||
left filing eleven imported hosts under a group as eleven trips through a form. What replaced it:
|
||
|
||
· a tap still connects, and raises nothing — see VaultViewModel.ConnectToRowAsync;
|
||
· a long press puts a tick against the host it landed on, and further taps tick and untick;
|
||
· the actions move into a bar across the top of the screen, in the vault header's place, carrying the
|
||
count, a pencil and a ⋯ — see HostActionBar.axaml and PhoneShell.ShowsHostSelectionBar;
|
||
· the seven things behind that ⋯ are the sheet at the foot of this file, and the three of them that
|
||
ask a question first raise a panel above the list rather than over it.
|
||
|
||
Two pieces of the old bar survive here, both because they had nowhere else to be. A host that
|
||
authenticates with a typed password cannot be reached by a tap alone, so that tap raises a sheet with the
|
||
box in it. And the editor, which used to be a card in the list's own row, is now a page over the whole
|
||
screen — the pencil in the action bar opens it, and PhoneShell stands its own chrome down for it.
|
||
-->
|
||
|
||
<!--
|
||
The Panel is what lets the sheets and the editor page cover the screen. The + does not need it — it is
|
||
layered inside the list's own Grid row, which is where it has to be so that a panel raised above the list
|
||
shortens it rather than being covered by it. See the button itself.
|
||
-->
|
||
<Panel>
|
||
|
||
<Grid RowDefinitions="Auto,Auto,*">
|
||
|
||
<!-- ============ filter ============ -->
|
||
<!--
|
||
The design's "Search or connect…" well. On the desktop this is Ctrl+K over a palette; on a phone the
|
||
list is already the palette, so it filters in place rather than opening a second surface.
|
||
|
||
It stays up while hosts are ticked, deliberately. Filtering does not clear the set — the ticks are
|
||
held as ids rather than as rows, see VaultViewModel.chosenHostIds — so searching for the second half
|
||
of a selection and ticking it is a thing this screen supports rather than merely survives.
|
||
-->
|
||
<Border Grid.Row="0" Margin="14,10,14,4" Background="{StaticResource Field}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="10" Height="44">
|
||
<Grid ColumnDefinitions="Auto,*">
|
||
<TextBlock Grid.Column="0" Text="⌕" Foreground="{StaticResource TextFaint}" FontSize="13"
|
||
Margin="13,0,0,0" VerticalAlignment="Center" />
|
||
<TextBox Grid.Column="1" Text="{Binding HostFilter}" PlaceholderText="Search hosts"
|
||
Padding="9,0" VerticalContentAlignment="Center"
|
||
Background="Transparent" BorderThickness="0"
|
||
Foreground="{StaticResource Text}" FontFamily="{StaticResource MonoFont}" FontSize="12" />
|
||
</Grid>
|
||
</Border>
|
||
|
||
<!-- ============ sync + conflicts ============ -->
|
||
<StackPanel Grid.Row="1" Margin="16,4,16,0" Spacing="7">
|
||
|
||
<TextBlock Classes="detail" Text="{Binding Status}" TextWrapping="Wrap" />
|
||
|
||
<!--
|
||
◆ The conflict log. The merge is only allowed to pick a winner because this exists, so it is a
|
||
banner the user has to dismiss rather than a screen they might never open.
|
||
-->
|
||
<Border IsVisible="{Binding HasConflicts}" Background="{StaticResource WarnWash}"
|
||
BorderBrush="{StaticResource WarnSoft}" BorderThickness="1" CornerRadius="12" Padding="13,11">
|
||
<StackPanel Spacing="9">
|
||
<ItemsControl ItemsSource="{Binding Conflicts}">
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:ConflictRowViewModel">
|
||
<TextBlock Classes="detail" Foreground="{StaticResource WarnText}" TextWrapping="Wrap"
|
||
Text="{Binding Summary}" Margin="0,2" />
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
<Button Classes="secondary" Height="40" Content="DISMISS ALL" FontSize="10"
|
||
Command="{Binding AcknowledgeAllConflictsCommand}" />
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<Border IsVisible="{Binding HasUnreadableItems}" Background="{StaticResource DangerWash}"
|
||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="12" Padding="13,11">
|
||
<TextBlock Classes="detail" Foreground="{StaticResource DangerText}" TextWrapping="Wrap"
|
||
Text="{Binding UnreadableSummary}" />
|
||
</Border>
|
||
|
||
<!-- ============ ◆ what the action bar's menu asked ============ -->
|
||
<!--
|
||
THREE PANELS, AT MOST ONE OF THEM UP, AND ALL THREE ABOVE THE LIST RATHER THAN OVER IT.
|
||
|
||
Same arrangement as the group panels below, and for the reason written there: a card on top of the
|
||
list hides the rows it is talking about, and those rows — the ticked ones — are the information the
|
||
question exists to give. The list shortens instead, which keeps the ticks in view while the question
|
||
is answered.
|
||
|
||
Which of the three is showing is decided in the view model rather than here, because each of them
|
||
disarms the other two on the way up. See MoveChosenHostsToVault, RegroupChosenHosts and
|
||
DeleteChosenHosts, all of which clear the other two before setting their own.
|
||
-->
|
||
|
||
<!--
|
||
◆ MOVING OR COPYING TO ANOTHER VAULT. One panel for both verbs, because they differ in one word and
|
||
in the sentence under the picker; two copies of this would be two places to fix the day a third
|
||
vault rule arrives. What varies is bound rather than duplicated — see
|
||
VaultViewModel.ChosenHostVaultPanelTitle and its two siblings.
|
||
|
||
The sentence is not decoration. Groups and tags are items of the vault being left, so neither can
|
||
come along: a host that arrived carrying either would point at something the destination does not
|
||
contain, and everybody but the person who moved it would see it filed under nothing. On a phone,
|
||
where the status line afterwards is one line at the bottom of a screen somebody has already
|
||
navigated away from, saying it before the tap is the only place it reliably gets read.
|
||
-->
|
||
<Border IsVisible="{Binding IsSendingChosenHostsToAVault}" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="12"
|
||
Padding="13,12">
|
||
<StackPanel Spacing="9">
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock Classes="label" Text="{Binding ChosenHostVaultPanelTitle}" />
|
||
<TextBlock Classes="mono" FontSize="11" VerticalAlignment="Center"
|
||
Text="{Binding ChosenHostsLabel}" />
|
||
</StackPanel>
|
||
|
||
<ComboBox HorizontalAlignment="Stretch" MinHeight="44"
|
||
ItemsSource="{Binding ChosenHostVaultChoices}"
|
||
SelectedItem="{Binding SelectedChosenHostVault}">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Display}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<TextBlock Classes="body" Text="{Binding ChosenHostVaultPanelNote}" />
|
||
|
||
<!--
|
||
◆ THE KEY. A binding resolves across vaults, so the host goes on working here whichever way this
|
||
is answered — but the vault it has just joined holds one key, and a host whose own key stayed
|
||
behind is one its new colleagues cannot connect with.
|
||
|
||
Drawn only for a move of exactly one host, which is the only shape the question has an answer in:
|
||
which key to carry is a fact about one machine, and a copy that took the key away would leave the
|
||
original unable to connect. See VaultViewModel.BringsTheChosenBindingAlong.
|
||
|
||
Unticked, because moving a key into a team's vault hands it to everybody who holds that vault's
|
||
key: a disclosure is chosen, never defaulted into. The line under it is the count of what else
|
||
uses that key, which is the difference between an obvious yes and an obvious no.
|
||
-->
|
||
<CheckBox IsChecked="{Binding BringsTheChosenBindingAlong}" MinHeight="44"
|
||
IsVisible="{Binding HasAChosenBindingToBring}">
|
||
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Text="{Binding ChosenBindingToBringQuestion}" />
|
||
</CheckBox>
|
||
<TextBlock Classes="body" TextWrapping="Wrap"
|
||
IsVisible="{Binding HasAChosenBindingToBring}"
|
||
Text="{Binding ChosenBindingToBringNote}" />
|
||
|
||
<Grid ColumnDefinitions="*,8,*">
|
||
<Button Grid.Column="0" Classes="primary" Height="44"
|
||
Content="{Binding ChosenHostVaultPanelVerb}"
|
||
Command="{Binding ConfirmSendChosenHostsToAVaultCommand}"
|
||
IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelSendChosenHostsToAVaultCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ FILING THE TICKED HOSTS UNDER A GROUP. The one action in the bar that has always existed with no
|
||
control on this head: it is the same write dragging a card onto a group makes on the desktop, and
|
||
there is no dragging here.
|
||
|
||
One keychain's groups, and a selection spanning two is refused before this panel is drawn at all —
|
||
a group belongs to one vault, so filing a host from another under it would leave everybody but the
|
||
person who did it seeing a machine filed under nothing. See VaultViewModel.RegroupChosenHosts.
|
||
-->
|
||
<Border IsVisible="{Binding IsRegroupingChosenHosts}" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="12"
|
||
Padding="13,12">
|
||
<StackPanel Spacing="9">
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock Classes="label" Text="CHANGE GROUP" />
|
||
<TextBlock Classes="mono" FontSize="11" VerticalAlignment="Center"
|
||
Text="{Binding ChosenHostsLabel}" />
|
||
</StackPanel>
|
||
|
||
<ComboBox HorizontalAlignment="Stretch" MinHeight="44"
|
||
ItemsSource="{Binding ChosenHostGroupChoices}"
|
||
SelectedItem="{Binding SelectedChosenHostGroup}">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:GroupChoice">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<TextBlock Classes="body"
|
||
Text="A group lends its port, its account and its key to every host filed under it that says nothing itself, so this can change what these machines dial. Nothing else about them moves." />
|
||
|
||
<Grid ColumnDefinitions="*,8,*">
|
||
<Button Grid.Column="0" Classes="primary" Height="44" Content="FILE"
|
||
Command="{Binding ConfirmRegroupChosenHostsCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelRegroupChosenHostsCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ THE DELETION QUESTION, naming a count rather than a machine. One question for the whole set is the
|
||
reason DeletionTarget.ChosenHosts exists: six copies of "delete prod-db?" is not a confirmation
|
||
anybody reads, and the number is the difference between a sentence somebody reads and one they press
|
||
past.
|
||
-->
|
||
<Border IsVisible="{Binding IsConfirmingChosenHostDeletion}" Background="{StaticResource DangerWash}"
|
||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="12" Padding="13,12">
|
||
<StackPanel Spacing="6">
|
||
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||
Text="{Binding PendingDeletion.Question}" />
|
||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||
Text="{Binding PendingDeletion.Consequence}" />
|
||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||
IsVisible="{Binding PendingDeletion.HasUsage}"
|
||
Text="{Binding PendingDeletion.Usage}" />
|
||
|
||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||
<Button Grid.Column="0" Classes="danger" Height="44" Content="DELETE"
|
||
Command="{Binding ConfirmDeleteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||
Command="{Binding CancelDeleteCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!-- ============ ◆ what the sheet raised about a group ============ -->
|
||
<!--
|
||
Both of these are about a heading in the list below, and both are drawn above it rather than over it
|
||
— which is what KEYCHAIN does with its own confirmation and for the reason written there: a card on
|
||
top of the list hides the row it is talking about, and the row is the information the question exists
|
||
to give. Neither replaces the list, because neither replaces the heading either.
|
||
|
||
At most one of the two is ever up. MoveGroup disarms a deletion and DeleteGroup folds the move away,
|
||
which is the same pairing the desktop's GROUPS section makes.
|
||
|
||
◆ THE DELETION QUESTION, and the tick under it is the whole reason it is a question rather than a
|
||
warning. Off is "the machines stay and turn up under UNGROUPED", which is recoverable; on is not, and
|
||
it is reset to off on every question — see VaultViewModel.DeletionTakesTheHostsToo. Below the count
|
||
rather than above it, so nobody answers it before reading how many.
|
||
-->
|
||
<Border IsVisible="{Binding IsConfirmingGroupDeletion}" Background="{StaticResource DangerWash}"
|
||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1" CornerRadius="12" Padding="13,12">
|
||
<StackPanel Spacing="6">
|
||
<TextBlock Classes="mono" FontSize="12" TextWrapping="Wrap"
|
||
Text="{Binding PendingDeletion.Question}" />
|
||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||
Text="{Binding PendingDeletion.Consequence}" />
|
||
<TextBlock Classes="body" Foreground="{StaticResource DangerText}"
|
||
IsVisible="{Binding PendingDeletion.HasUsage}"
|
||
Text="{Binding PendingDeletion.Usage}" />
|
||
|
||
<CheckBox MinHeight="44" IsVisible="{Binding PendingDeletion.HasChoice}"
|
||
IsChecked="{Binding DeletionTakesTheHostsToo}">
|
||
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Foreground="{StaticResource WarnText}" Text="{Binding PendingDeletion.Choice}" />
|
||
</CheckBox>
|
||
|
||
<Grid ColumnDefinitions="*,8,*" Margin="0,4,0,0">
|
||
<Button Grid.Column="0" Classes="danger" Height="44" Content="DELETE"
|
||
Command="{Binding ConfirmDeleteCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="KEEP" FontSize="10.5"
|
||
Command="{Binding CancelDeleteCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ MOVING THE GROUP TO ANOTHER VAULT. The host's panel one level up, and not drawn in the danger
|
||
colours beside it: what is being asked is which vault, and a move is undone by moving it back.
|
||
|
||
It names the group, which the desktop's copy of this panel does not have to — there the group's own
|
||
card is on screen underneath the picker, and here the panel is above a list the heading may have
|
||
scrolled out of. See VaultViewModel.MovingGroupLabel.
|
||
|
||
The sentence is not decoration. A group cannot go anywhere alone: the groups nested inside it and
|
||
every machine filed under any of them are items of the vault it is leaving, so all of them are
|
||
re-sealed under the destination's key. What cannot come is the group it was nested under, which is why
|
||
it arrives at the top level, and the hosts' tags stay behind with it. On a phone the status line
|
||
afterwards is one line at the bottom of a screen somebody has already navigated away from, so before
|
||
the tap is the only place this reliably gets read.
|
||
-->
|
||
<Border IsVisible="{Binding IsMovingGroup}" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" CornerRadius="12"
|
||
Padding="13,12">
|
||
<StackPanel Spacing="9">
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock Classes="label" Text="MOVE GROUP" />
|
||
<TextBlock Classes="mono" FontSize="11" Text="{Binding MovingGroupLabel}"
|
||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||
</StackPanel>
|
||
|
||
<ComboBox HorizontalAlignment="Stretch" MinHeight="44"
|
||
ItemsSource="{Binding MoveGroupVaultChoices}"
|
||
SelectedItem="{Binding SelectedMoveGroupVault}">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Display}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<TextBlock Classes="body"
|
||
Text="The group goes with the groups inside it and every host filed under them, all re-encrypted with the other vault's key. Any group it is nested under stays behind, so it arrives at the top level, and the hosts' tags stay behind with it." />
|
||
|
||
<Grid ColumnDefinitions="*,8,*">
|
||
<Button Grid.Column="0" Classes="primary" Height="44" Content="MOVE"
|
||
Command="{Binding ConfirmMoveGroupCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelMoveGroupCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
</StackPanel>
|
||
|
||
<!-- ============ the list ============ -->
|
||
<!--
|
||
◆ Named, because both gestures are attached from code-behind now: the long press only fires at all once
|
||
IsHoldingEnabled is on, and the tap has a branch in it that the markup cannot express — it connects, or
|
||
it ticks, depending on whether anything is ticked already. Setting the attached property and both
|
||
handlers in one place is what keeps the three from drifting apart. See HostsScreen.axaml.cs.
|
||
|
||
The row stays a plain Border rather than becoming a Button, for the reason FilesScreen writes out: a
|
||
button as the item template swallows the press before the list sees it, so neither handler would ever
|
||
run.
|
||
|
||
◆ THE SELECTION FILL IS GONE FROM THE ROW STYLE, and that is the change rather than an omission. A tap
|
||
connects and a long press ticks, so nothing on this screen means "selected" any more — a lit row would
|
||
be a mark with no action behind it, sitting beside ticks that have one. What the list still keeps is
|
||
SelectedItem, because the shared view model reads it: a tap assigns SelectedHost on the way to
|
||
connecting, and the password sheet is about that host.
|
||
-->
|
||
<ListBox x:Name="Rows" Grid.Row="2" Margin="0,6,0,0" IsVisible="{Binding ShowsHostList}"
|
||
ItemsSource="{Binding SidebarRows}"
|
||
SelectedItem="{Binding SelectedSidebarRow}"
|
||
Tapped="OnRowTapped"
|
||
Background="Transparent" BorderThickness="0">
|
||
|
||
<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="12" />
|
||
</Style>
|
||
|
||
<!--
|
||
◆ The ticked row, filled. On a Border inside the item rather than on the item's own selected state,
|
||
because "ticked" is a property of the host and the list has no idea about it — the fill and the
|
||
tick at the right-hand end are the same fact drawn twice, which is deliberate: a fill alone is
|
||
what selection used to look like here, and a tick alone is a 14-pixel mark at the far edge of a
|
||
360dp row.
|
||
-->
|
||
<Style Selector="Border.hostrow">
|
||
<Setter Property="Background" Value="Transparent" />
|
||
<Setter Property="CornerRadius" Value="12" />
|
||
</Style>
|
||
<Style Selector="Border.hostrow.chosen">
|
||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||
</Style>
|
||
</ListBox.Styles>
|
||
|
||
<ListBox.DataTemplates>
|
||
|
||
<!--
|
||
A group heading. SidebarGroupHeader, not HostGroupRowViewModel — the latter is a row in the
|
||
groups editor, and the two are deliberately different shapes: this one carries the count *after*
|
||
the filter, which is what a heading over a filtered list has to say.
|
||
-->
|
||
<DataTemplate DataType="vm:SidebarGroupHeader">
|
||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" Margin="8,12,8,5">
|
||
<TextBlock Grid.Column="0" Classes="detail" Text="{Binding Chevron}" VerticalAlignment="Center" />
|
||
|
||
<!--
|
||
The name, and the vault it is in where this session holds more than one. The badge is what
|
||
makes one heading per group survive a shared vault: two vaults may each hold a "production",
|
||
and this list has no nesting to tell the two apart with. Drawn as the same outline tag a host
|
||
row wears, so "which vault" looks the same wherever it is answered.
|
||
-->
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" Margin="7,0,0,0">
|
||
<TextBlock Classes="section" Text="{Binding Label}" TextTrimming="CharacterEllipsis" />
|
||
<Border Classes="tag outline" VerticalAlignment="Center"
|
||
IsVisible="{Binding HasVaultBadge}">
|
||
<TextBlock Text="{Binding VaultBadge}" />
|
||
</Border>
|
||
</StackPanel>
|
||
|
||
<TextBlock Grid.Column="2" Classes="detail" Text="{Binding Count}" FontSize="9"
|
||
VerticalAlignment="Center" />
|
||
|
||
<!--
|
||
◆ The phone's only way to do anything to a group. The desktop reaches all three from a menu on
|
||
the group's own card, which this head does not draw, and a heading's own selection deliberately
|
||
bounces back to the host — so without this a group could be created here and never corrected,
|
||
moved or removed. It matters more for a group than for a host: its defaults are what every host
|
||
beneath it falls back to, and it is the one thing on this screen that can take hosts with it.
|
||
|
||
It was the pencil alone, and became a menu when there were three things behind it rather than
|
||
one. Three icons after a chevron, a name, a vault badge and a count is what would be left of the
|
||
name at 360dp; see VaultViewModel.OpenGroupSheet.
|
||
|
||
Hidden on the ungrouped heading, which has no group behind it to act on.
|
||
-->
|
||
<Button Grid.Column="3" Classes="icon" FontSize="15" Content="⋯" Margin="4,0,0,0"
|
||
IsVisible="{Binding GroupId, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||
Command="{Binding $parent[views:HostsScreen].((vm:VaultViewModel)DataContext).OpenGroupSheetCommand}"
|
||
CommandParameter="{Binding}" />
|
||
</Grid>
|
||
</DataTemplate>
|
||
|
||
<!-- A host. -->
|
||
<DataTemplate DataType="vm:HostRowViewModel">
|
||
<Border Classes="hostrow" Classes.chosen="{Binding IsChosen}">
|
||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" MinHeight="56" Margin="14,12">
|
||
|
||
<!-- Green means a terminal is open on this host right now. See the .dot style. -->
|
||
<Ellipse Grid.Column="0" Classes="dot" Classes.live="{Binding IsConnected}" Width="8" Height="8"
|
||
VerticalAlignment="Center" />
|
||
|
||
<StackPanel Grid.Column="1" Margin="12,0,0,0" Spacing="3" VerticalAlignment="Center">
|
||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="14" FontWeight="SemiBold"
|
||
TextTrimming="CharacterEllipsis" />
|
||
<Border Classes="tag outline" IsVisible="{Binding HasVaultBadge}">
|
||
<TextBlock Text="{Binding VaultBadge}" />
|
||
</Border>
|
||
<Border Classes="tag outline" BorderBrush="{StaticResource WarnSoft}"
|
||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||
<TextBlock Foreground="{StaticResource Warn}" Text="{Binding Badge}" />
|
||
</Border>
|
||
</StackPanel>
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Address}"
|
||
TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Classes="detail" FontSize="10.5" Text="·" />
|
||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Authentication}" />
|
||
</StackPanel>
|
||
|
||
<!--
|
||
The design's tag chips, and the row is where they were always meant to be — what a machine
|
||
*is*, under what it is called and where it answers. Wrapped rather than trimmed: a host
|
||
wearing four tags on a 360dp row takes a second line, which is the honest shape.
|
||
-->
|
||
<ItemsControl ItemsSource="{Binding TagLabels}" IsVisible="{Binding HasTags}"
|
||
Margin="0,2,0,0">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="x:String">
|
||
<Border Classes="tag" Margin="0,0,4,3">
|
||
<TextBlock Text="{Binding}" />
|
||
</Border>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
</StackPanel>
|
||
|
||
<!--
|
||
The design's "live" tag, on exactly the hosts the dot is green for — and in the same green,
|
||
overriding the tag style's accent text. A status word in the accent colour beside a green dot
|
||
meaning the same thing would be one fact drawn in two colours.
|
||
-->
|
||
<Border Grid.Column="2" Classes="tag" IsVisible="{Binding IsConnected}"
|
||
Background="{StaticResource LiveWash}">
|
||
<TextBlock Text="live" Foreground="{StaticResource Live}" />
|
||
</Border>
|
||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||
VerticalAlignment="Center" IsVisible="{Binding !IsConnected}" />
|
||
|
||
<!--
|
||
◆ The tick, in a column of its own at the far end rather than in place of the dot or the
|
||
chevron. Both of those say something a tick does not — whether a shell is open on this
|
||
machine, and that the row leads somewhere — and swapping either for the tick would make
|
||
choosing a host hide a fact about it. An Auto column takes no width while the mark is
|
||
collapsed, so a list with nothing ticked is laid out exactly as it was.
|
||
-->
|
||
<TextBlock Grid.Column="3" Text="✓" Margin="10,0,0,0" FontSize="15"
|
||
Foreground="{StaticResource AccentText}" VerticalAlignment="Center"
|
||
IsVisible="{Binding IsChosen}" />
|
||
</Grid>
|
||
</Border>
|
||
</DataTemplate>
|
||
|
||
</ListBox.DataTemplates>
|
||
</ListBox>
|
||
|
||
<!-- ============ the group editor ============ -->
|
||
<!--
|
||
In the list's row and swapped for it, following SnippetsScreen. A group is enough of a thing to need
|
||
one: a name, where it sits in the tree, and the three things hosts inside it fall back to. Every one of
|
||
those may be left empty, and empty means the group lends nothing rather than that it has not been
|
||
filled in.
|
||
|
||
It stays a card in the list's row where the host editor became a page, and the difference is honest
|
||
rather than an oversight: this form is seven controls and that one is twenty, and the host editor is
|
||
the one the action bar's pencil opens — a control whose whole promise is that it leads somewhere with
|
||
everything on it.
|
||
|
||
The parent picker leaves out this group and everything beneath it, so a cycle cannot be made here. That
|
||
is a courtesy rather than the guarantee — one assembled from two offline re-parents was never offered
|
||
this list, which is why the walk itself stops at a repeat.
|
||
-->
|
||
<Border Grid.Row="2" Classes="card" Margin="12,0" IsVisible="{Binding IsEditingGroup}"
|
||
VerticalAlignment="Top">
|
||
<ScrollViewer MaxHeight="560">
|
||
<StackPanel Spacing="10">
|
||
<TextBlock Classes="label" Text="GROUP" />
|
||
|
||
<TextBox Classes="field" Text="{Binding GroupEditorLabel}" PlaceholderText="group name" />
|
||
|
||
<!--
|
||
◆ WHICH VAULT THIS GROUP WILL LIVE IN, on the same terms as the host editor's picker and for the
|
||
same reason: it is what makes the group shared, and it cannot be changed afterwards. Above INSIDE
|
||
because it decides what INSIDE can offer — a parent belongs to one vault, and one from another is
|
||
a level half the readers cannot resolve.
|
||
-->
|
||
<StackPanel Spacing="6" IsVisible="{Binding ShowsGroupEditorVaultChoice}">
|
||
<TextBlock Classes="label" Text="VAULT" Margin="0,4,0,0" />
|
||
<ComboBox ItemsSource="{Binding GroupEditorVaultChoices}"
|
||
SelectedItem="{Binding GroupEditorSelectedVault}"
|
||
HorizontalAlignment="Stretch" MinHeight="44">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Display}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
<TextBlock Classes="body"
|
||
Text="A group in a shared vault is visible to everybody holding that vault's key, and only hosts in the same vault can be filed under it." />
|
||
</StackPanel>
|
||
|
||
<TextBlock Classes="label" Text="INSIDE" Margin="0,4,0,0" />
|
||
<ComboBox ItemsSource="{Binding GroupEditorParentChoices}"
|
||
SelectedItem="{Binding GroupEditorSelectedParent}"
|
||
HorizontalAlignment="Stretch" MinHeight="44">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:GroupChoice">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<TextBlock Classes="label" Text="HOSTS INSIDE FALL BACK TO" Margin="0,6,0,0" />
|
||
|
||
<Grid ColumnDefinitions="*,8,2*">
|
||
<NumericUpDown Grid.Column="0" Value="{Binding GroupEditorDefaultPort}" Minimum="1"
|
||
Maximum="65535" FormatString="0" ShowButtonSpinner="False" MinHeight="44"
|
||
PlaceholderText="no default" />
|
||
<TextBox Grid.Column="2" Classes="field" Text="{Binding GroupEditorDefaultUsername}"
|
||
PlaceholderText="no default user" />
|
||
</Grid>
|
||
|
||
<ComboBox ItemsSource="{Binding GroupEditorAuthenticationChoices}"
|
||
SelectedItem="{Binding GroupEditorSelectedAuthentication}"
|
||
HorizontalAlignment="Stretch" MinHeight="44">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||
<TextBlock Classes="detail" FontSize="10" VerticalAlignment="Center"
|
||
Text="{Binding Qualifier}"
|
||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<TextBlock Classes="body"
|
||
Text="A host inside this group uses these wherever it says nothing itself. They are read at connect time rather than copied in, so changing one here changes every host that never overrode it." />
|
||
|
||
<Grid ColumnDefinitions="*,8,*">
|
||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||
Command="{Binding SaveGroupCommand}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelGroupEditCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
</Border>
|
||
|
||
<!-- ============ add ============ -->
|
||
<!--
|
||
◆ The design's floating +, and the reason every version of this file until v3 said it could not be
|
||
drawn.
|
||
|
||
<b>Grid.Row 2, inside the Grid — not the Panel around it.</b> A Panel child aligned to the bottom is
|
||
anchored to the bottom of the screen and would sit over whatever the list's own row holds. In the
|
||
list's row it rides with the list, which is what a floating button does on every other Android
|
||
application and what a thumb reaches for.
|
||
|
||
Hidden rather than disabled while anything is open over the list, and hidden while hosts are ticked. A
|
||
+ that opens an editor on top of an open editor is a control with nothing to do; a + offering to add a
|
||
twelfth machine while eleven are ticked for deletion is worse, because it does something and the
|
||
something is not what the screen is about.
|
||
-->
|
||
<Button Grid.Row="2" Classes="fab" Content="+" Margin="0,0,18,18"
|
||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||
IsVisible="{Binding ShowsAddButton}"
|
||
Command="{Binding OpenAddSheetCommand}" />
|
||
|
||
</Grid>
|
||
|
||
<!-- ============ the add sheet ============ -->
|
||
<!--
|
||
Follows HostKeySheet, this head's one existing sheet: a scrim, a bottom-aligned panel with the top two
|
||
corners rounded, and a grab handle that is decoration. Two things it does differently, both because it
|
||
is a menu rather than a decision.
|
||
|
||
It is dismissible. Tapping the scrim closes it, which HostKeySheet deliberately refuses — answering
|
||
"do you trust this key" by tapping beside it is the swipe-to-dismiss that head was built to avoid.
|
||
"Which kind of thing are you adding" has no wrong answer and no answer at all is one of them.
|
||
|
||
And it lives here rather than in PhoneShell. HostKeySheet sits up there because the files screen raises
|
||
it too; nothing but this screen raises this. The cost is that the scrim covers the screen and not the
|
||
64px nav bar underneath, so the four destinations stay lit and tappable — which for a menu is right:
|
||
navigating away is a perfectly good way to decide not to add anything.
|
||
-->
|
||
<Panel IsVisible="{Binding IsAddSheetOpen}">
|
||
|
||
<!--
|
||
Declared before the sheet so the sheet draws over it: a Panel stacks its children in declaration
|
||
order. See the .scrim style for why a tap on it must not light anything up.
|
||
-->
|
||
<Button Classes="scrim" Command="{Binding CloseAddSheetCommand}" />
|
||
|
||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||
<StackPanel Spacing="0">
|
||
|
||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||
HorizontalAlignment="Center" Margin="0,0,0,16" />
|
||
|
||
<TextBlock Classes="title" Text="ADD" FontSize="13" />
|
||
|
||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding NewHostCommand}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="New host" />
|
||
<TextBlock Classes="detail" FontSize="10.5"
|
||
Text="A machine to connect to, filed under a group if you want one." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Button Classes="row" Command="{Binding NewGroupCommand}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="New group" />
|
||
<TextBlock Classes="detail" FontSize="10.5"
|
||
Text="A heading for hosts, and the port, user and key they fall back to." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Button Classes="secondary" Content="CANCEL" Margin="0,12,0,0"
|
||
Command="{Binding CloseAddSheetCommand}" />
|
||
|
||
</StackPanel>
|
||
</Border>
|
||
</Panel>
|
||
|
||
<!-- ============ ◆ the host action sheet ============ -->
|
||
<!--
|
||
THE SEVEN THINGS BEHIND THE ⋯ IN THE ACTION BAR, and the whole of what the connect card used to be
|
||
plus three things it could not express.
|
||
|
||
A sheet rather than a flyout hanging off the button that raised it. That button is in the top-right
|
||
corner of a 360dp screen and the hand holding the phone is at the bottom of it; a flyout would put
|
||
seven entries where the thumb is not, and would have no room for the sentence three of them need. This
|
||
is the add sheet's shape doing the same job the group sheet already does.
|
||
|
||
Dismissible, on the add sheet's terms rather than the host key sheet's: "which of these seven" has no
|
||
wrong answer, and none of them is a perfectly good outcome.
|
||
|
||
── which entries are drawn ─────────────────────────────────────────────────────────────────────────
|
||
The first two are collapsed unless exactly one host is ticked. A terminal and a file-transfer session
|
||
are each a connection somebody is about to look at, and six of either is five nobody asked for — see
|
||
VaultViewModel.HasOneChosenHost. The remaining four act on the whole set and read better for it: it is
|
||
the reason the set exists.
|
||
|
||
Delete is below a rule, which is the same separator the group sheet draws and means the same thing: a
|
||
phone has no hover and no tooltip, so where the thumb lands is the only thing between a destructive
|
||
entry and an ordinary one.
|
||
-->
|
||
<Panel IsVisible="{Binding IsHostActionSheetOpen}">
|
||
|
||
<Button Classes="scrim" Command="{Binding CloseHostActionSheetCommand}" />
|
||
|
||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||
<StackPanel Spacing="0">
|
||
|
||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||
HorizontalAlignment="Center" Margin="0,0,0,16" />
|
||
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock Classes="title" Text="HOSTS" FontSize="13" />
|
||
<TextBlock Classes="mono" FontSize="12" VerticalAlignment="Center"
|
||
Text="{Binding ChosenHostsLabel}" />
|
||
</StackPanel>
|
||
|
||
<!--
|
||
Scrolled rather than trusted to fit. Seven entries, three of them two lines, is taller than a
|
||
small phone in landscape — and a sheet whose last entry is the destructive one must not be a
|
||
sheet whose last entry falls off the bottom.
|
||
-->
|
||
<ScrollViewer MaxHeight="440" Margin="0,10,0,0">
|
||
<StackPanel Spacing="0">
|
||
|
||
<Button Classes="row" IsVisible="{Binding HasOneChosenHost}"
|
||
Command="{Binding ConnectToChosenHostCommand}">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||
Text="Connect" />
|
||
</Button>
|
||
|
||
<Button Classes="row" IsVisible="{Binding HasOneChosenHost}"
|
||
Command="{Binding BrowseChosenHostCommand}">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||
Text="Connect via SFTP" />
|
||
</Button>
|
||
|
||
<Button Classes="row" Command="{Binding MoveChosenHostsToVaultCommand}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Move to vault" />
|
||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||
Text="Re-encrypted with the other vault's key. Groups and tags stay behind." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Button Classes="row" Command="{Binding CopyChosenHostsToVaultCommand}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Copy to vault" />
|
||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||
Text="A second copy over there, and these stay where they are." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Button Classes="row" Command="{Binding RegroupChosenHostsCommand}">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||
Text="Change group" />
|
||
</Button>
|
||
|
||
<Button Classes="row" Command="{Binding DuplicateChosenHostsCommand}">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" VerticalAlignment="Center"
|
||
Text="Duplicate" />
|
||
</Button>
|
||
|
||
<Border Height="1" Background="{StaticResource Border}" Margin="0,8" />
|
||
|
||
<Button Classes="row" Command="{Binding DeleteChosenHostsCommand}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||
Foreground="{StaticResource Danger}" Text="Remove" />
|
||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||
Text="Asks first, and says how many. There is no undo." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
|
||
<Button Classes="secondary" Content="CANCEL" Margin="0,12,0,0"
|
||
Command="{Binding CloseHostActionSheetCommand}" />
|
||
|
||
</StackPanel>
|
||
</Border>
|
||
</Panel>
|
||
|
||
<!-- ============ ◆ the password sheet ============ -->
|
||
<!--
|
||
WHAT IS LEFT OF THE CONNECT CARD, and the only part of it that had nowhere else to go.
|
||
|
||
A host that authenticates with a typed password cannot be reached by a tap alone: there is nothing on
|
||
the row to type into. So the tap that cannot finish raises this, rather than doing nothing or — the one
|
||
outcome that must never happen — dialling with no password. See VaultViewModel.ConnectToRowAsync, where
|
||
the branch lives, and IsAskingForConnectPassword, which is this sheet.
|
||
|
||
The difference from the bar it replaces is that this is up only while a question is on screen. The bar
|
||
was raised by a long press and stayed, so it was a password box sitting over the list whether or not
|
||
anything was being asked.
|
||
|
||
Dismissible, and dismissing empties the box: a password left behind would be somebody else's secret in
|
||
the box the next tap reads, and it would satisfy the length check that decides whether to raise this at
|
||
all. See CancelConnectPassword.
|
||
-->
|
||
<Panel IsVisible="{Binding IsAskingForConnectPassword}">
|
||
|
||
<Button Classes="scrim" Command="{Binding CancelConnectPasswordCommand}" />
|
||
|
||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||
<StackPanel Spacing="10">
|
||
|
||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||
HorizontalAlignment="Center" Margin="0,0,0,6" />
|
||
|
||
<!--
|
||
It names the host, as the bar did and for the same reason: the row it was tapped on is behind a
|
||
sheet and a scrim, and "which machine is this password for" is the one thing somebody about to
|
||
type a secret has to be sure of.
|
||
-->
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock Classes="label" Text="CONNECT TO" />
|
||
<TextBlock Classes="mono" FontSize="11" VerticalAlignment="Center"
|
||
Text="{Binding SelectedHost.Label}" TextTrimming="CharacterEllipsis" />
|
||
</StackPanel>
|
||
|
||
<TextBox Classes="field secret" Text="{Binding ConnectPassword}" PlaceholderText="password">
|
||
<TextBox.KeyBindings>
|
||
<KeyBinding Gesture="Enter" Command="{Binding ConnectCommand}" />
|
||
</TextBox.KeyBindings>
|
||
</TextBox>
|
||
|
||
<!--
|
||
The tick is the phone's whole answer to storing a password, and on this head it is the only one:
|
||
the keychain lists credentials here but has no editor to create one in, so before it a password
|
||
typed on a phone could only ever be typed again. What it produces is an ordinary credential bound
|
||
to this host — see VaultViewModel.RemembersConnectPassword — rather than a fourth place a secret
|
||
quietly lives.
|
||
-->
|
||
<CheckBox IsChecked="{Binding RemembersConnectPassword}" MinHeight="44">
|
||
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||
Text="Remember this password for this host" />
|
||
</CheckBox>
|
||
|
||
<Grid ColumnDefinitions="*,8,*">
|
||
<Button Grid.Column="0" Classes="primary" Height="44" Content="CONNECT"
|
||
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelConnectPasswordCommand}" />
|
||
</Grid>
|
||
|
||
</StackPanel>
|
||
</Border>
|
||
</Panel>
|
||
|
||
<!-- ============ ◆ the group sheet ============ -->
|
||
<!--
|
||
The three things that can be done to a group, raised from the ⋯ on its heading. The desktop draws the
|
||
same three as a right-click menu on the group's card; this head has no card and no right-click, so it
|
||
is the add sheet's shape doing the same job — a scrim, a bottom panel with the top two corners rounded,
|
||
and a grab handle that is decoration.
|
||
|
||
Dismissible, on the add sheet's terms and not the host key sheet's: "which of these three" has no wrong
|
||
answer and none of them is a perfectly good outcome.
|
||
|
||
It names the group in its own title, which the desktop's menu does not have to — a right-click menu
|
||
opens over the card it is about, and this opens over a list the heading may have scrolled out of. Two
|
||
vaults may each hold a "production", so the badge on the heading is the other half of that answer.
|
||
|
||
◆ Delete is below a rule and Move is above it, which is the desktop's own order and its own separator:
|
||
moving is not a field of the group and it is not a deletion either. The two vaults are encrypted under
|
||
different keys, so a move is a re-seal of the whole shelf into one and a tombstone per item in the
|
||
other — the entry says so rather than reading as a filing gesture.
|
||
|
||
Open is the entry the desktop has and this does not. Its grid holds one level of the tree at a time and
|
||
this list holds all of it flattened, so there is nothing here to open a group into.
|
||
-->
|
||
<Panel IsVisible="{Binding GroupSheet, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||
|
||
<Button Classes="scrim" Command="{Binding CloseGroupSheetCommand}" />
|
||
|
||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||
<StackPanel Spacing="0">
|
||
|
||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||
HorizontalAlignment="Center" Margin="0,0,0,16" />
|
||
|
||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||
<TextBlock Classes="title" Text="GROUP" FontSize="13" />
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding GroupSheetLabel}"
|
||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||
</StackPanel>
|
||
|
||
<Button Classes="row" Margin="0,10,0,0"
|
||
Command="{Binding EditGroupFromHeadingCommand}"
|
||
CommandParameter="{Binding GroupSheet}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Edit" />
|
||
<TextBlock Classes="detail" FontSize="10.5"
|
||
Text="Its name, where it sits, and the port, user and key its hosts fall back to." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Button Classes="row"
|
||
Command="{Binding MoveGroupFromHeadingCommand}"
|
||
CommandParameter="{Binding GroupSheet}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||
Text="Move to another vault" />
|
||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||
Text="Takes the groups inside it and every host filed under them, re-encrypted with the other vault's key." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Border Height="1" Background="{StaticResource Border}" Margin="0,8" />
|
||
|
||
<Button Classes="row"
|
||
Command="{Binding DeleteGroupFromHeadingCommand}"
|
||
CommandParameter="{Binding GroupSheet}">
|
||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold"
|
||
Foreground="{StaticResource Danger}" Text="Delete" />
|
||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||
Text="Asks first, and asks separately whether the hosts filed under it go too." />
|
||
</StackPanel>
|
||
</Button>
|
||
|
||
<Button Classes="secondary" Content="CANCEL" Margin="0,12,0,0"
|
||
Command="{Binding CloseGroupSheetCommand}" />
|
||
|
||
</StackPanel>
|
||
</Border>
|
||
</Panel>
|
||
|
||
<!-- ============ ◆ the host editor, as a page ============ -->
|
||
<!--
|
||
A PAGE OVER THE WHOLE SCREEN, WHICH IS THE OTHER HALF OF THE PENCIL MOVING INTO THE ACTION BAR.
|
||
|
||
It was a card in the list's own row, under the search box and the sync line — a form with twenty
|
||
controls in it sharing a screen with two rows of chrome about the list it had replaced. Declared last
|
||
in this Panel so it draws over everything, opaque so nothing shows through, and PhoneShell stands its
|
||
own four rows down while it is up: no vault header, no shells strip, no bottom bar, no rail. See
|
||
PhoneShell.RefreshChrome.
|
||
|
||
Its header carries what the surrounding chrome no longer can: the way back, what this is, and SAVE. The
|
||
arrow cancels rather than saving, which is what CANCEL below the form does and what the system back
|
||
gesture does — three ways out with one meaning between them.
|
||
|
||
Every field is the shared view model's — the same properties the desktop's drawer binds — so the two
|
||
heads cannot drift about what a host is or what saving one does.
|
||
|
||
◆ The port and username boxes may be left empty, and empty is not "unset": it means the host takes its
|
||
group's. The watermark on each is what it will actually get, and it follows the group picker below as
|
||
that moves, so the form says what leaving a box blank will do rather than making it a guess. See
|
||
HostInheritance.
|
||
-->
|
||
<Grid IsVisible="{Binding IsEditing}" RowDefinitions="Auto,*"
|
||
Background="{StaticResource Canvas}">
|
||
|
||
<!-- DeepChrome rather than Chrome, since v5: "its header carries what the surrounding chrome no longer
|
||
can" below is still true, and now the header itself carries the darker chrome the rest of this
|
||
head's frame does — see the remark on PhoneShell's vault header. -->
|
||
<Border Grid.Row="0" Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
|
||
BorderThickness="0,0,0,1" Padding="6,0" Height="56">
|
||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||
<Button Grid.Column="0" Classes="icon" Content="←" Command="{Binding CancelEditCommand}"
|
||
ToolTip.Tip="Leave without saving" />
|
||
<TextBlock Grid.Column="1" Classes="heading" FontSize="16" Margin="4,0"
|
||
VerticalAlignment="Center" Text="{Binding DrawerTitle}"
|
||
TextTrimming="CharacterEllipsis" />
|
||
<Button Grid.Column="2" Classes="icon accent" Content="✓" FontSize="17"
|
||
Command="{Binding SaveHostCommand}" ToolTip.Tip="Save this host" />
|
||
</Grid>
|
||
</Border>
|
||
|
||
<ScrollViewer Grid.Row="1">
|
||
<StackPanel Spacing="10" Margin="16,14,16,24">
|
||
<TextBlock Classes="label" Text="HOST" />
|
||
|
||
<TextBox Classes="field" Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||
<TextBox Classes="field" Text="{Binding EditorHostname}" PlaceholderText="hostname or address" />
|
||
|
||
<Grid ColumnDefinitions="*,8,2*">
|
||
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
|
||
FormatString="0" ShowButtonSpinner="False" MinHeight="44"
|
||
PlaceholderText="{Binding EditorPortPlaceholder}" />
|
||
<TextBox Grid.Column="2" Classes="field" Text="{Binding EditorUsername}"
|
||
PlaceholderText="{Binding EditorUsernamePlaceholder}" />
|
||
</Grid>
|
||
|
||
<TextBox Classes="field" Text="{Binding EditorNotes}" PlaceholderText="notes" />
|
||
|
||
<!--
|
||
One picker for all of them, as on the desktop, which is what makes "a key or a credential, never
|
||
both" impossible to express rather than merely invalid. The qualifier beside each label matters
|
||
for the same reason it does there: a key called deploy and the deploy account's password are the
|
||
ordinary case, and bare labels would offer two identical-looking entries.
|
||
-->
|
||
<TextBlock Classes="label" Text="AUTHENTICATION" Margin="0,4,0,0" />
|
||
<ComboBox ItemsSource="{Binding EditorAuthenticationChoices}"
|
||
SelectedItem="{Binding EditorSelectedAuthentication}"
|
||
HorizontalAlignment="Stretch" MinHeight="44">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||
<TextBlock Classes="detail" FontSize="10" VerticalAlignment="Center"
|
||
Text="{Binding Qualifier}"
|
||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||
</StackPanel>
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<!--
|
||
Making a credential without leaving the host, as on the desktop and on the same reasoning: the
|
||
moment one is wanted is while deciding how a host authenticates, and this head has no keychain
|
||
editor for credentials at all — so without this a phone could bind a host to a credential but
|
||
never make one. Writes to the keychain the instant ADD is pressed, exactly as the new-tag box
|
||
below does and for the same reason: a host can only name an id that exists.
|
||
-->
|
||
<Button Classes="secondary" Content="+ NEW CREDENTIAL" HorizontalAlignment="Left"
|
||
MinHeight="40" Padding="14,0"
|
||
IsVisible="{Binding !IsAddingEditorCredential}"
|
||
Command="{Binding BeginEditorCredentialCommand}" />
|
||
|
||
<Border CornerRadius="12" Background="{StaticResource Field}"
|
||
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="12"
|
||
IsVisible="{Binding IsAddingEditorCredential}">
|
||
<StackPanel Spacing="8">
|
||
<TextBlock Classes="label" Text="NEW CREDENTIAL" />
|
||
<TextBox Classes="field" Text="{Binding EditorNewCredentialLabel}"
|
||
PlaceholderText="name" />
|
||
<!--
|
||
Optional, and what makes a credential its own item: one account on twenty machines is
|
||
rotated in one place. Left blank, this host's own username is used.
|
||
-->
|
||
<TextBox Classes="field" Text="{Binding EditorNewCredentialUsername}"
|
||
PlaceholderText="username (blank: this host's own)" />
|
||
<TextBox Classes="field secret" Text="{Binding EditorNewCredentialPassword}"
|
||
PlaceholderText="password" />
|
||
<TextBox Classes="field" Text="{Binding EditorNewCredentialNotes}"
|
||
PlaceholderText="notes" />
|
||
<TextBlock Classes="detail" TextWrapping="Wrap"
|
||
Text="Added to the keychain as soon as you press ADD, so it stays even if you leave this host without saving." />
|
||
<Grid ColumnDefinitions="*,8,*">
|
||
<Button Grid.Column="0" Classes="primary" Content="ADD" MinHeight="44"
|
||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||
Command="{Binding AddEditorCredentialCommand}" />
|
||
<Button Grid.Column="2" Classes="secondary" Content="CANCEL" MinHeight="44"
|
||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||
Command="{Binding CancelEditorCredentialCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<!--
|
||
◆ WHICH VAULT THIS HOST WILL LIVE IN. Drawn only while adding and only where there is more than
|
||
one vault that can be written to, exactly as on the desktop — an existing host's vault is not a
|
||
field of this form, because the two are encrypted under different keys and changing it is a
|
||
re-seal into one vault and a tombstone in the other. That is "Move to vault" in the action bar's
|
||
menu, and it is separate so that it cannot happen as a side effect of saving something else.
|
||
Above GROUP rather than below it because it decides what GROUP can offer: a group is an item in
|
||
one vault, so choosing a vault refills that list with that vault's groups.
|
||
-->
|
||
<StackPanel Spacing="6" IsVisible="{Binding ShowsEditorVaultChoice}">
|
||
<TextBlock Classes="label" Text="VAULT" Margin="0,4,0,0" />
|
||
<ComboBox ItemsSource="{Binding EditorVaultChoices}"
|
||
SelectedItem="{Binding EditorSelectedVault}"
|
||
HorizontalAlignment="Stretch" MinHeight="44">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Display}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
<TextBlock Classes="body"
|
||
Text="A host in a shared vault is readable by everybody holding that vault's key. It can be moved out later, from the action bar's menu — what it cannot do is become unreadable to somebody who has already synced it." />
|
||
</StackPanel>
|
||
|
||
<TextBlock Classes="label" Text="GROUP" Margin="0,4,0,0" />
|
||
<ComboBox ItemsSource="{Binding EditorGroupChoices}"
|
||
SelectedItem="{Binding EditorSelectedGroup}"
|
||
HorizontalAlignment="Stretch" MinHeight="44">
|
||
<ComboBox.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:GroupChoice">
|
||
<TextBlock Classes="mono" FontSize="12" Text="{Binding Label}" />
|
||
</DataTemplate>
|
||
</ComboBox.ItemTemplate>
|
||
</ComboBox>
|
||
|
||
<!--
|
||
◆ The tags this host wears. Chips that toggle, matching the chips on the row behind this page —
|
||
a list of names to tick would make the user match an entry to a chip they can see.
|
||
|
||
The box under them creates one and puts it on straight away, which is where a tag is usually
|
||
wanted: while tagging a host and finding it does not exist yet. It writes to the keychain
|
||
immediately, unlike every other field here, because a host can only name a tag that has an id —
|
||
so cancelling this editor leaves the tag behind. Renaming and deleting are on the keychain
|
||
screen, with every other item kind.
|
||
-->
|
||
<TextBlock Classes="label" Text="TAGS" Margin="0,4,0,0" />
|
||
|
||
<ItemsControl ItemsSource="{Binding EditorTagChoices}" IsVisible="{Binding HasTagChoices}">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="vm:TagChoice">
|
||
<Button Classes="chiptoggle" Classes.worn="{Binding IsWorn}" Margin="0,0,6,6"
|
||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).ToggleEditorTagCommand}"
|
||
CommandParameter="{Binding}">
|
||
<TextBlock Classes="mono" FontSize="11.5" Text="{Binding Label}" />
|
||
</Button>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Grid ColumnDefinitions="*,8,Auto">
|
||
<TextBox Grid.Column="0" Classes="field" Text="{Binding EditorNewTag}"
|
||
PlaceholderText="new tag" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Width="72" Content="ADD"
|
||
Command="{Binding AddEditorTagCommand}" />
|
||
</Grid>
|
||
|
||
<!--
|
||
◆ QUICK ACCESS. Staged on the shared VaultViewModel.EditorPinnedPaths the way EditorTagChoices
|
||
stages tags above — populated when the editor opens, read back by BuildHost on SAVE, left alone
|
||
by CANCEL. Nothing here is phone-only: a pin added on this page is the same PinnedPathList entry
|
||
the desktop's drawer stages under the same field, so it syncs to every other client the moment
|
||
this vault does, exactly as a tag would.
|
||
|
||
No folder glyph in the row, unlike the desktop's — this head carries MonoFont and a heading font
|
||
(see Theme/Phone.axaml) and no icon font, so the desktop's  would draw as a tofu box
|
||
rather than a folder here. The path text carries the row on its own, trimmed in a Grid's star
|
||
column rather than a StackPanel's — a StackPanel measures its children at infinity, so
|
||
TextTrimming never actually engages inside one; this is the same fix TransfersScreen's pane
|
||
headers needed for the same reason.
|
||
|
||
The remove button is Classes="icon" at its default 44x44, not the desktop's 22x22 close box —
|
||
see this file's touch-floor rule at the top of the class: a target sized for a mouse pointer is
|
||
not one a thumb can reliably land on.
|
||
|
||
See surface B, the Files screen's pin chips, for where these pins actually get used on this
|
||
head — the hint sentence below names it rather than the desktop's terminal strip, which does
|
||
not exist here.
|
||
-->
|
||
<TextBlock Classes="label" Text="QUICK ACCESS" Margin="0,4,0,0" />
|
||
|
||
<ItemsControl ItemsSource="{Binding EditorPinnedPaths}">
|
||
<ItemsControl.ItemsPanel>
|
||
<ItemsPanelTemplate><StackPanel Spacing="6" /></ItemsPanelTemplate>
|
||
</ItemsControl.ItemsPanel>
|
||
<ItemsControl.ItemTemplate>
|
||
<DataTemplate x:DataType="x:String">
|
||
<Border MinHeight="44" CornerRadius="10" Background="{StaticResource Field}"
|
||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12,0">
|
||
<Grid ColumnDefinitions="*,Auto">
|
||
<TextBlock Grid.Column="0" Classes="mono" FontSize="12.5" Text="{Binding}"
|
||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||
<Button Grid.Column="1" Classes="icon" Content="✕"
|
||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).RemoveEditorPinCommand}"
|
||
CommandParameter="{Binding}" ToolTip.Tip="Unpins this path" />
|
||
</Grid>
|
||
</Border>
|
||
</DataTemplate>
|
||
</ItemsControl.ItemTemplate>
|
||
</ItemsControl>
|
||
|
||
<Grid ColumnDefinitions="*,8,Auto">
|
||
<TextBox Grid.Column="0" Classes="field" Text="{Binding EditorNewPin}"
|
||
PlaceholderText="/path/to/folder" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Width="72" Content="ADD"
|
||
Command="{Binding AddEditorPinCommand}" />
|
||
</Grid>
|
||
|
||
<TextBlock Classes="body"
|
||
Text="Pins show up as chips on the Files screen while you are connected to this host — tap one to jump straight there." />
|
||
|
||
<!--
|
||
◆ The one control here that publishes something. Turning it on copies this host's address and
|
||
port into plaintext columns the server can read, which is the single deliberate concession in
|
||
the design — see ADR 0004 — so it says so rather than being a switch labelled "relay".
|
||
|
||
◆ AND IT SAYS, FIRST, THAT IT BUYS NOTHING TODAY. The relay's server half is built and this
|
||
client has no path to it — the connect path dials the address directly whether this is ticked or
|
||
not — so the box as it stood spent that concession and delivered nothing for it. Left tickable
|
||
rather than disabled, so a host already carrying the flag can lose it. See
|
||
docs/reaching-a-host-you-cannot-dial.md.
|
||
-->
|
||
<CheckBox IsChecked="{Binding EditorRelayEnabled}" MinHeight="44">
|
||
<TextBlock Classes="mono" FontSize="11.5"
|
||
Text="Reach this host through the server relay (not wired up yet)"
|
||
TextWrapping="Wrap" />
|
||
</CheckBox>
|
||
<TextBlock Classes="body"
|
||
Text="Not built yet: this app always dials the host itself, so ticking this stores the address on the server and changes nothing about how the host is reached. When it works, the relay will dial on your behalf — which is why the address and port have to be stored in the clear. Everything else about the host stays encrypted either way, and a relayed host needs a port of its own rather than its group's." />
|
||
|
||
<!--
|
||
This page covers the whole screen while it is open, which is why it needs a Status line of its
|
||
own rather than relying on the one the list behind it draws at the top of HOSTS (see this file's
|
||
other Classes="detail" Text="{Binding Status}") — that TextBlock is off-screen for as long as
|
||
IsEditing is true. AddEditorTagCommand and AddEditorPinCommand both refuse silently into Status
|
||
rather than throwing, so without a line to draw it here the honesty rule would be broken: a
|
||
refusal that writes a message nothing on screen shows is the same, to whoever pressed ADD, as no
|
||
refusal at all.
|
||
-->
|
||
<TextBlock Classes="detail" Text="{Binding Status}" TextWrapping="Wrap"
|
||
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||
|
||
<Grid ColumnDefinitions="*,8,*" Margin="0,6,0,0">
|
||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||
Command="{Binding SaveHostCommand}" />
|
||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||
Command="{Binding CancelEditCommand}" />
|
||
</Grid>
|
||
</StackPanel>
|
||
</ScrollViewer>
|
||
</Grid>
|
||
|
||
</Panel>
|
||
|
||
</UserControl>
|