Public Access
Merge main into the phone connections branch
Main had already taken this branch's first two commits, so what merged is the Connections work against three things that landed beside it. Four of the six conflicts were prose about arrangements both sides changed; two were real. **The phone hub gained a Teams row while this branch was moving the keychain onto it.** Both are additions to `IsMoreSurface` and both belong: teams because the desktop reaches them from its rail and the phone through the hub, the keychain because a bottom bar is for the places a session moves between. The membership test, the back gesture's first case and the hub's own arithmetic all take the union. The distinction is now written down rather than implied — teams is the design's count plus one, and the keychain is the only rearrangement of it: the bar lost a slot to gain that row. **`ConnectAndAnnounceAsync` was the real one.** Main gave it `RememberTypedPasswordAsync`, which binds the password that just worked to the host it worked on; this branch had replaced the `HostRowViewModel` that method needs with a four-field `ConnectionTarget`. Keeping both meant deciding what a manual connection does with a password that succeeded, and the answer was already written on the screen it is typed into: nothing. There is no item to bind a credential to and none to bind it on, and that path saves nothing by design. So `ConnectionTarget` carries the row again — as a nullable, in place of the host id it had, with `HostId` derived from it. Two things read it and both are things that can only be done to a keychain item rather than to an address: naming the log entry, and keeping the password. Null is not missing data there; it is the whole of what makes the manual path different, and having one field rather than two keeps "was this a keychain host" a question with one answer. The desktop's rail lost SFTP and S3 to the tab strip on main, so the README's "a rail with nine slots has room" was true when it was written this afternoon and is not now. It says the room rather than the number. Phase 11's four new device checks and main's Phase 12 on teams were the same conflict twice — two appends to the end of one file — and both are kept. Verified after resolving: the solution builds, the Android head builds clean, and 837 tests pass across the seven client suites, including main's own additions (233 shell, 79 layout, 240 domain, 118 sync, 54 session, 74 terminal, 39 storage).
This commit is contained in:
@@ -1,25 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
The launcher mark, and it is the same mark ServerScreen and LockedScreen draw: a square
|
||||
outline in the accent with >_ inside it. Redrawn as a vector rather than exported as a
|
||||
bitmap so there is one geometry to change and no set of five PNG densities to forget one
|
||||
of.
|
||||
The launcher mark, and it is the same mark PhoneShell's header and the desktop titlebar draw:
|
||||
>_ in the canvas colour on a solid accent tile. Filled rather than outlined since v2.
|
||||
|
||||
#5B8CFF is AccentColor from DodoSSH.Client.Shell's Theme/Palette.axaml, written out
|
||||
because an Android resource cannot reference a XAML dictionary. The same duplication
|
||||
colors.xml already carries for the window background, and the same rule applies: if the
|
||||
palette moves, this moves with it.
|
||||
The tile is not in this file. It is the background layer — @color/dodo_accent, see ic_launcher.xml
|
||||
— and that is the whole trick of the filled design on Android: the rounding a launcher applies is
|
||||
its mask, so letting the mask make the tile gets a squircle on one device and a circle on another
|
||||
without either being drawn here. A rounded rectangle painted into this layer would be a second
|
||||
rounded shape inside the first, visibly clipped at the corners on any device whose mask is not the
|
||||
one it was drawn for.
|
||||
|
||||
108x108 with the artwork inside the middle 72 is the adaptive-icon contract — the outer
|
||||
18 on each edge is what the launcher eats for masking and parallax. That 72 is a width,
|
||||
though, and the mark is a square: its corners are what a circular mask reaches first. At
|
||||
48 across the corners land 33.9 out against a radius of 36 and read as clipped even
|
||||
though they technically clear it. 42 puts them at 29.7, which is margin one can see.
|
||||
#0E1220 is AccentInk from DodoSSH.Client.Shell's Theme/Palette.axaml, written out because an
|
||||
Android resource cannot reference a XAML dictionary. It equals Canvas today and is named
|
||||
separately in the palette for a reason worth keeping in mind here: this is ink on the accent, not
|
||||
the window behind it, and it follows AccentInk if the two ever part.
|
||||
|
||||
The stroke widths are the one place this deliberately departs from the screen. In the app
|
||||
the box is a 1px border on 44px; scaled honestly that would be 1.0 here, and a launcher
|
||||
drawing this at 48dp would render it at half a pixel and show nothing. 2.2 and 2.8 are
|
||||
what keep it reading as the same hairline mark at the size it is actually looked at.
|
||||
108x108 with the artwork inside the middle 72 is the adaptive-icon contract — the outer 18 on each
|
||||
edge is what the launcher eats for masking and parallax. The glyph spans 36.06..71.94, which is
|
||||
half the width of that 72 and centred in it. That half is taken from the headers rather than
|
||||
invented: the phone draws >_ at font size 10 on a 26px tile and the titlebar at 9 on 20px, both a
|
||||
little under half the tile across, and a launcher icon is looked at from further away than either.
|
||||
|
||||
The stroke width is the one place this deliberately departs from the screen. In the app the glyph
|
||||
is a bold mono face whose stems come out near a fifth of its height; 5.4 here is nearer a quarter,
|
||||
which is what keeps it reading as the same mark at the size it is actually looked at.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
@@ -27,28 +31,21 @@
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- The box: 42 across, centred, square-cornered as the Border in the app is. -->
|
||||
<path
|
||||
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5B8CFF"
|
||||
android:strokeWidth="2.2" />
|
||||
|
||||
<!-- The chevron of >_ -->
|
||||
<path
|
||||
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
|
||||
android:pathData="M36.06,43.33 L49.91,53.57 L36.06,63.8"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5B8CFF"
|
||||
android:strokeWidth="2.8"
|
||||
android:strokeColor="#0E1220"
|
||||
android:strokeWidth="5.4"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
|
||||
<!-- The underscore, on the baseline the chevron bottoms out at. -->
|
||||
<path
|
||||
android:pathData="M54,59.7 L63.2,59.7"
|
||||
android:pathData="M54,64.68 L71.94,64.68"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5B8CFF"
|
||||
android:strokeWidth="2.8"
|
||||
android:strokeColor="#0E1220"
|
||||
android:strokeWidth="5.4"
|
||||
android:strokeLineCap="round" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
the wallpaper's colours. The system tints this by its alpha and discards the colour, so
|
||||
the geometry is the foreground's and white is only a way of saying "opaque here".
|
||||
|
||||
Note what that means for the filled design: the accent tile is the background layer, and a
|
||||
themed icon drops the background entirely. So the shape that survives here is the glyph, not
|
||||
the tile — which is the right way round anyway. Filling this layer to the edges to stand in
|
||||
for the tile would tint to a featureless square with nothing of the mark left in it.
|
||||
|
||||
Worth shipping rather than leaving out: a launcher with themed icons on and no monochrome
|
||||
layer to use falls back to the full-colour icon, so the one app on the home screen still
|
||||
drawn in green is this one.
|
||||
drawn in blue is this one.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
@@ -15,24 +20,18 @@
|
||||
android:viewportHeight="108">
|
||||
|
||||
<path
|
||||
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
|
||||
android:pathData="M36.06,43.33 L49.91,53.57 L36.06,63.8"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="2.2" />
|
||||
|
||||
<path
|
||||
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="2.8"
|
||||
android:strokeWidth="5.4"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
|
||||
<path
|
||||
android:pathData="M54,59.7 L63.2,59.7"
|
||||
android:pathData="M54,64.68 L71.94,64.68"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="2.8"
|
||||
android:strokeWidth="5.4"
|
||||
android:strokeLineCap="round" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
adaptive icons landed in 26, so there is no device this ships to that would need the
|
||||
bitmaps. Density buckets exist to pick a PNG; a vector has nothing to pick between.
|
||||
|
||||
The background is the same @color/dodo_window the window, status bar and navigation bar
|
||||
use, so the mark sits on the app's own near-black rather than on a second dark that is
|
||||
almost but not quite it.
|
||||
The background is the accent, and that is the tile itself rather than a backdrop for one:
|
||||
the v2 mark is >_ knocked out of a solid accent square, so the square is this layer and the
|
||||
launcher's mask is what rounds it. See ic_launcher_foreground.xml for why the rounding is
|
||||
left to the mask instead of drawn.
|
||||
|
||||
It was @color/dodo_window until the mark went from outlined to filled, which is worth
|
||||
knowing if a home screen still shows the dark version: a launcher caches icons, and the
|
||||
cache outlives the install that changed them.
|
||||
-->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/dodo_window" />
|
||||
<background android:drawable="@color/dodo_accent" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -10,4 +10,11 @@
|
||||
hidden.
|
||||
-->
|
||||
<color name="dodo_window">#0E1220</color>
|
||||
|
||||
<!--
|
||||
AccentColor from the same palette, here because the launcher icon's background layer is a colour
|
||||
and not a drawable. Same hand-kept duplication as above, and the same rule: if the palette moves,
|
||||
this moves with it.
|
||||
-->
|
||||
<color name="dodo_accent">#5B8CFF</color>
|
||||
</resources>
|
||||
|
||||
@@ -418,6 +418,10 @@
|
||||
<!--
|
||||
Shown only for a host that actually asks for one. A password box beside a key-authenticated host
|
||||
is an invitation to type a secret nothing will use.
|
||||
|
||||
The tick below it is the phone's whole answer to storing one, and on this head it is the only one:
|
||||
the keychain lists credentials here but has no editor to create one in, so before this a password
|
||||
typed on a phone could only ever be typed again. The host editor's picker could then bind it.
|
||||
-->
|
||||
<TextBox Classes="field secret" IsVisible="{Binding SelectedHostAsksForAPassword}"
|
||||
Text="{Binding ConnectPassword}" PlaceholderText="password">
|
||||
@@ -426,6 +430,12 @@
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding RemembersConnectPassword}" MinHeight="44"
|
||||
IsVisible="{Binding SelectedHostAsksForAPassword}">
|
||||
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="Remember this password for this host" />
|
||||
</CheckBox>
|
||||
|
||||
<TextBlock Classes="detail" TextWrapping="Wrap" IsVisible="{Binding !SelectedHostAsksForAPassword}"
|
||||
Text="{Binding SelectedHostAuthenticationNote}" />
|
||||
|
||||
|
||||
@@ -80,9 +80,10 @@
|
||||
</RadioButton>
|
||||
|
||||
<!--
|
||||
Tags. The one category holding nothing secret — a tag is a name — and here because renaming one is
|
||||
one write instead of twenty, and a rename needs somewhere to happen. Making one usually happens in
|
||||
a host's editor instead; this is where they are renamed and deleted.
|
||||
Tags. The one category holding nothing secret — a tag is a name — and here because a host's editor
|
||||
is the only place on this head that makes one, which leaves nowhere else to see the whole set or
|
||||
what wears it. Deleting one happens here, with every other item kind; renaming is desktop work,
|
||||
because nothing on this screen opens an editor.
|
||||
-->
|
||||
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsTags, Mode=OneWay}"
|
||||
Command="{Binding ShowSectionCommand}"
|
||||
@@ -112,11 +113,46 @@
|
||||
-->
|
||||
<TextBlock Grid.Row="2" Classes="detail" Margin="18,6,18,2" Text="{Binding SectionSummary}" />
|
||||
|
||||
<!-- ============ what to do with the chosen item ============ -->
|
||||
<!--
|
||||
DELETE and nothing beside it. The desktop's detail pane offers EDIT as well, and there is nothing here
|
||||
for it to open — so this is the one action this head has for a keychain item, and until it existed the
|
||||
confirmation below could not be reached at all.
|
||||
|
||||
It shares row 2 with that confirmation rather than being a bottom bar like the one on HOSTS or FILES,
|
||||
because the question is what this button turns into: ShowsItemActions is false while a deletion is
|
||||
armed, so the two swap in place and DELETE cannot be pressed again underneath its own question. A
|
||||
button at the foot of the screen and its question at the top would be one exchange drawn twice.
|
||||
|
||||
And it names the item, because a keychain row is a card with a background of its own — see the selected
|
||||
style below, which had nothing to colour until now. DELETE is the last button that should be pressed on
|
||||
a guess about which row it means.
|
||||
-->
|
||||
<Border Grid.Row="2" IsVisible="{Binding ShowsItemActions}" Margin="12,4"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="12" Padding="14,10">
|
||||
<Grid ColumnDefinitions="*,10,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="label" Text="SELECTED" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
|
||||
Text="{Binding SelectedVaultItem.Name}" TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="tag outline">
|
||||
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2" Classes="danger" Height="44" Width="104" Content="DELETE"
|
||||
Command="{Binding DeleteSelectedItemCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ ◆ the counted confirmation ============ -->
|
||||
<!--
|
||||
In place, above the list it refers to. The text comes from the view model, which counts what breaks —
|
||||
"three hosts authenticate with this key and will refuse to connect" — and the count is the whole
|
||||
difference between a sentence somebody reads and one they click past.
|
||||
In place, above the list it refers to and over the button that asked it. The text comes from the view
|
||||
model, which counts what breaks — "three hosts authenticate with this key and will refuse to connect" —
|
||||
and the count is the whole difference between a sentence somebody reads and one they click past.
|
||||
-->
|
||||
<Border Grid.Row="3" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
@@ -152,6 +188,16 @@
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
<!--
|
||||
The fill goes on the card and not on the ContentPresenter behind it, which is where HOSTS and
|
||||
FILES put theirs. Those rows are bare Borders; a keychain row is Border.card, which paints an
|
||||
opaque Sidebar over anything drawn underneath — so the selector has to reach the card itself or
|
||||
the chosen row looks exactly like the others. It has to look different now that a button above
|
||||
the list acts on it.
|
||||
-->
|
||||
<Style Selector="ListBoxItem:selected Border.card">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
||||
|
||||
@@ -24,10 +24,11 @@
|
||||
|
||||
<!-- Identity. The chip names the account so a phone with two profiles is not a guess. -->
|
||||
<StackPanel Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10" Margin="0,48,0,36">
|
||||
<Border Width="44" Height="44" BorderBrush="{StaticResource Accent}" BorderThickness="1"
|
||||
<!-- Filled since v2; see ServerScreen for where the 14 comes from. -->
|
||||
<Border Width="44" Height="44" CornerRadius="14" Background="{StaticResource Accent}"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text=">_" Foreground="{StaticResource Accent}" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="16" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
@@ -49,37 +50,31 @@
|
||||
the nearest thing to hand, and reaching past it to a button is the sort of friction that gets a
|
||||
phone client called slow.
|
||||
-->
|
||||
<TextBox Classes="secret" Text="{Binding Passphrase}" PlaceholderText="vault passphrase"
|
||||
Height="48" Padding="14,0" VerticalContentAlignment="Center"
|
||||
Background="{StaticResource Field}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="6" Foreground="{StaticResource Text}"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="12"
|
||||
<TextBox Classes="field secret" Text="{Binding Passphrase}" PlaceholderText="vault passphrase"
|
||||
IsEnabled="{Binding !IsBusy}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding UnlockCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
|
||||
<Button Content="UNLOCK" Command="{Binding UnlockCommand}" IsEnabled="{Binding !IsBusy}"
|
||||
Height="48" Margin="0,10,0,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Background="{StaticResource Accent}" Foreground="{StaticResource AccentInk}"
|
||||
CornerRadius="6" FontFamily="{StaticResource MonoFont}" FontSize="12" FontWeight="SemiBold" />
|
||||
<Button Classes="primary" Content="UNLOCK" Margin="0,10,0,0"
|
||||
Command="{Binding UnlockCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
|
||||
<!--
|
||||
Present only when this phone actually holds a device key. The design draws it unconditionally,
|
||||
but offering a fingerprint that cannot open anything is worse than not offering one — see
|
||||
AndroidDeviceKeyStore for the three ordinary ways it stops being available.
|
||||
|
||||
The label sets no font of its own: inside a Button.secondary it inherits the class's, which is
|
||||
what stops this one reading half a point smaller than every other outlined button on the phone.
|
||||
-->
|
||||
<Button Command="{Binding UnlockWithDeviceCommand}" IsVisible="{Binding CanUnlockWithDevice}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
Height="48" Margin="0,8,0,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
Background="Transparent" BorderBrush="{StaticResource BorderMid}" BorderThickness="1"
|
||||
CornerRadius="6" Foreground="{StaticResource Text}">
|
||||
<Button Classes="secondary" Margin="0,8,0,0"
|
||||
Command="{Binding UnlockWithDeviceCommand}" IsVisible="{Binding CanUnlockWithDevice}"
|
||||
IsEnabled="{Binding !IsBusy}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="9">
|
||||
<Ellipse Width="16" Height="16" Stroke="{StaticResource Accent}" StrokeThickness="1.5"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="UNLOCK WITH FINGERPRINT" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<TextBlock Text="UNLOCK WITH FINGERPRINT" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
@@ -93,10 +88,15 @@
|
||||
|
||||
<!--
|
||||
◆ The disclosure. Absent when there is nothing to disclose; never a card reading zero.
|
||||
|
||||
Not Border.card, and neither are its two counterparts on HOSTS and FILES: that class is a panel,
|
||||
and this is a warning, so the background and the border are the warn pair rather than the chrome
|
||||
one. What it does take is the radius the v2 ladder gives anything card-sized, which is what the
|
||||
other two already draw.
|
||||
-->
|
||||
<Border IsVisible="{Binding HasLiveSessions}" Margin="0,22,0,0"
|
||||
Background="{StaticResource WarnWash}" BorderBrush="{StaticResource WarnSoft}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
||||
BorderThickness="1" CornerRadius="12" Padding="14,12">
|
||||
<StackPanel Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<!-- Live, not Accent: this marks shells that are still running, which is a fact and not a
|
||||
@@ -116,12 +116,17 @@
|
||||
<!--
|
||||
The only answer to a forgotten passphrase, and it is deliberately the last thing on the screen and
|
||||
the only red one. Nothing can recover a passphrase; this empties the phone and starts again.
|
||||
|
||||
Two of Button.danger's properties are overridden rather than taken, and they are the two that would
|
||||
change what this control says. Elsewhere that class is a DELETE sitting beside a KEEP, where a red
|
||||
outline marks which of two offered actions costs something; here there is no pair, and a bordered
|
||||
red button under UNLOCK would read as the second thing to try rather than as the last. Borderless
|
||||
and Medium is what keeps it a way out rather than an offer. Everything else — the height, the
|
||||
centring, the colour, the type — is the class's.
|
||||
-->
|
||||
<Button Grid.Row="2" Command="{Binding SignOutCommand}"
|
||||
Margin="0,28,0,20" Padding="0,14" HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" Background="Transparent" BorderThickness="0"
|
||||
Foreground="{StaticResource Danger}" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="10.5" FontWeight="Medium"
|
||||
<Button Grid.Row="2" Classes="danger" Margin="0,28,0,20"
|
||||
BorderThickness="0" FontWeight="Medium"
|
||||
Command="{Binding SignOutCommand}"
|
||||
Content="RESET THIS PHONE — forgot passphrase" />
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -8,18 +8,22 @@
|
||||
<!--
|
||||
Design v2 — SETTINGS: the hub for everything the bottom bar has no room for.
|
||||
|
||||
Three slots and nine destinations is the arithmetic the design solves by putting the rest one tap
|
||||
Three slots and ten destinations is the arithmetic the design solves by putting seven of them one tap
|
||||
deeper. This screen is that tap. It takes the shell as its data context rather than the vault, because
|
||||
every row on it is a navigation command and nothing here reads an item.
|
||||
|
||||
Teams is the tenth and the design never drew it — see the row itself. That one is the design's count
|
||||
plus one rather than a rearrangement of it. The keychain below is a rearrangement, and the only one:
|
||||
the bottom bar lost a slot to gain this row.
|
||||
|
||||
The rows are the design's list rows rather than cards: a card is one thing you act on, and a destination
|
||||
is not a thing — it is a place. Each carries a sentence saying what is behind it, because a hub whose
|
||||
entries are one word each is a menu you have to open to read.
|
||||
|
||||
── from MORE to SETTINGS ──────────────────────────────────────────────────────────────────────────────
|
||||
The hub was called More because that was the honest name for five things that did not fit. With the
|
||||
keychain on it as well it is the place everything that is not connecting to a machine lives, which has a
|
||||
name of its own and a glyph everyone already knows.
|
||||
The hub was called More because that was the honest name for a handful of things that did not fit. With
|
||||
the keychain on it as well it is the place everything that is not connecting to a machine lives, which
|
||||
has a name of its own and a glyph everyone already knows.
|
||||
|
||||
The keychain is first, and above the rule, because it is the only row here that holds the user's own
|
||||
material rather than a screen about the application. Everything below it is somewhere to look at what
|
||||
@@ -123,6 +127,29 @@
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
Teams, which the v2 design has no row for — it is a shipped screen the design had no slot for
|
||||
rather than a drawn one with nothing behind it. It is on the phone because an invitation is
|
||||
claimed by signing in, and somebody being invited is at least as likely to be holding a phone.
|
||||
|
||||
◎ rather than a glyph of its own. The desktop rail already draws teams with it, and two heads
|
||||
giving one destination two marks is how a user learns the wrong one.
|
||||
-->
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Team}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="◎" Foreground="{StaticResource AccentText}" FontSize="14"
|
||||
Width="22" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Teams" />
|
||||
<TextBlock Classes="detail" Foreground="{StaticResource TextDim}"
|
||||
Text="Who shares a keychain with you, and who holds its key." />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="›" Foreground="{StaticResource TextGhost}" FontSize="15"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
@@ -20,10 +20,17 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel VerticalAlignment="Center" Margin="24,48" Spacing="12">
|
||||
|
||||
<Border Width="44" Height="44" BorderBrush="{StaticResource BorderMid}" BorderThickness="1"
|
||||
<!--
|
||||
The same filled tile the rest of the head draws since v2, at the same 44 and the same 14 — and
|
||||
drained rather than recoloured. Accent on this screen would be the shell's one "this is what the
|
||||
screen wants you to do" colour spent on the one screen that wants nothing, so the tile is Raised
|
||||
and the glyph stays TextFaint. The shape says which application this is; the colour says the
|
||||
screen is inert, which is the whole message.
|
||||
-->
|
||||
<Border Width="44" Height="44" CornerRadius="14" Background="{StaticResource Raised}"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock Text=">_" Foreground="{StaticResource TextFaint}" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
FontSize="16" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
state machine. What differs is only what each one draws.
|
||||
|
||||
── v2 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
The desktop's eight rail destinations become four in a bottom bar, and five more live one tap deeper
|
||||
behind MORE: snippets, SFTP, S3, logs and preferences. That is the v2 design's own arrangement, and it
|
||||
replaces the first design's four, which had nothing behind them at all. Five characters was a desktop
|
||||
constraint and the phone uses words.
|
||||
The desktop's eight rail destinations become four in a bottom bar, and the rest live one tap deeper
|
||||
behind MORE: snippets, SFTP, S3, logs, preferences — and teams, which v2 did not draw and which is
|
||||
argued for on the screen itself. That is the v2 design's own arrangement, and it replaces the first
|
||||
design's four, which had nothing behind them at all. Five characters was a desktop constraint and the
|
||||
phone uses words.
|
||||
|
||||
The order is the design's rather than the rail's. Terminal sits second, beside Hosts, because those two
|
||||
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
|
||||
@@ -58,7 +59,7 @@
|
||||
|
||||
Hidden behind SETTINGS, and that is the design's arrangement rather than a saving. v2 gives every
|
||||
screen one header carrying that screen's own name and its own actions — a back arrow, an add, a
|
||||
refresh — so the hub screens draw their own and this one stands down rather than stacking a second
|
||||
refresh — so the hub's screens draw their own and this one stands down rather than stacking a second
|
||||
row of chrome above theirs. What is left is HOSTS, which is the screen the application opens on and
|
||||
the one where the vault's name and the sync light are the most useful thing a header could say.
|
||||
|
||||
@@ -135,7 +136,7 @@
|
||||
============ under MORE ============
|
||||
|
||||
The hub itself takes the shell as its data context, because every row on it is a navigation
|
||||
command; the five destinations behind it each take the view model they are about, so each one is
|
||||
command; the destinations behind it each take the view model they are about, so each one is
|
||||
wrapped. SnippetsScreen and LogsScreen are nullable on the shell — they are rebuilt on every
|
||||
unlock and nulled on lock — and it is the collapsed wrapper that keeps a template from binding
|
||||
against nothing.
|
||||
@@ -150,6 +151,15 @@
|
||||
<views:LogsScreen DataContext="{Binding LogsScreen}" />
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
The sixth destination behind MORE, and the one v2 never drew — see the comment on the screen
|
||||
itself. Wrapped like its neighbours even though Teams is not nullable: the reason for the wrapper
|
||||
is the data context, not the null. IsTeamShowing is the shell's and Teams is not the shell.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsTeamShowing}">
|
||||
<views:TeamsScreen DataContext="{Binding Teams}" />
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
One screen for both file destinations. SFTP and S3 differ in which picker they offer and in
|
||||
nothing else below it — the panes, the queue and the transfers are the same IRemoteFileStore
|
||||
@@ -282,13 +292,14 @@
|
||||
Command="{Binding ShowTerminalCommand}" />
|
||||
|
||||
<!--
|
||||
IsMoreSurface rather than IsMoreShowing: this tab stands for seven screens, and a bar that went
|
||||
dark the moment you opened one of them would only ever light two of its three entries.
|
||||
IsMoreSurface rather than IsMoreShowing: this tab stands for the hub and everything behind it,
|
||||
and a bar that went dark the moment you opened one of them would only ever light two of its
|
||||
three entries.
|
||||
|
||||
"Settings" and a gear, where it was "More" and a hamburger. The hub held five destinations and a
|
||||
paragraph and could not be called anything better than More; with the keychain and preferences
|
||||
both on it, it is the place everything that is not connecting to something lives — which is what
|
||||
a gear has meant on a phone since before this application existed.
|
||||
"Settings" and a gear, where it was "More" and a hamburger. The hub held a handful of things
|
||||
that did not fit and could not be called anything better than More; with the keychain, teams and
|
||||
preferences all on it, it is the place everything that is not connecting to something lives —
|
||||
which is what a gear has meant on a phone since before this application existed.
|
||||
-->
|
||||
<views:NavButton Grid.Column="2" Label="Settings" Glyph="⚙" IsCurrent="{Binding IsMoreSurface}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
|
||||
@@ -272,7 +272,7 @@ internal sealed partial class PhoneShell : UserControl
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// v2 is the first arrangement here with a second level: five destinations sit behind MORE, each with
|
||||
/// v2 is the first arrangement here with a second level: six destinations sit behind MORE, each with
|
||||
/// its own back arrow. Android's back is the same gesture as that arrow and users reach for it first,
|
||||
/// and left unhandled it does not go up — it finishes the activity. Ending the application from a log
|
||||
/// screen is not a plausible reading of "back".
|
||||
@@ -347,7 +347,8 @@ internal sealed partial class PhoneShell : UserControl
|
||||
switch (current.Screen)
|
||||
{
|
||||
case ShellScreen.Snippets or ShellScreen.Logs or ShellScreen.Transfers
|
||||
or ShellScreen.Buckets or ShellScreen.Preferences or ShellScreen.Vault:
|
||||
or ShellScreen.Buckets or ShellScreen.Preferences or ShellScreen.Team
|
||||
or ShellScreen.Vault:
|
||||
current.ShowScreenCommand.Execute(ShellScreen.More);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
<StackPanel Margin="24,0" VerticalAlignment="Center" Spacing="0">
|
||||
|
||||
<StackPanel Spacing="10" HorizontalAlignment="Center" Margin="0,48,0,36">
|
||||
<Border Width="44" Height="44" BorderBrush="{StaticResource Accent}" BorderThickness="1"
|
||||
<!--
|
||||
Filled rather than outlined since v2, the same mark the header, the desktop titlebar and the
|
||||
launcher icon carry. The radius is not picked: the mark runs 6 at 20 and 8 at 26, which is a
|
||||
third of a unit per unit of tile and lands on 14 at 44 — and 14 is a rung of Phone.axaml's
|
||||
ladder, the one for a block of monospaced output, which is what this is.
|
||||
-->
|
||||
<Border Width="44" Height="44" CornerRadius="14" Background="{StaticResource Accent}"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text=">_" Foreground="{StaticResource Accent}" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
<TextBlock Text=">_" Foreground="{StaticResource AccentInk}" FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="16" FontWeight="Bold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<TextBlock Classes="title" Text="DodoSSH" HorizontalAlignment="Center" />
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
<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.TeamsScreen"
|
||||
x:DataType="vm:TeamsViewModel"
|
||||
Background="{StaticResource Canvas}">
|
||||
|
||||
<!--
|
||||
TEAMS, under MORE — and the one screen behind that hub the v2 phone design never drew.
|
||||
|
||||
It is the reverse of every other entry in docs/design-import-gaps.md: a shipped screen the design had
|
||||
no slot for, rather than a drawn screen with nothing behind it. It is on the phone because an
|
||||
invitation is claimed by *signing in*, and the person being invited is at least as likely to be
|
||||
holding a phone as sitting at a desktop — a team the server has just put somebody into, visible only
|
||||
on a head they may never have installed, is a membership they cannot see.
|
||||
|
||||
That argument is also why the invited list is drawn here and not treated as an administrator's detail:
|
||||
the people on it are the ones who cannot yet see the team, and the row says out loud that no mail was
|
||||
sent.
|
||||
|
||||
So there is no mock-up to depart from. What this departs from instead is the desktop screen over the
|
||||
same view model, and every difference below is a phone difference rather than a second opinion.
|
||||
|
||||
**The desktop's two columns are one.** A 268-pixel team list beside a members-and-vaults table does
|
||||
not exist at 360dp, so the three lists stack in one scrolling column with the teams at the top. That
|
||||
is the same thing HOSTS does with the desktop's sidebar and its connect column, and for the same
|
||||
reason.
|
||||
|
||||
**Nothing scrolls inside anything.** The desktop caps its members and vaults lists at 240 and 200
|
||||
pixels so the two can sit above each other in one pane. Here every list is sized to its content and
|
||||
the screen's own ScrollViewer does all of the scrolling: a list that scrolls inside a page is a region
|
||||
a thumb has to find the edges of, and three of them on one screen is three ways to get stuck.
|
||||
|
||||
◆ **SHARE KEY is drawn and nothing that takes something away is.** That is a decision rather than a
|
||||
subset. Wrapping a vault key is the one act on this screen a server cannot perform at all — it needs a
|
||||
machine that already holds the key, and this phone is one — so a teams screen that could only be read
|
||||
would leave the product's central claim undemonstrated on the head most people carry. REMOVE MEMBER,
|
||||
WITHDRAW KEY and REVOKE INVITATION are the other half of that, and each of them acts on the first
|
||||
press: the view model's armed-confirmation state covers archiving a team and handing one over, and
|
||||
those three are not armed by it. The desktop guards them with a tooltip instead, which is a control a
|
||||
touch screen has no way to show. An irreversible revocation under a thumb with its explanation missing
|
||||
is the wrong trade, so all three stay on the desktop — where the sentence beside them is visible.
|
||||
Archiving and hand-over are not drawn either, for a plainer reason: they decide whether a team goes on
|
||||
existing and who controls it, which is not a thing to do while walking.
|
||||
|
||||
**ADD MEMBER is not drawn either**, and it is the operation this screen least needs. It is an address
|
||||
typed into a box, a directory lookup, a role picker, and a paragraph beside it saying what adding
|
||||
somebody did *not* do — and since invitations arrived the ordinary way into a team is one the server
|
||||
claims at sign-in, which is what put this screen on the phone at all. Creating a team is here, because
|
||||
a team is where those invitations are sent from and it is two short fields.
|
||||
|
||||
**The key-holder list under a vault is not drawn.** It is a fourth list, it belongs to the selected
|
||||
vault rather than to the team, and the view model publishes no flag saying whether it has anything in
|
||||
it — so a heading for it would sit over nothing whenever nobody holds a key, which is exactly the
|
||||
empty state this head insists comes from the view model rather than from markup. What the phone can
|
||||
answer about a vault is on the vault's own row: whether *this* machine can open it.
|
||||
|
||||
**↻ and `+` both, because this screen has more reason to re-read than any other.** Nothing here is
|
||||
cached — it is all read from the server on arrival and again at the end of every command — so the one
|
||||
thing a member cannot otherwise see is a change somebody else just made: a vault key wrapped to them
|
||||
from a colleague's desktop, or a team they have this moment been invited into. On the desktop the
|
||||
re-read is leaving the rail and coming back, which is one click. Here it is a trip out to MORE and
|
||||
back, so the button earns its place. It binds to a real command rather than to ShowScreen(Team),
|
||||
which would set Screen to the value it already holds, raise nothing and reload nothing.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<!-- ============ header ============ -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto" Height="56" Margin="8,0">
|
||||
<Button Grid.Column="0" Classes="icon" Content="←"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.More}" />
|
||||
<TextBlock Grid.Column="1" Classes="heading" Text="Teams" Margin="4,0" />
|
||||
<Button Grid.Column="2" Classes="icon" Content="↻" Command="{Binding RefreshCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="3" Classes="icon accent" Content="+" Command="{Binding NewTeamCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
</Grid>
|
||||
|
||||
<!-- ============ a new team ============ -->
|
||||
<!--
|
||||
Above the list rather than in place of it, which is the opposite of what the host and snippet
|
||||
editors do — and the difference is what the form is about. Those two edit a row that is on screen,
|
||||
so a card stacked over the list hides the thing being changed. This one is about a team that does
|
||||
not exist yet, and the teams that do are exactly the useful thing to be able to see while naming it:
|
||||
the slug has to be unique on this server, and the near misses are right underneath.
|
||||
-->
|
||||
<Border Grid.Row="1" Classes="card" Margin="12,0,12,8" IsVisible="{Binding IsCreatingTeam}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Classes="label" Text="NEW TEAM" />
|
||||
|
||||
<TextBox Classes="field" Text="{Binding NewTeamName}" PlaceholderText="name" />
|
||||
<TextBox Classes="field" Text="{Binding NewTeamSlug}" PlaceholderText="slug-for-urls" />
|
||||
|
||||
<TextBlock Classes="body"
|
||||
Text="The slug is lowercase letters, digits and hyphens, and has to be unique across this server. It is fixed once the team exists — a team can be renamed and its slug cannot." />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="CREATE"
|
||||
Command="{Binding CreateTeamCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Content="CANCEL"
|
||||
Command="{Binding CancelNewTeamCommand}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
Status, and it is the empty state as well: the view model writes "you are not in a team yet" into
|
||||
the same property it writes an offline notice and every command's outcome into. A literal here would
|
||||
be a second voice saying the same thing slightly differently.
|
||||
-->
|
||||
<TextBlock Grid.Row="2" Classes="detail" Margin="18,2,18,6" TextWrapping="Wrap"
|
||||
Text="{Binding Status}"
|
||||
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<!-- ============ the column ============ -->
|
||||
<ScrollViewer Grid.Row="3">
|
||||
<StackPanel Margin="0,0,0,18">
|
||||
|
||||
<TextBlock Classes="section" Text="TEAMS" Margin="18,4,18,4" />
|
||||
|
||||
<!--
|
||||
Rows as cards, filled when chosen, which is what HOSTS settled on in v2 and what the radius
|
||||
ladder calls a card: one item, one rule, one thing you act on. The fill is on the item rather
|
||||
than on a Border inside it so the rounding the theme draws for selection is the row's own.
|
||||
-->
|
||||
<ListBox ItemsSource="{Binding Teams}" SelectedItem="{Binding SelectedTeam}"
|
||||
IsVisible="{Binding HasTeams}" 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>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TeamRowViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto" MinHeight="54" Margin="14,11">
|
||||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Detail}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- The caller's own role in this team, which is what says why some of it is read-only. -->
|
||||
<Border Grid.Column="1" Classes="tag outline" Margin="8,0,0,0">
|
||||
<TextBlock Text="{Binding Role}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- ============ the chosen team ============ -->
|
||||
<StackPanel IsVisible="{Binding HasSelection}">
|
||||
|
||||
<TextBlock Classes="section" Text="MEMBERS" Margin="18,18,18,4" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Members}" SelectedItem="{Binding SelectedMember}"
|
||||
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>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TeamMemberRowViewModel">
|
||||
<Grid ColumnDefinitions="*,Auto" MinHeight="54" Margin="14,11">
|
||||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="{Binding Email}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!--
|
||||
◆ The one fact on this row that decides whether the button at the foot of the screen
|
||||
can do anything: an account with no published identity key has nothing for a vault
|
||||
key to be wrapped to. One sentence, from the view model, painted twice rather than
|
||||
written twice — the warning colour is the whole of the difference, and a converter
|
||||
for it would hide that the two are the same string.
|
||||
|
||||
The published case is quiet rather than green. Green on this head means a shell is
|
||||
open right now, and a published key is a durable fact about an account — borrowing
|
||||
the status colour for it would be the second meaning that makes the first
|
||||
unreadable. Only the missing key is coloured, because only it needs answering.
|
||||
-->
|
||||
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextDim}" Text="{Binding KeyState}"
|
||||
IsVisible="{Binding Member.IsEnrolled}" />
|
||||
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource WarnText}" Text="{Binding KeyState}"
|
||||
IsVisible="{Binding !Member.IsEnrolled}" />
|
||||
|
||||
<!--
|
||||
A date to the day, or that they have never been here at all. The view model writes
|
||||
both, and neither is a guess: the server records the account's last authenticated
|
||||
request at most once an hour, which is what makes a day the honest unit.
|
||||
-->
|
||||
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding LastActive}" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Column="1" Classes="tag outline" Margin="8,0,0,0">
|
||||
<TextBlock Text="{Binding Role}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="body" Margin="18,10,18,0"
|
||||
Text="Being in a team is what lets the server hand somebody this team's vaults. It is not what lets them read one: a vault key can only be wrapped by a machine that already holds it, which is what sharing below does." />
|
||||
|
||||
<!-- ============ ◆ who has been asked and has not arrived ============ -->
|
||||
<!--
|
||||
The section this screen exists for, and the one the desktop had nothing to draw until
|
||||
invitations were built. Read-only here: withdrawing one is a control that acts on the first
|
||||
press, which is the line drawn at the top of this file.
|
||||
|
||||
So these are cards rather than the flat rows above them, and the shape is the difference: a row
|
||||
that fills when you touch it is one of several you are choosing between, and there is nothing
|
||||
to choose here. An ItemsControl rather than a ListBox for the same reason — a list with a
|
||||
selection nothing reads would be a control offering something it cannot do.
|
||||
|
||||
Gated on the view model's own count rather than left to stand over an empty list, because a
|
||||
team with nobody outstanding is the ordinary case and a permanent empty heading would make it
|
||||
look like a section that had failed to load.
|
||||
|
||||
The waiting row carries the whole mechanism in its own sentence — no mail was sent, and they
|
||||
join when they first sign in here. That is the sentence somebody has to read, because every
|
||||
other product's version of this word means an email is on its way.
|
||||
-->
|
||||
<StackPanel IsVisible="{Binding HasInvitations}">
|
||||
<TextBlock Classes="section" Text="INVITED" Margin="18,18,18,4" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Invitations}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TeamInvitationRowViewModel">
|
||||
<Border Classes="card" Margin="12,3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="12.5" Text="{Binding Email}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextDim}" Text="{Binding State}"
|
||||
IsVisible="{Binding !IsPending}" />
|
||||
<TextBlock Classes="detail" FontSize="10" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource WarnText}" Text="{Binding State}"
|
||||
IsVisible="{Binding IsPending}" />
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Classes="tag outline" Margin="8,0,0,0">
|
||||
<TextBlock Text="{Binding Role}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="section" Text="VAULTS" Margin="18,18,18,4" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Vaults}" SelectedItem="{Binding SelectedVault}"
|
||||
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>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource Active}" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TeamVaultRowViewModel">
|
||||
<StackPanel Spacing="3" MinHeight="54" Margin="14,11" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!--
|
||||
Whether *this* phone can open it, which is a property of its keyring rather than
|
||||
anything the server could answer. Painted the same two ways as the member's key
|
||||
state above, because it is the same question asked from the other end.
|
||||
-->
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextDim}" Text="{Binding State}"
|
||||
IsVisible="{Binding IsReadable}" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource WarnText}" Text="{Binding State}"
|
||||
IsVisible="{Binding !IsReadable}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="body" Margin="18,10,18,0"
|
||||
Text="A vault listed here that this phone has no key to stays listed and stays shut. That is the ordinary case rather than a fault: somebody has been added to the team and nobody has wrapped the key to them yet." />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ ◆ giving somebody the key ============ -->
|
||||
<!--
|
||||
Raised over the column when both halves of the act have been chosen, as HOSTS raises its connect bar
|
||||
and SNIPPETS its insert bar, and for the reason written there: there is no second column to put it
|
||||
in, so it names what it will do rather than relying on a selection being visible beside the button.
|
||||
|
||||
Two wrappers rather than one condition. Sharing needs a member *and* a vault, and a binding cannot
|
||||
say `SelectedMember is not null && SelectedVault is not null` without a converter that does not
|
||||
exist — the log screen makes the same trade for the same reason. It also gets the halves in the
|
||||
right order: choosing who comes first, and until a vault is chosen there is nothing to offer them.
|
||||
-->
|
||||
<Panel Grid.Row="4" IsVisible="{Binding SelectedMember, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Border IsVisible="{Binding SelectedVault, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" Padding="14,12">
|
||||
<StackPanel Spacing="9">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="label" Text="THE KEY TO" />
|
||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding SelectedVault.Name}"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="label" Text="FOR" />
|
||||
<TextBlock Classes="mono" FontSize="11" Text="{Binding SelectedMember.Name}"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Classes="primary" Content="SHARE KEY" Command="{Binding ShareVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
|
||||
<!--
|
||||
The sentence the desktop hangs off a tooltip, which a phone cannot show — so it is body text
|
||||
under the button, where it is read before the tap rather than after it. It is not decoration:
|
||||
the key-log check proves this server has been consistent with itself and nothing more.
|
||||
-->
|
||||
<TextBlock Classes="body"
|
||||
Text="Their published key is checked against the server's append-only key log first, and nothing is wrapped if it does not appear there unchanged. That proves the server has been consistent with itself — not that the key is the right person's. Compare the fingerprint with them over a channel this server does not carry before sharing anything that matters." />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DodoSSH.Client.Android.Views;
|
||||
|
||||
/// <summary>Teams, under MORE — who is in one, and which of its vaults this phone can open.</summary>
|
||||
internal sealed partial class TeamsScreen : UserControl
|
||||
{
|
||||
public TeamsScreen() => AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
Reference in New Issue
Block a user