Public Access
Give the application a settings area built from what really exists
This commit is contained in:
@@ -1261,6 +1261,57 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
── v5c ──────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Settings mode's own furniture: a page title, a tracked section label, a card group and the rows inside
|
||||
it. Named apart from Border.section and TextBlock.sectionlabel above rather than reusing them — those
|
||||
are the hosts drawer's 320-pixel column, at that column's own padding and heading size, and the
|
||||
settings pages are a 1100-pixel one at the numbers Settings-General.dc.html and its three siblings all
|
||||
state identically: 12-radius cards on Raised with a 1px Border inset, 20/24 row padding, a 1px
|
||||
BorderSubtle divider between rows and none under the last one.
|
||||
-->
|
||||
<Style Selector="TextBlock.settingstitle">
|
||||
<Setter Property="FontSize" Value="33" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="LetterSpacing" Value="-0.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.settingssection">
|
||||
<Setter Property="FontSize" Value="10.5" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="LetterSpacing" Value="1.2" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextGhost}" />
|
||||
<Setter Property="Margin" Value="0,32,0,12" />
|
||||
</Style>
|
||||
<Style Selector="Border.settingscard">
|
||||
<Setter Property="Background" Value="{StaticResource Raised}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="Border.settingsrow">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtle}" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<Setter Property="Padding" Value="24,20" />
|
||||
</Style>
|
||||
<Style Selector="Border.settingsrow.last">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.settingsrowtitle">
|
||||
<Setter Property="FontSize" Value="14.5" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="LetterSpacing" Value="-0.2" />
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.settingsrowcaption">
|
||||
<Setter Property="FontSize" Value="12.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="LineHeight" Value="19.4" />
|
||||
<Setter Property="MaxWidth" Value="640" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
</Style>
|
||||
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
forty entries for machines that stopped existing years ago. So scanning writes nothing and the list
|
||||
says what each entry means; importing is a separate press on a set somebody has looked at.
|
||||
|
||||
Reachable from the preferences screen and not from the nav rail. It is a task rather than a
|
||||
destination — done once, or once a year — and a seventh rail entry would cost every screen a slot for
|
||||
something almost nobody is looking at.
|
||||
v5c-3: restyled into Import.dc.html's own table over SettingsView's content column — SettingsNav stays
|
||||
lit on Preferences while this is up, and the titlebar says "Back to preferences"; see
|
||||
MainWindowViewModel.IsImportOpen. No longer reachable from the nav rail, exactly as before: it is a task
|
||||
done once or once a year, reached from the Preferences page's own "OPEN IMPORTER" row.
|
||||
|
||||
── ◆ THE ONE TICK THAT READS PRIVATE KEYS ─────────────────────────────────────────────────────────────
|
||||
Below the list, off, and drawn only where the scan actually found an IdentityFile. It is the only control
|
||||
@@ -26,144 +27,212 @@
|
||||
What comes back afterwards is the report under the list: one line per key file, saying which were stored,
|
||||
which are protected by a passphrase this cannot know, and which were not there at all. That is reported
|
||||
rather than previewed for the same reason — previewing would mean reading them.
|
||||
|
||||
── ◆ WHAT THIS MEANS ──────────────────────────────────────────────────────────────────────────────────
|
||||
One chip per row rather than the old separate AUTHENTICATION/STATE columns, mapped off the two facts a
|
||||
row actually carries: ImportRowViewModel.AlreadyPresent and HasWarnings. A skipped Host pattern (a
|
||||
wildcard block) never becomes a row at all — see SshConfigImport.SkippedPatterns — so there is no third,
|
||||
"skipped" state to draw here; a warned row is the amber case instead, and it wins over "already here"
|
||||
because the warning is the more actionable of the two facts. See ImportRowViewModel.Meaning.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
<UserControl.Styles>
|
||||
<!-- The row's own hover fill, matching the design's per-row style-hover — Track, the same as every other table this application draws. -->
|
||||
<Style Selector="Border.importrow:pointerover">
|
||||
<Setter Property="Background" Value="{StaticResource Track}" />
|
||||
</Style>
|
||||
<Style Selector="Border.meaningchip.new">
|
||||
<Setter Property="Background" Value="{StaticResource LiveWash}" />
|
||||
</Style>
|
||||
<Style Selector="Border.meaningchip.new > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
||||
</Style>
|
||||
<Style Selector="Border.meaningchip.exists">
|
||||
<Setter Property="Background" Value="{StaticResource Chip}" />
|
||||
</Style>
|
||||
<Style Selector="Border.meaningchip.exists > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
</Style>
|
||||
<Style Selector="Border.meaningchip.warn">
|
||||
<Setter Property="Background" Value="{StaticResource WarnWash}" />
|
||||
</Style>
|
||||
<Style Selector="Border.meaningchip.warn > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource WarnText}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Border Grid.Row="0" Padding="14,0" Height="44"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="IMPORT SSH CONFIG" FontSize="12"
|
||||
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding ConfigPath}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="2" Classes="ghost" Content="SCAN" Command="{Binding ScanCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Reads the file and shows what it found. Nothing is stored." />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch" RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding Status}" FontSize="12" Margin="14,12,14,0"
|
||||
<!-- ============ HEADER ============ -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="10">
|
||||
<TextBlock Classes="settingstitle" Text="Import SSH config" />
|
||||
<TextBlock Classes="mono" Text="{Binding HeaderStatus}" FontSize="12"
|
||||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Height="40" VerticalAlignment="Top" Content="SCAN AGAIN"
|
||||
Command="{Binding ScanCommand}" IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Reads the file again and shows what it found. Nothing is stored." />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding Status}" FontSize="12" Margin="0,10,0,0"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<!--
|
||||
What could not be honoured, above the list rather than beside it. Every one of these is a way the
|
||||
import is quieter than the file — an ignored Match block, a dropped ProxyCommand — and a person
|
||||
comparing the two needs to be told before they conclude the parser lost something.
|
||||
What could not be honoured, at document level. Every one of these is a way the import is quieter than
|
||||
the file — an ignored Match block, a dropped ProxyCommand — and a person comparing the two needs to be
|
||||
told before they conclude the parser lost something.
|
||||
-->
|
||||
<Border Grid.Row="2" Margin="14,12,14,0" Padding="10,8" CornerRadius="4"
|
||||
<Border Grid.Row="2" Margin="0,14,0,0" Padding="14,10" CornerRadius="10"
|
||||
Background="{StaticResource WarnWash}" BorderBrush="{StaticResource WarnSoft}"
|
||||
BorderThickness="1" IsVisible="{Binding HasWarnings}">
|
||||
<ItemsControl ItemsSource="{Binding Warnings}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{Binding}" Foreground="{StaticResource WarnText}" FontSize="11"
|
||||
<TextBlock Text="{Binding}" Foreground="{StaticResource WarnText}" FontSize="11.5"
|
||||
TextWrapping="Wrap" Margin="0,2" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="3" RowDefinitions="Auto,*" Margin="0,12,0,0" IsVisible="{Binding HasRows}">
|
||||
<!-- ============ THE TABLE ============ -->
|
||||
<Border Grid.Row="3" Margin="0,18,0,0" CornerRadius="12" Background="{StaticResource Pane}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1" ClipToBounds="True"
|
||||
IsVisible="{Binding HasRows}">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<Grid Grid.Row="0" ColumnDefinitions="34,1.1*,1.4*,1.6*,96" Margin="14,0,14,6">
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="ADDRESS" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="AUTHENTICATION" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" />
|
||||
<Border Grid.Row="0" Padding="24,14,24,10" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="24,1*,1.4*,0.7*,0.5*,1.3*">
|
||||
<!--
|
||||
The header tick-all box. A plain Button rather than a CheckBox — ImportRowViewModel's own
|
||||
ToggleAllCommand is "tick everything, or untick everything" in one press, which is a command
|
||||
rather than a two-way bound bool, and a CheckBox has no Command of its own to hang that on.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="flat" Width="18" Height="18" Padding="0"
|
||||
VerticalAlignment="Center" Command="{Binding ToggleAllCommand}"
|
||||
ToolTip.Tip="Tick or untick every row">
|
||||
<Panel Width="18" Height="18">
|
||||
<Border CornerRadius="5" Background="{StaticResource Accent}" IsVisible="{Binding AllTicked}">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="13"
|
||||
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Border CornerRadius="5" BorderBrush="{StaticResource BorderMid}" BorderThickness="1.5"
|
||||
IsVisible="{Binding !AllTicked}" />
|
||||
</Panel>
|
||||
</Button>
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="ALIAS" FontSize="9.5" LetterSpacing="1"
|
||||
Margin="14,0,0,0" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="HOSTNAME" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text="USER" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="4" Classes="label" Text="PORT" FontSize="9.5" LetterSpacing="1" />
|
||||
<TextBlock Grid.Column="5" Classes="label" Text="WHAT THIS MEANS" FontSize="9.5" LetterSpacing="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ItemsControl ItemsSource="{Binding Rows}" Margin="12,8">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ImportRowViewModel">
|
||||
<Border Classes="importrow" CornerRadius="8">
|
||||
<Grid ColumnDefinitions="24,1*,1.4*,0.7*,0.5*,1.3*" Margin="12" MinHeight="20">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Alias}" FontSize="12.5"
|
||||
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="14,0,10,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
|
||||
ToolTip.Tip="{Binding Authentication}" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Hostname}" FontSize="11.5"
|
||||
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding User}" FontSize="11.5"
|
||||
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Port}" FontSize="11.5"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="5" Classes="meaningchip" CornerRadius="5" Padding="8,3"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
Classes.new="{Binding IsMeaningNew}" Classes.exists="{Binding IsMeaningExisting}"
|
||||
Classes.warn="{Binding IsMeaningWarned}" ToolTip.Tip="{Binding Meaning}">
|
||||
<TextBlock Text="{Binding Meaning}" FontSize="10" FontWeight="SemiBold"
|
||||
MaxWidth="230" TextTrimming="CharacterEllipsis" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ FOOTER ============ -->
|
||||
<StackPanel Grid.Row="4" Margin="0,16,0,0" Spacing="16" IsVisible="{Binding HasRows}">
|
||||
|
||||
<!--
|
||||
Said before the button, not after. Whether the key material comes with the host is the difference
|
||||
between a bookmark that connects and one that asks for a password, and somebody who is not told
|
||||
will conclude the import was broken.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap" IsVisible="{Binding !ImportsKeys}"
|
||||
Text="Key files are not read. Where ssh_config names an IdentityFile the path is recorded as a note, and the host asks for a password until you bind it to a key in your keychain." />
|
||||
|
||||
<!--
|
||||
◆ THE OPT-IN CARD. Hidden entirely where the scan found no IdentityFile anywhere — an offer to read
|
||||
~/.ssh on a screen where it would read nothing is a control that teaches people to ignore it.
|
||||
-->
|
||||
<Border CornerRadius="12" Background="{StaticResource Raised}" BorderBrush="{StaticResource WarnSoft}"
|
||||
BorderThickness="1" Padding="18,16" IsVisible="{Binding HasKeyFiles}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="14">
|
||||
<CheckBox VerticalAlignment="Top" Margin="0,3,0,0" IsChecked="{Binding ImportsKeys}" />
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Also store the private keys these entries point at" FontSize="13.5"
|
||||
FontWeight="SemiBold" Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="hint" FontSize="11.5" LineHeight="17.5" TextWrapping="Wrap"
|
||||
Text="{Binding KeyMaterialIntro}" />
|
||||
<!--
|
||||
The warning sentence appears only when the tick is on, and it is the one place this application
|
||||
says out loud that it is about to open private keys.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" IsVisible="{Binding ImportsKeys}"
|
||||
Foreground="{StaticResource WarnText}"
|
||||
Text="Pressing IMPORT will read each host's first IdentityFile out of ~/.ssh, store it in this vault encrypted, and bind the host to it. One key is stored per file however many hosts name it, and a file already in your keychain is bound to rather than stored twice. A key protected by a passphrase comes in without one — nothing on disk says what it is — and the report below will name it." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
◆ What became of each key file, after the fact. Capped with a scroll viewer because a config with
|
||||
thirty keyed hosts would otherwise push IMPORT off the window — the one control this screen must
|
||||
never lose.
|
||||
-->
|
||||
<Border IsVisible="{Binding HasKeyReport}" Padding="12,10" CornerRadius="10"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1">
|
||||
<ScrollViewer MaxHeight="120">
|
||||
<ItemsControl ItemsSource="{Binding KeyReport}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{Binding}" Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,2" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- "N of M entries selected · saving to {vault}", Cancel, Import N hosts. -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="hint" FontSize="12" VerticalAlignment="Center"
|
||||
Text="{Binding SelectionSummary}" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
|
||||
<Button Classes="ghost" Height="44" Content="CANCEL" Command="{Binding CancelCommand}"
|
||||
ToolTip.Tip="Back to Preferences. Nothing is stored." />
|
||||
<Button Classes="accent" Height="44" Content="{Binding ImportLabel}" Command="{Binding ImportCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ImportRowViewModel">
|
||||
<StackPanel Margin="14,0">
|
||||
<Grid ColumnDefinitions="34,1.1*,1.4*,1.6*,96" Margin="0,7">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Alias}" FontSize="12"
|
||||
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Address}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Authentication}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
|
||||
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="4" Classes="chip" HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasBadge}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
||||
</Border>
|
||||
</Grid>
|
||||
<TextBlock Classes="hint" Text="{Binding Warnings}" FontSize="10.5" Margin="34,0,0,8"
|
||||
TextWrapping="Wrap" Foreground="{StaticResource WarnText}"
|
||||
IsVisible="{Binding HasWarnings}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="4" Padding="14,10" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding HasRows}">
|
||||
<StackPanel Spacing="8">
|
||||
<!--
|
||||
Said before the button, not after. Whether the key material comes with the host is the difference
|
||||
between a bookmark that connects and one that asks for a password, and somebody who is not told
|
||||
will conclude the import was broken.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !ImportsKeys}"
|
||||
Text="Key files are not read. Where ssh_config names an IdentityFile the path is recorded as a note, and the host asks for a password until you bind it to a key in your keychain." />
|
||||
|
||||
<!--
|
||||
◆ THE TICK. Hidden entirely where the scan found no IdentityFile anywhere — an offer to read ~/.ssh
|
||||
on a screen where it would read nothing is a control that teaches people to ignore it.
|
||||
|
||||
The warning sentence appears only when it is on, and it is the one place this application says out
|
||||
loud that it is about to open private keys. It names the directory rather than saying "your keys",
|
||||
because what somebody is agreeing to is a read of that directory.
|
||||
-->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding HasKeyFiles}">
|
||||
<CheckBox IsChecked="{Binding ImportsKeys}">
|
||||
<TextBlock Classes="mono" FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="Also import the private keys these hosts point at" />
|
||||
</CheckBox>
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
IsVisible="{Binding ImportsKeys}" Foreground="{StaticResource WarnText}"
|
||||
Text="Pressing IMPORT will read each host's first IdentityFile out of ~/.ssh, store it in this vault encrypted, and bind the host to it. One key is stored per file however many hosts name it, and a file already in your keychain is bound to rather than stored twice. A key protected by a passphrase comes in without one — nothing on disk says what it is — and the report below will name it." />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Content="{Binding ImportLabel}" Command="{Binding ImportCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="TICK ALL / NONE" Command="{Binding ToggleAllCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
◆ What became of each key file, after the fact. Below the button because it is the answer rather
|
||||
than the offer, and capped with a scroll viewer because a config with thirty keyed hosts would
|
||||
otherwise push IMPORT off the window — the one control this screen must never lose.
|
||||
-->
|
||||
<Border IsVisible="{Binding HasKeyReport}" Padding="10,8" CornerRadius="4"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer MaxHeight="120">
|
||||
<ItemsControl ItemsSource="{Binding KeyReport}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<TextBlock Text="{Binding}" Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Margin="0,2" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
@@ -10,7 +9,11 @@ namespace DodoSSH.Client.App.Views;
|
||||
/// Importing hosts from <c>~/.ssh/config</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A task rather than a destination, which is why it is reached from preferences and not from the nav rail.
|
||||
/// A task rather than a destination, which is why it is reached from Preferences rather than from the nav
|
||||
/// rail. v5c-3 moved it inside settings mode as an overlay over the Preferences page — it no longer has a
|
||||
/// keyboard target of its own to hand back: <c>MainWindow.axaml.cs</c>'s <c>KeyboardHome</c> falls back to
|
||||
/// the window for settings mode as a whole, the same way it already does for the account and logs screens,
|
||||
/// so a <c>KeyboardTarget</c> property here would be dead code nothing reads.
|
||||
/// </remarks>
|
||||
internal sealed partial class ImportScreen : UserControl
|
||||
{
|
||||
@@ -24,9 +27,6 @@ internal sealed partial class ImportScreen : UserControl
|
||||
AddHandler(ToggleButton.IsCheckedChangedEvent, OnTickChanged, RoutingStrategies.Bubble);
|
||||
}
|
||||
|
||||
/// <summary>Where the keyboard lands when this screen is the one showing.</summary>
|
||||
internal IInputElement KeyboardTarget => this;
|
||||
|
||||
private void OnTickChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is ImportViewModel import)
|
||||
|
||||
@@ -91,12 +91,13 @@
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<views:TitleBar Grid.Row="0" />
|
||||
<views:TitleBar Grid.Row="0" IsVisible="{Binding !IsSettingsMode}" />
|
||||
|
||||
<Panel Grid.Row="1">
|
||||
|
||||
<!-- The unlocked application. -->
|
||||
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding IsUnlocked}">
|
||||
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding IsUnlocked}"
|
||||
IsEnabled="{Binding !IsSettingsMode}">
|
||||
|
||||
<!--
|
||||
The window's own furniture, at 255 pixels — see NavRail.axaml's own v5b remark. It no longer sits
|
||||
@@ -176,13 +177,19 @@
|
||||
by a toggle inside the screen — see ShowFiles, and the rail's own switcher segment that
|
||||
calls it now; see NavRail.axaml.
|
||||
|
||||
Deliberately not given the session shell above: a bucket is not a host, has no terminal tab
|
||||
Deliberately not given the full session shell above: a bucket is not a host, has no terminal tab
|
||||
to be the other end of a cross-surface button, and pins nothing the sidebar's QUICK ACCESS
|
||||
could show. Wave B's own scope is the terminal and SFTP surfaces; S3 stays the plain screen it
|
||||
already was, and this is the recorded reason rather than an oversight.
|
||||
could show — so no tab row, no host header, no status bar and no sidebar. Wave B's own scope
|
||||
was the terminal and SFTP surfaces and left this plain; v5c-3 gives it the session shell's own
|
||||
LOOK without its machinery — a 26px padded column and the same bordered, radius-12 container —
|
||||
since design-notes/v5c-fidelity-notes.md calls for the fidelity pass here too, and there is
|
||||
nothing this screen does that needs the parts still withheld.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsBucketsScreen}">
|
||||
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
||||
<Border Margin="26" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
|
||||
ClipToBounds="True">
|
||||
<views:TransfersScreen DataContext="{Binding Transfers}" />
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- ============ KEYCHAIN ============ -->
|
||||
@@ -217,30 +224,21 @@
|
||||
<views:LogsScreen x:Name="LogsPane" DataContext="{Binding LogsScreen}" />
|
||||
</Panel>
|
||||
|
||||
<!-- ============ VAULTS ============ -->
|
||||
<!--
|
||||
Wrapped, for the reason the keychain and transfers screens are: the visibility is the shell's
|
||||
business and the data context is the vaults view model, and both on one element would resolve
|
||||
IsVaultsScreen against a type that does not have it.
|
||||
v5c-2: the VAULTS panel that used to live here is gone. ShellScreen.Vaults now only ever shows
|
||||
through settings mode's own SettingsVaultsPage — see MainWindowViewModel.ShowScreen, which
|
||||
redirects it into EnterSettings before Screen is ever set — so IsVaultsScreen being true also
|
||||
means IsSettingsMode is true, and this whole page-area Panel (bound to !IsSettingsMode, above)
|
||||
is already hidden behind SettingsView by then. A Panel here would never have been drawn.
|
||||
|
||||
Bound to Vaults, which is the vaults themselves and the people in them — not to Vault, which
|
||||
is one vault's contents and is what the keychain and hosts screens above draw.
|
||||
v5c-3: IMPORT left this Panel the same way, and for the same reason. Import.dc.html draws the
|
||||
importer inside the settings chrome — SettingsNav lit on Preferences, a "Back to preferences"
|
||||
titlebar rather than the ordinary rail and titlebar this Panel belongs to — so ImportScreen now
|
||||
lives in SettingsView.axaml behind MainWindowViewModel.IsImportOpen, beside the seven settings
|
||||
pages rather than among the screens here. See ShowScreen's own translation of
|
||||
ShellScreen.Import for what still reaches it: the Preferences page's own "OPEN IMPORTER" row,
|
||||
and anything else that names the same destination.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsVaultsScreen}">
|
||||
<views:VaultsScreen DataContext="{Binding Vaults}" />
|
||||
</Panel>
|
||||
|
||||
<!-- ============ PREFERENCES ============ -->
|
||||
<views:PreferencesScreen IsVisible="{Binding IsPreferencesScreen}" />
|
||||
|
||||
<!-- ============ IMPORT ============ -->
|
||||
<!--
|
||||
Reached from preferences rather than from the rail; see ShellScreen.Import. Wrapped, like
|
||||
the others whose data context is their own view model.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsImportScreen}">
|
||||
<views:ImportScreen x:Name="ImportPane" DataContext="{Binding ImportScreen}" />
|
||||
</Panel>
|
||||
|
||||
</Panel>
|
||||
|
||||
@@ -319,6 +317,22 @@
|
||||
</Panel>
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
v5c: the settings mode, which replaces the titlebar above and everything in this Panel below it with
|
||||
its own — see SettingsView.axaml and TitleBar's own IsVisible next to it in this file. Later in the
|
||||
Panel than the unlocked Grid, on the same reasoning the setup Border below gives for its own
|
||||
position: Avalonia z-order draws a later sibling over an earlier one, and SettingsView's own
|
||||
Background is opaque, so it covers the rail and the page area completely rather than needing them
|
||||
hidden out from under it. The unlocked Grid keeps IsEnabled bound to the negation above so a Tab
|
||||
press cannot walk keyboard focus into a control this mode has covered.
|
||||
|
||||
Earlier than the palette and the host-key decision, both further down this same Panel, so either can
|
||||
still interrupt settings mode exactly as it already interrupts every other screen — a host key that
|
||||
changes while the sync loop is running does not stop being worth answering because the window
|
||||
happens to be showing Settings.
|
||||
-->
|
||||
<views:SettingsView IsVisible="{Binding IsSettingsMode}" />
|
||||
|
||||
<!--
|
||||
Setup and unlock. Later in the Panel, so it is above the application content in Avalonia's z-order —
|
||||
which covers Avalonia-drawn content and nothing else. The terminal is collapsed rather than covered;
|
||||
@@ -476,11 +490,18 @@
|
||||
no DataContext yields UnsetValue, IsVisible falls back to true, and the previewer would show a banner
|
||||
announcing an update that does not exist.
|
||||
-->
|
||||
<views:UpdateBanner Grid.Row="2"
|
||||
DataContext="{Binding Updates}"
|
||||
IsVisible="{Binding IsBannerShowing, FallbackValue=False}" />
|
||||
<!--
|
||||
v5c: wrapped rather than given a third condition of its own — UpdateBanner sets its own DataContext to
|
||||
UpdateViewModel on this same element, so a binding against IsSettingsMode has to live on an ancestor
|
||||
whose context is still the shell. The design's settings titlebar has no room for this strip; hiding it
|
||||
here rather than teaching the banner about a mode it otherwise has no reason to know about.
|
||||
-->
|
||||
<Panel Grid.Row="2" IsVisible="{Binding !IsSettingsMode}">
|
||||
<views:UpdateBanner DataContext="{Binding Updates}"
|
||||
IsVisible="{Binding IsBannerShowing, FallbackValue=False}" />
|
||||
</Panel>
|
||||
|
||||
<views:StatusBar Grid.Row="3" />
|
||||
<views:StatusBar Grid.Row="3" IsVisible="{Binding !IsSettingsMode}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -111,11 +111,13 @@ internal sealed partial class MainWindow : Window
|
||||
/// </remarks>
|
||||
private IInputElement KeyboardHome => shell switch
|
||||
{
|
||||
// v5c: settings mode has no keyboard-focused control of its own yet — its pages are read-only prose
|
||||
// and buttons, the same shape the account and logs screens already fall back to the window for.
|
||||
{ IsSettingsMode: true } => this,
|
||||
{ IsTerminalShowing: true } => Terminal,
|
||||
{ Screen: ShellScreen.Keychain } => VaultPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Hosts } => HostsPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.KnownHosts } => PinsPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Import } => ImportPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Snippets } => SnippetsPane.KeyboardTarget,
|
||||
{ Screen: ShellScreen.Logs } => LogsPane.KeyboardTarget,
|
||||
_ => this,
|
||||
@@ -213,6 +215,25 @@ internal sealed partial class MainWindow : Window
|
||||
{
|
||||
Palette.HandleKey(e);
|
||||
}
|
||||
// v5c: Escape leaves settings mode, the same full-window-state idiom the palette's own Escape
|
||||
// already follows one branch up. Checked after the palette rather than before it: the two states
|
||||
// are mutually exclusive in practice — opening the palette does not enter settings mode and entering
|
||||
// settings does not open the palette — but an Escape while both were somehow true should close the
|
||||
// thing drawn on top, which is the palette.
|
||||
//
|
||||
// v5c: with the importer up, Escape closes only that — the same "closest thing first" rule, and the
|
||||
// same one the titlebar's own back button follows by showing "Back to preferences" rather than
|
||||
// "Back to application" while IsImportOpen is true.
|
||||
else if (e.Key == Key.Escape && viewModel.IsImportOpen)
|
||||
{
|
||||
viewModel.CloseImportCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape && viewModel.IsSettingsMode)
|
||||
{
|
||||
viewModel.LeaveSettingsCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
@@ -249,12 +249,15 @@
|
||||
<Border Height="1" Background="{StaticResource BorderMid}" />
|
||||
|
||||
<!--
|
||||
Settings and Preferences both land on the one screen this application actually has — the
|
||||
mock's own Settings area is a family of screens that do not exist yet; see the decided
|
||||
deviation in design-notes/v5b-fidelity-notes.md. Two doors to one room rather than one door
|
||||
removed, so neither word on the mock goes unanswered.
|
||||
v5c: Settings, Vaults and Preferences now each land on their own page of the settings mode —
|
||||
see MainWindowViewModel.EnterSettings and SettingsView.axaml. Settings opens on General, the
|
||||
mode's own default landing page; Vaults and Preferences open directly on the page they name,
|
||||
which is also what anything that used to navigate to ShellScreen.Vaults or
|
||||
ShellScreen.Preferences now does — see ShowScreen. Three handlers rather than the two rows
|
||||
sharing one before this wave: the mock's Settings area was a family of screens that did not
|
||||
exist yet, and now that it does, "Settings" and "Preferences" are no longer the same click.
|
||||
-->
|
||||
<Button Classes="poprow" Click="OnPopoverPreferencesPressed">
|
||||
<Button Classes="poprow" Click="OnPopoverSettingsPressed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="12"
|
||||
Foreground="{StaticResource Text}" />
|
||||
@@ -282,8 +285,8 @@
|
||||
|
||||
<!--
|
||||
The existing sign-out flow, with its own confirm card — see
|
||||
MainWindowViewModel.SignOutFromPopover for why this goes through Preferences rather than
|
||||
calling SignOutCommand directly from wherever the popover happened to be opened.
|
||||
MainWindowViewModel.SignOutFromPopover for why this goes through the Account settings page
|
||||
rather than calling SignOutCommand directly from wherever the popover happened to be opened.
|
||||
-->
|
||||
<Button Classes="poprow" Click="OnPopoverLogoutPressed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
|
||||
@@ -48,30 +48,42 @@ internal sealed partial class NavRail : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Settings and Preferences both land here — see the remark in the markup.</summary>
|
||||
/// <summary>Opens settings mode on its default landing page — see the remark in the markup.</summary>
|
||||
private void OnPopoverSettingsPressed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ClosePopover();
|
||||
|
||||
if (DataContext is MainWindowViewModel shell)
|
||||
{
|
||||
shell.EnterSettingsCommand.Execute(SettingsPage.General);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Opens settings mode on its Preferences page.</summary>
|
||||
private void OnPopoverPreferencesPressed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ClosePopover();
|
||||
|
||||
if (DataContext is MainWindowViewModel shell)
|
||||
{
|
||||
shell.ShowScreenCommand.Execute(ShellScreen.Preferences);
|
||||
shell.EnterSettingsCommand.Execute(SettingsPage.Preferences);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Opens settings mode on its Vaults page.</summary>
|
||||
private void OnPopoverVaultsPressed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ClosePopover();
|
||||
|
||||
if (DataContext is MainWindowViewModel shell)
|
||||
{
|
||||
shell.ShowScreenCommand.Execute(ShellScreen.Vaults);
|
||||
shell.EnterSettingsCommand.Execute(SettingsPage.Vaults);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a sign-out, through Preferences so the confirmation card has somewhere to be seen — see
|
||||
/// <see cref="MainWindowViewModel.SignOutFromPopover"/>.
|
||||
/// Starts a sign-out, through the Account settings page so the confirmation card has somewhere to be
|
||||
/// seen — see <see cref="MainWindowViewModel.SignOutFromPopover"/>.
|
||||
/// </summary>
|
||||
private void OnPopoverLogoutPressed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
<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.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.PreferencesScreen"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
Preferences.
|
||||
|
||||
The design's rail has six sections and its TERMINAL panel has six settings. One of those six is now
|
||||
real: text size. It needed all three of the things this comment used to record as missing — somewhere
|
||||
to keep a preference (settings.json beside the cache, outside it deliberately, so it can be read on a
|
||||
launch that never unlocks anything), a frame carrying a terminal option (TerminalServerOpcode.FontSize),
|
||||
and a way for the page's own chords to reach the host that owns the value
|
||||
(TerminalClientOpcode.FontSizeStep). The rule that kept it off this screen until then still stands and
|
||||
is why the storage came first: a stepper that reset on every launch is worse than no stepper.
|
||||
|
||||
So this screen ships what is real, which is not nothing: this machine's device key is a genuine
|
||||
preference with a genuine effect, and it is the one thing on the design's SECURITY panel that exists.
|
||||
The two commands behind it were already in the shell; they were merely homeless, wedged into the old
|
||||
account bar because there was nowhere else to put them.
|
||||
|
||||
Everything else is listed as absent rather than omitted, because a preferences screen that is silent
|
||||
about the settings it has not got reads as a product with six preferences.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="620" Margin="28,26" HorizontalAlignment="Left">
|
||||
|
||||
<TextBlock Classes="mono" Text="THIS MACHINE" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Unlock with Windows Hello" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Registers this machine so a later launch can open the keychain with a Windows confirmation instead of your passphrase. Your passphrase keeps working." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Content="REGISTER"
|
||||
Command="{Binding RegisterDeviceCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
IsVisible="{Binding CanRegisterDevice}" />
|
||||
<!--
|
||||
The withdrawal, in the place the offer was. Its own flag rather than the negation of that one: a
|
||||
machine with no TPM and a machine that is already registered are both "cannot register", and only
|
||||
the second has anything to take back.
|
||||
-->
|
||||
<Button Grid.Column="1" Classes="danger" Content="STOP UNLOCKING HERE"
|
||||
Command="{Binding ForgetDeviceCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
IsVisible="{Binding CanForgetDevice}"
|
||||
ToolTip.Tip="Withdraws this machine's device key, here and from your account, so it goes back to asking for your passphrase. Do this to a machine you have lost." />
|
||||
</Grid>
|
||||
|
||||
<!-- Neither flag is set on a machine that cannot keep a key at all, and that is worth saying. -->
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||||
Text="This machine has nowhere to keep a device key, so the keychain will keep asking for your passphrase. That needs a TPM and a Windows keystore willing to release the key."
|
||||
IsVisible="{Binding HasNoDeviceKeyOption}" />
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="UPDATES" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<!--
|
||||
Not on the design at all, unlike everything else here. It arrived with packaging: an installed
|
||||
client can replace itself, and the moment that is true the question of where a replacement comes
|
||||
from stops being theoretical. The answer is the security content of this section rather than a
|
||||
footnote to it, which is why it is printed under the version instead of hidden in a tooltip.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding Updates.CurrentVersion}" FontSize="12" Margin="0,8,0,0"
|
||||
Foreground="{StaticResource Info}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,4,0,0"
|
||||
Text="Builds come from the project's own release page, and never from the server you sign in to. That is deliberate: whoever hands you the client can hand you a client that copies your passphrase, and the operator of a DodoSSH deployment is the party the trust model is about. A deployment may tell you where to get it. It is not where it comes from." />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Check for updates" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Asks the release page whether there is a newer build, and downloads it if there is. Nothing is ever installed while you are using it — a downloaded update waits for a restart you ask for, or for the next time you start DodoSSH." />
|
||||
</StackPanel>
|
||||
<Button x:Name="CheckNowButton" Grid.Column="1" Classes="ghost" Content="CHECK NOW"
|
||||
Command="{Binding Updates.CheckNowCommand}"
|
||||
IsEnabled="{Binding Updates.CanCheckNow}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Check on its own" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Every six hours while DodoSSH is running, starting a couple of minutes after launch. It keeps checking while the keychain is locked, because where builds come from has nothing to do with your vault." />
|
||||
</StackPanel>
|
||||
<CheckBox x:Name="AutomaticUpdatesToggle" Grid.Column="1" VerticalAlignment="Top"
|
||||
IsChecked="{Binding Updates.IsAutomatic}"
|
||||
IsEnabled="{Binding Updates.IsSupported}" />
|
||||
</Grid>
|
||||
|
||||
<!-- The only other ProgressBar in the application is the transfers one; same height, same brushes. -->
|
||||
<ProgressBar Height="4" Minimum="0" Maximum="100" Margin="0,12,0,0"
|
||||
Value="{Binding Updates.DownloadPercent}"
|
||||
Foreground="{StaticResource Accent}" Background="{StaticResource Raised}"
|
||||
IsVisible="{Binding Updates.IsDownloading}" />
|
||||
|
||||
<!--
|
||||
The restart, with the sentence the banner only has room for in a tooltip. This screen scrolls, so
|
||||
this is where the warning can be as long as it needs to be — and it needs to be, because this
|
||||
application has spent a lot of words teaching that locking keeps shells running.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0" IsVisible="{Binding Updates.IsReady}">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="{Binding Updates.ReadyHeadline}" Foreground="{StaticResource Text}"
|
||||
FontSize="13" FontWeight="Medium" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding Updates.RestartWarning}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Content="RESTART NOW"
|
||||
Command="{Binding Updates.RestartNowCommand}" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||||
Text="{Binding Updates.Status}"
|
||||
IsVisible="{Binding Updates.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<!--
|
||||
The HasNoDeviceKeyOption precedent, one section up: a machine that gets none of the above is told
|
||||
why rather than shown three controls that cannot do anything.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||||
Text="This copy of DodoSSH cannot replace itself, so none of the above does anything. That is what a build run from a source checkout looks like, and also what a copy somebody unzipped by hand looks like — it is the installer that registers the update path."
|
||||
IsVisible="{Binding Updates.IsUnsupported}" />
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="TERMINAL" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<!--
|
||||
Here as well as on the chord, and not because the chord is in doubt. Ctrl+plus can only be heard
|
||||
while a terminal has focus, since that is where the keyboard is being read — so somebody who has
|
||||
not opened one yet, or who has just made the text too small to find anything in, has nowhere else
|
||||
to look. This is that place, and it names the chord so the screen teaches it rather than replacing
|
||||
it.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Text size" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="How large a terminal draws, in pixels. Ctrl+plus and Ctrl+minus do the same while a terminal has focus, and Ctrl+0 puts it back. It resizes the grid rather than magnifying it, so the remote is told how many columns it now has — which is also why it stops before the columns run out." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" VerticalAlignment="Top">
|
||||
<Button Classes="ghost" Content="A−" Command="{Binding ShrinkTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanShrinkTerminalFont}"
|
||||
ToolTip.Tip="Smaller · Ctrl+minus" />
|
||||
<TextBlock Classes="mono" FontSize="13" MinWidth="26" VerticalAlignment="Center"
|
||||
TextAlignment="Center" Foreground="{StaticResource Text}"
|
||||
Text="{Binding TerminalFontSize}" />
|
||||
<Button Classes="ghost" Content="A+" Command="{Binding EnlargeTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanEnlargeTerminalFont}"
|
||||
ToolTip.Tip="Larger · Ctrl+plus" />
|
||||
<Button Classes="ghost" Content="RESET" Command="{Binding ResetTerminalFontCommand}"
|
||||
ToolTip.Tip="Back to the size it ships at · Ctrl+0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="KEYCHAIN" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Lock the keychain" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Closes the keychain and forgets every key it held. Shells you have open keep running and reappear when you unlock — locked describes the keychain, not this machine's access to your hosts." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Content="LOCK NOW" Command="{Binding LockCommand}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Synchronise" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Runs a pass now. One runs on its own when the keychain opens, straight after any change, and every minute while it stays open — and a pass that finds this machine offline signs it back in from the session it remembered, so nothing here depends on being pressed." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="ghost" Content="SIGN IN" Command="{Binding SignInCommand}"
|
||||
IsVisible="{Binding !IsOnline}"
|
||||
ToolTip.Tip="Opens your browser. Only needed when there is no remembered session to resume — after signing out, or once your identity provider stops accepting the one this machine held." />
|
||||
<Button Classes="ghost" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Import from ~/.ssh/config" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Reads this machine's OpenSSH configuration and offers what it finds. It shows you the list first and stores nothing until you say so, and it does not read any private key — where a key file is named, the path is recorded as a note." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Content="IMPORT HOSTS"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Import}" />
|
||||
</Grid>
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="ACCOUNT" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||||
|
||||
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="12" Margin="0,8,0,0"
|
||||
Foreground="{StaticResource Info}" TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock Text="Sign out of this machine" Foreground="{StaticResource Text}" FontSize="13"
|
||||
FontWeight="Medium" />
|
||||
<TextBlock Classes="hint" FontSize="11"
|
||||
Text="Deletes this machine's copy of the keychain and withdraws its device key, so it goes back to knowing nothing. The keychain stays on the server; signing in again brings it back. Use this to hand a machine on, or to enrol a different account." />
|
||||
</StackPanel>
|
||||
<!--
|
||||
Hidden rather than disabled while the confirmation is up, because the card below carries the
|
||||
button that actually does it and two sign-out buttons on one screen is one too many.
|
||||
-->
|
||||
<Button Grid.Column="1" Classes="danger" Content="SIGN OUT"
|
||||
Command="{Binding SignOutCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
IsVisible="{Binding !IsConfirmingSignOut}" />
|
||||
</Grid>
|
||||
|
||||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14" Margin="0,14,0,0"
|
||||
IsVisible="{Binding IsConfirmingSignOut}">
|
||||
<views:SignOutCard />
|
||||
</Border>
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||||
|
||||
<TextBlock Classes="mono" Text="NOT BUILT YET" FontSize="14" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource TextDim}" />
|
||||
<TextBlock Classes="hint" FontSize="12" Margin="0,8,0,0"
|
||||
Text="These are on the design and have nothing behind them. They are listed rather than left out, so that what this screen does not do is as legible as what it does. The full list, and what each would take, is in docs/design-import-gaps.md." />
|
||||
|
||||
<ItemsControl Margin="0,12,0,0">
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="TextBlock.gap">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="Margin" Value="0,0,0,7" />
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
<TextBlock Classes="gap"
|
||||
Text="Terminal font, cursor and scrollback — the renderer hard-codes those three. The text size above is the one that is not, and it is the path the others would follow if they were worth a row here." />
|
||||
<TextBlock Classes="gap"
|
||||
Text="Changing channel from in here — there is a nightly as well as a release, but which one a copy follows is fixed when it is built, so moving between them means installing the other one." />
|
||||
<TextBlock Classes="gap"
|
||||
Text="Auto-lock after idle — nothing tracks idleness, and the lock policy would have to decide what to do about a shell mid-job." />
|
||||
<TextBlock Classes="gap"
|
||||
Text="Per-use approval before a key signs — keys are handed to the SSH stack whole at connect time, so there is no per-signature moment to interrupt." />
|
||||
<TextBlock Classes="gap"
|
||||
Text="SSO and organisation policy — the server has endpoints for membership and none for policy, so there is nothing for this screen to show." />
|
||||
<TextBlock Classes="gap"
|
||||
Text="Keyboard shortcuts — the window binds one chord, and the terminal keeps the rest for the remote." />
|
||||
</ItemsControl>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -1,9 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Preferences: what this build can actually change, and a list of what it cannot.</summary>
|
||||
internal sealed partial class PreferencesScreen : UserControl
|
||||
{
|
||||
public PreferencesScreen() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<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.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsAccountPage"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: Account, against Settings-Account.dc.html.
|
||||
|
||||
The profile card is read-only, unlike the design's own — there is no "Edit profile" row here because
|
||||
there is no endpoint to send an edit to. Name, email and avatar all come from MeResponse and nothing in
|
||||
this client can change any of the three.
|
||||
|
||||
The SIGN-IN card keeps one row rather than the design's two. The issuer sentence is real:
|
||||
MainWindowViewModel.Issuer is MeResponse.Issuer, cached the same turn AccountName and Email are and
|
||||
surfaced here for the first time — see the remark on that property. "Master passphrase" and its "Change
|
||||
passphrase" button are refused outright: there is no change-passphrase flow, only the one enrollment-time
|
||||
choice, and a button that opened nothing would be worse than no button.
|
||||
|
||||
DEVICES and "Sign out everywhere" are both refused. There is no list-devices endpoint and no
|
||||
sign-out-everywhere endpoint; the one honest device fact this client has — this machine's own Windows
|
||||
Hello registration — is drawn on the Security page, not duplicated here as a one-row list.
|
||||
|
||||
SignOutCard is the same control the old PreferencesScreen used, restyled into this page's card idiom
|
||||
rather than rewritten — see that control's own remark on why it is a bare StackPanel and not a card of
|
||||
its own, and why one control serves both this screen and the unlock screen's copy of it.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock Classes="settingstitle" Text="Account" />
|
||||
|
||||
<Border Classes="settingscard" Margin="0,26,0,0" Padding="24,22">
|
||||
<StackPanel Orientation="Horizontal" Spacing="18">
|
||||
<Border Width="64" Height="64" CornerRadius="60" Background="{StaticResource AvatarGradient}">
|
||||
<TextBlock Text="{Binding AvatarInitials}" FontWeight="Bold" FontSize="20" LetterSpacing="0.5"
|
||||
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<StackPanel Spacing="7" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding AccountName}" FontSize="20" FontWeight="Bold" LetterSpacing="-0.25"
|
||||
Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="mono" FontSize="12.5" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding Email}"
|
||||
IsVisible="{Binding Email, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="SIGN-IN" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel>
|
||||
<Border Classes="settingsrow last">
|
||||
<StackPanel Spacing="6" MaxWidth="640">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Single sign-on" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Signing in proves who you are — it never decrypts a vault." />
|
||||
<TextBlock Classes="mono" FontSize="12" Margin="0,4,0,0" Foreground="{StaticResource Live}"
|
||||
Text="{Binding Issuer, StringFormat='You sign in through {0}.'}"
|
||||
IsVisible="{Binding Issuer, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="THIS MACHINE" />
|
||||
|
||||
<Border Classes="settingscard" Margin="0,12,0,40">
|
||||
<Panel>
|
||||
<Border Classes="settingsrow last" IsVisible="{Binding !IsConfirmingSignOut}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Sign out of this machine" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Deletes this machine's copy of the keychain and withdraws its device key, so it goes back to knowing nothing. The keychain stays on the server; signing in again brings it back. Use this to hand a machine on, or to enrol a different account." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="danger" Height="38" Content="SIGN OUT"
|
||||
Command="{Binding SignOutCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Padding="24,20" IsVisible="{Binding IsConfirmingSignOut}">
|
||||
<views:SignOutCard />
|
||||
</Border>
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Settings mode's Account page: the read-only profile, the sign-in fact, and signing out.</summary>
|
||||
internal sealed partial class SettingsAccountPage : UserControl
|
||||
{
|
||||
public SettingsAccountPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsGeneralPage"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: General, against Settings-General.dc.html.
|
||||
|
||||
The design's page has three cards — STARTUP, APPEARANCE, UPDATES — and this one has one. STARTUP
|
||||
(launch at login, reopen tabs) and APPEARANCE (theme, language) are refused outright: nothing tracks
|
||||
whether Windows starts this application, nothing remembers a tab list across a launch, only the one dark
|
||||
theme exists, and there is no i18n anywhere in the client. Building either card would be two rows of
|
||||
controls that toggle a field nothing reads. See design-notes/v5c-fidelity-notes.md.
|
||||
|
||||
UPDATES is the whole page's real content, moved here from the old PreferencesScreen.axaml verbatim —
|
||||
same UpdateViewModel, same states, same warnings, restyled into this page's row idiom. Its own
|
||||
update-channel switcher is refused the same way STARTUP and APPEARANCE are: which channel a copy follows
|
||||
is fixed when it is built, so this screen cannot offer to switch it.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock Classes="settingstitle" Text="General" />
|
||||
|
||||
<TextBlock Classes="settingssection" Text="UPDATES" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel>
|
||||
|
||||
<Border Classes="settingsrow">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Version" />
|
||||
<TextBlock Classes="mono" FontSize="12" Foreground="{StaticResource TextFaint}"
|
||||
TextTrimming="CharacterEllipsis" Text="{Binding Updates.CurrentVersion}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Height="38" Content="CHECK NOW"
|
||||
Command="{Binding Updates.CheckNowCommand}"
|
||||
IsEnabled="{Binding Updates.CanCheckNow}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingsrow">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Check on its own" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Every six hours while DodoSSH is running, starting a couple of minutes after launch. It keeps checking while the keychain is locked, because where builds come from has nothing to do with your vault." />
|
||||
</StackPanel>
|
||||
<CheckBox Grid.Column="1" VerticalAlignment="Top"
|
||||
IsChecked="{Binding Updates.IsAutomatic}"
|
||||
IsEnabled="{Binding Updates.IsSupported}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingsrow" IsVisible="{Binding Updates.IsDownloading}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Downloading…" />
|
||||
<ProgressBar Height="4" Minimum="0" Maximum="100"
|
||||
Value="{Binding Updates.DownloadPercent}"
|
||||
Foreground="{StaticResource Accent}" Background="{StaticResource Chip}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingsrow" IsVisible="{Binding Updates.IsReady}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="{Binding Updates.ReadyHeadline}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="settingsrowcaption" Text="{Binding Updates.RestartWarning}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Height="38" Content="RESTART NOW"
|
||||
Command="{Binding Updates.RestartNowCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingsrow last">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Builds come from the project's own release page, and never from the server you sign in to. Whoever hands you the client can hand you a client that copies your passphrase, and the operator of a DodoSSH deployment is the party the trust model is about. A deployment may tell you where to get it. It is not where it comes from." />
|
||||
<TextBlock Classes="settingsrowcaption" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding Updates.Status}"
|
||||
IsVisible="{Binding Updates.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="This copy of DodoSSH cannot replace itself, so nothing above does anything. That is what a build run from a source checkout looks like, and also what a copy somebody unzipped by hand looks like — it is the installer that registers the update path."
|
||||
IsVisible="{Binding Updates.IsUnsupported}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="NOT BUILT YET" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel Margin="24,20" Spacing="10">
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="These are on the design and have nothing behind them here. They are listed rather than left out, so what this page does not do is as legible as what it does. The full list, and what each would take, is in docs/design-import-gaps.md." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Update channel — there is a nightly as well as a release, but which one a copy follows is fixed when it is built, so moving between them means installing the other one." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Launch at login — nothing registers this application with Windows' own startup list." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Reopen tabs from last session — nothing remembers which tabs were open across a launch, and every connection would need to authenticate again regardless." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Theme and language — only the one dark theme exists, and there is no translation anywhere in this client." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Settings mode's General page: the Updates card, and the refused items as an essay.</summary>
|
||||
internal sealed partial class SettingsGeneralPage : UserControl
|
||||
{
|
||||
public SettingsGeneralPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<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.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsGroupsPage"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
x:Name="Root">
|
||||
|
||||
<!--
|
||||
v5c-2: Groups, against Settings-Groups.dc.html — a real management page over VaultViewModel's own group
|
||||
commands (Groups, NewGroup, EditGroup, DeleteGroup), given its own row here rather than only living inside
|
||||
the hosts board's sidebar. DataContext.Vault (VaultViewModel) is nullable — locked, or the settings mode
|
||||
reached before an unlock — so every binding below is {Binding Vault.*}, and the empty-vault caption covers
|
||||
the null case the same way the vaults page's own does.
|
||||
|
||||
── THE INTRO SENTENCE IS NOT THE MOCK'S ────────────────────────────────────────────────────────────────
|
||||
The design says "the order here is the order there" and draws drag handles to back it up. Neither is true:
|
||||
HostGroupRowViewModel — see VaultViewModel.RebuildGroups — orders groups by the vault new items go into
|
||||
first, then by vault name, then by label; there is no manual order to drag into and nothing this page
|
||||
could do would make one real. The sentence below says what is actually true instead, and there are no
|
||||
drag handles.
|
||||
|
||||
── EDIT AND DELETE ──────────────────────────────────────────────────────────────────────────────────────
|
||||
EditGroupCommand and DeleteGroupCommand already take the row as their argument — see
|
||||
VaultViewModel.EditGroup and VaultViewModel.EditGroupFromHeading's own remark on why — so this card's
|
||||
buttons bind straight to them with CommandParameter="{Binding}"; no wrapper commands were needed the way
|
||||
the tags page needs one. Editing opens the same drawer form the hosts board's own GROUPS section does
|
||||
(HostDrawer.axaml's IsEditingGroup panel); it is not reproduced here as a second form, on the reasoning
|
||||
the vaults page gives for reusing VaultsViewModel's commands rather than rebuilding them: one set of
|
||||
fields, one place they can drift from what SaveGroupCommand actually writes.
|
||||
|
||||
── THE "NO GROUP" FOOTER ────────────────────────────────────────────────────────────────────────────────
|
||||
Real, all three facts: VaultViewModel.UngroupedHostCount counts hosts whose group has gone or was never
|
||||
set, "always listed first" is true of both FlattenIntoSections call sites (RebuildSidebarRows passes
|
||||
ungroupedFirst: false — it is drawn last there — but the desktop's own board, RebuildHostSections, passes
|
||||
true), and it genuinely cannot be renamed or deleted: there is no HostGroupSecret behind it for either
|
||||
command to act on.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||
<TextBlock Classes="settingstitle" Text="Groups" />
|
||||
<Border Background="{StaticResource Chip}" CornerRadius="9" MinWidth="34" Height="30"
|
||||
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<TextBlock Text="{Binding Vault.Groups.Count}" FontSize="13.5" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextFaint}" Margin="8,0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Height="40" Content="+ NEW GROUP"
|
||||
Command="{Binding Vault.NewGroupCommand}"
|
||||
IsEnabled="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" Margin="0,12,0,0" TextWrapping="Wrap"
|
||||
Text="Groups order by which vault new items go into, then by vault, then by label — there is no manual order to set." />
|
||||
|
||||
<!-- The group editor, in place: HostDrawer's own IsEditingGroup form, restyled into this page's card
|
||||
idiom rather than duplicated. Opened by + NEW GROUP above or an EDIT icon below. -->
|
||||
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
|
||||
IsVisible="{Binding Vault.IsEditingGroup}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Classes="settingsrowtitle" Text="{Binding Vault.DrawerTitle}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="A heading for the hosts board, and the defaults every host under it inherits when it says nothing itself." />
|
||||
<TextBox PlaceholderText="group name" Text="{Binding Vault.GroupEditorLabel}" />
|
||||
<StackPanel Spacing="4" IsVisible="{Binding Vault.ShowsGroupEditorVaultChoice}">
|
||||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Vault.GroupEditorVaultChoices}"
|
||||
SelectedItem="{Binding Vault.GroupEditorSelectedVault}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
||||
<TextBlock Text="{Binding Display}" FontSize="12" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Vault.GroupEditorParentChoices}"
|
||||
SelectedItem="{Binding Vault.GroupEditorSelectedParent}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding Vault.GroupEditorDefaultPort}" Minimum="1"
|
||||
Maximum="65535" FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="default port" />
|
||||
<TextBox Grid.Column="2" Text="{Binding Vault.GroupEditorDefaultUsername}"
|
||||
PlaceholderText="default username" />
|
||||
</Grid>
|
||||
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Vault.GroupEditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding Vault.GroupEditorSelectedAuthentication}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Height="36" Content="{Binding Vault.GroupSaveLabel}"
|
||||
Command="{Binding Vault.SaveGroupCommand}" />
|
||||
<Button Classes="ghost" Height="36" Content="CANCEL"
|
||||
Command="{Binding Vault.CancelGroupEditCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- The delete confirmation, in place. The same control the hosts board's GROUPS section and the
|
||||
vaults page both reuse. -->
|
||||
<Border Margin="0,20,0,0" Padding="20" CornerRadius="12"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding Vault.PendingDeletion, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<views:ConfirmDeleteCard DataContext="{Binding Vault}" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Margin="0,24,0,40" Spacing="10">
|
||||
<ItemsControl ItemsSource="{Binding Vault.Groups}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HostGroupRowViewModel">
|
||||
<Border Classes="settingscard" Padding="20,16">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<Border Grid.Column="0" Width="38" Height="38" CornerRadius="10"
|
||||
Background="{StaticResource AvatarGradient}">
|
||||
<TextBlock Text="{Binding Initial}" FontWeight="Bold" FontSize="14"
|
||||
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="5" Margin="14,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Label}" FontSize="15" FontWeight="Bold" LetterSpacing="-0.2"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding Description}" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Column="2" Classes="chip" Margin="0,0,14,0" VerticalAlignment="Center"
|
||||
IsVisible="{Binding HasVaultBadge}">
|
||||
<TextBlock Text="{Binding VaultBadge}" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Classes="paneicon" Width="34" Height="34" FontSize="15"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vault.EditGroupCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="Rename this group or change what its hosts inherit.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
<Button Classes="paneicon danger" Width="34" Height="34" FontSize="15"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vault.DeleteGroupCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="Delete this group. The hosts filed under it are asked about separately.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- The "No group" footer row. Always drawn, even with zero groups, because it is true whether or
|
||||
not any group exists — it is just as often the only row on this page. -->
|
||||
<Border Classes="settingscard" Padding="20,16" Opacity="0.7"
|
||||
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="14">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="17" Text=""
|
||||
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
|
||||
<StackPanel Spacing="5">
|
||||
<TextBlock Text="No group" FontSize="14" FontWeight="SemiBold" LetterSpacing="-0.2"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
|
||||
Text="{Binding Vault.UngroupedHostCount, StringFormat='{}{0} hosts · always listed first · cannot be renamed or deleted'}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNull}}"
|
||||
Text="Unlock your keychain to see and manage your groups." />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Groups: every group, and the hosts filed under each — see the remark at the top of the markup.</summary>
|
||||
internal sealed partial class SettingsGroupsPage : UserControl
|
||||
{
|
||||
public SettingsGroupsPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsNav"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: settings mode's own 340px rail — SettingsNav.dc.html. Two sections rather than the design's three:
|
||||
the design's ORGANISATION section and its one row (Organisation settings) are refused outright — no
|
||||
organisation entity exists anywhere in this product; a Team is the membership list behind a shared vault,
|
||||
and there is exactly one tenant per deployment. See design-notes/v5c-fidelity-notes.md.
|
||||
|
||||
v5c-2: Groups and Tags joined CUSTOMIZE, after Preferences — the design's own order (Security,
|
||||
Preferences, Groups, Tags). Each opens a real management page over the existing group and tag commands;
|
||||
see SettingsGroupsPage.axaml and SettingsTagsPage.axaml.
|
||||
|
||||
Button.nav and its two TextBlock helpers are the exact rows NavRail.axaml already draws at 255px — height
|
||||
35, radius 8, a 19px glyph and a 10.5 semibold label, accent fill with white text when active. Reused
|
||||
rather than restyled: this design states the same three numbers for this rail's own rows, and a second
|
||||
copy of one style is how the two rails drift apart the day only one of them is touched again.
|
||||
-->
|
||||
|
||||
<Border Width="340" Background="{StaticResource DeepChrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
||||
<DockPanel LastChildFill="False" Margin="18,24">
|
||||
|
||||
<StackPanel DockPanel.Dock="Top" Spacing="8">
|
||||
|
||||
<TextBlock Text="SETTINGS" Margin="11,0,0,6" FontSize="10" FontWeight="SemiBold"
|
||||
LetterSpacing="1.5" Foreground="{StaticResource TextGhost}" />
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsGeneralPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.General}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="General" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsVaultsPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.Vaults}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Vaults" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsAccountPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.Account}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Account" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<TextBlock Text="CUSTOMIZE" Margin="11,14,0,6" FontSize="10" FontWeight="SemiBold"
|
||||
LetterSpacing="1.5" Foreground="{StaticResource TextGhost}" />
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsSecurityPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.Security}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Security" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsPreferencesPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.Preferences}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Preferences" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsGroupsPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.Groups}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Groups" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="flat nav" Classes.active="{Binding IsSettingsTagsPage}"
|
||||
Command="{Binding EnterSettingsCommand}"
|
||||
CommandParameter="{x:Static vm:SettingsPage.Tags}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Tags" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Wired to the same command the rail's own user popover Logout row calls — see
|
||||
MainWindowViewModel.SignOutFromPopover, which now lands on the Account page rather than on the old
|
||||
bare Preferences screen, so there is exactly one place the confirmation card is drawn.
|
||||
-->
|
||||
<Button DockPanel.Dock="Bottom" Classes="flat nav"
|
||||
Command="{Binding SignOutFromPopoverCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||
<TextBlock Classes="navicon" FontSize="17" Text="" />
|
||||
<TextBlock Classes="navlabel" Text="Logout" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Settings mode's own 340px rail — General/Vaults/Account, Security/Preferences, and Logout.</summary>
|
||||
internal sealed partial class SettingsNav : UserControl
|
||||
{
|
||||
public SettingsNav() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsPreferencesPage"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: Preferences, against Settings-Preferences.dc.html — the old PreferencesScreen.axaml's real content,
|
||||
restyled into the settings page idiom and split across two pages rather than one.
|
||||
|
||||
THIS MACHINE moved to the Security page whole: Windows Hello's register/"stop unlocking here" pair and
|
||||
the no-TPM explanation are Security's UNLOCKING card now, one home rather than two — see the cross-
|
||||
reference row at the foot of this page. Device name is not one of the rows that moved; it never existed
|
||||
here at all, because there is no device-name setting to move. Logs record the machine name on their own.
|
||||
|
||||
TERMINAL keeps its one real setting, text size, and drops the design's other five — font, cursor,
|
||||
scrollback, copy on select, terminal bell — which the renderer hard-codes. KEYCHAIN keeps Lock now,
|
||||
Sync now/Sign in, and the importer row exactly as the old screen had them; the design's own SECURITY
|
||||
card (clipboard-clear delay, confirm-run-on-insert) has nothing behind either row and is not drawn here.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock Classes="settingstitle" Text="Preferences" />
|
||||
|
||||
<TextBlock Classes="settingssection" Text="TERMINAL" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel>
|
||||
|
||||
<Border Classes="settingsrow last">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Text size" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="How large a terminal draws, in pixels. Ctrl+plus and Ctrl+minus do the same while a terminal has focus, and Ctrl+0 puts it back. It resizes the grid rather than magnifying it, so the remote is told how many columns it now has — which is also why it stops before the columns run out." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Top">
|
||||
<Button Width="36" Height="36" Classes="ghost" Padding="0"
|
||||
Command="{Binding ShrinkTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanShrinkTerminalFont}"
|
||||
ToolTip.Tip="Smaller · Ctrl+minus">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="15" />
|
||||
</Button>
|
||||
<TextBlock Classes="mono" FontSize="13.5" MinWidth="46" VerticalAlignment="Center"
|
||||
TextAlignment="Center" Foreground="{StaticResource Text}"
|
||||
Text="{Binding TerminalFontSize, StringFormat={}{0} px}" />
|
||||
<Button Width="36" Height="36" Classes="ghost" Padding="0"
|
||||
Command="{Binding EnlargeTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanEnlargeTerminalFont}"
|
||||
ToolTip.Tip="Larger · Ctrl+plus">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="15" />
|
||||
</Button>
|
||||
<Button Classes="ghost" Height="36" Content="RESET"
|
||||
Command="{Binding ResetTerminalFontCommand}"
|
||||
ToolTip.Tip="Back to the size it ships at · Ctrl+0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="KEYCHAIN" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel>
|
||||
|
||||
<Border Classes="settingsrow">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Lock the keychain" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Closes the keychain and forgets every key it held. Shells you have open keep running and reappear when you unlock — locked describes the keychain, not this machine's access to your hosts." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Height="38" Content="LOCK NOW"
|
||||
Command="{Binding LockCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingsrow">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Synchronise" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Runs a pass now. One runs on its own when the keychain opens, straight after any change, and every minute while it stays open." />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="ghost" Height="38" Content="SIGN IN" Command="{Binding SignInCommand}"
|
||||
IsVisible="{Binding !IsOnline}"
|
||||
ToolTip.Tip="Opens your browser. Only needed when there is no remembered session to resume." />
|
||||
<Button Classes="ghost" Height="38" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingsrow last">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Import SSH config" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Reads this machine's ~/.ssh/config, shows what it found, and imports only what you approve. Nothing is stored during the scan, and no private key is read — where a key file is named, the path is recorded as a note." />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Height="38" Content="OPEN IMPORTER"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Import}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="NOT BUILT YET" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel Margin="24,20" Spacing="10">
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Unlocking with Windows Hello moved to Security — it registers this machine rather than a preference of this screen's, and Security is where the rest of this machine's trust facts live." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Terminal font, cursor and scrollback — the renderer hard-codes those three. Text size above is the one that is not." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Copy on select and the terminal bell — neither is wired to anything the renderer does." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Clearing the clipboard after copying a secret, and confirming a snippet that runs on insert — neither exists; a copied secret stays on the clipboard until something else replaces it." />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="A device name — there is no such setting. Logs record this machine's own name on their own." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Settings mode's Preferences page: the Terminal and Keychain cards.</summary>
|
||||
internal sealed partial class SettingsPreferencesPage : UserControl
|
||||
{
|
||||
public SettingsPreferencesPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsSecurityPage"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: Security, against Settings-Security.dc.html.
|
||||
|
||||
The end-to-end card's sentence is ADR 0001's own claim in the design's words: "The server stores
|
||||
ciphertext and never holds a key" and "the relay operator ... see ciphertext only" — see docs/adr/0001-e2ee-trust-model.md.
|
||||
|
||||
UNLOCKING is Windows Hello's register/"Stop unlocking here" pair, moved here whole from the old
|
||||
PreferencesScreen.axaml — same RegisterDeviceCommand, ForgetDeviceCommand and the no-TPM explanation
|
||||
(HasNoDeviceKeyOption). One home for this fact rather than two; see the cross-reference on the
|
||||
Preferences page. The design's own auto-lock link row is refused: there is nothing to link to, since
|
||||
auto-lock and per-vault unlock rules do not exist anywhere in this client.
|
||||
|
||||
CONNECTING keeps exactly one row, Approved host keys. Strict host-key checking and the allowed-algorithm
|
||||
chips are both refused — the app always asks on a changed key, and there is no algorithm allow-list
|
||||
anywhere in the SSH stack. The count and the "N that no host dials" clause both come straight off
|
||||
KnownHostsViewModel.Summary, the same sentence the known-hosts screen's own header prints, so this row
|
||||
can never say a different number than the screen it sends you to. "Open host keys" leaves settings mode
|
||||
on purpose — known-hosts is a MAIN-chrome screen, and there is no honest way to show it without leaving;
|
||||
ShowScreenCommand already does that for any target that is not Preferences or Vaults.
|
||||
|
||||
RECENT SECURITY EVENTS is the link row alone, not the design's own list of rows. A real list would mean
|
||||
building a filtered read over LogsViewModel's activity and connection logs — which entries count as
|
||||
"security" is itself a judgement call the design does not resolve — and the honest link is complete on
|
||||
its own: Logs already holds the whole story, and this row says so rather than half-repeating it.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock Classes="settingstitle" Text="Security" />
|
||||
|
||||
<Border Classes="settingscard" Margin="0,26,0,0" Padding="20,20">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<Border Width="44" Height="44" CornerRadius="12" Background="#103A2F">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="20"
|
||||
Foreground="{StaticResource Live}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<StackPanel Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock Text="End-to-end encrypted" FontSize="15" FontWeight="Bold" LetterSpacing="-0.2"
|
||||
Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Every vault item is sealed on your devices. The server and the relay both store ciphertext and timestamps only — neither can read a single field." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="UNLOCKING" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel>
|
||||
<Border Classes="settingsrow last">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0" MaxWidth="640">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Windows Hello on this machine" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Registers this machine so a later launch can open the keychain with a Windows confirmation instead of your passphrase. Your passphrase keeps working."
|
||||
IsVisible="{Binding CanRegisterDevice}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Registered · the TPM holds a device key that can open the keychain with a Windows confirmation."
|
||||
IsVisible="{Binding CanForgetDevice}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="This machine has nowhere to keep a device key, so the keychain will keep asking for your passphrase. That needs a TPM and a Windows keystore willing to release the key."
|
||||
IsVisible="{Binding HasNoDeviceKeyOption}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Height="38" Content="REGISTER"
|
||||
Command="{Binding RegisterDeviceCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
IsVisible="{Binding CanRegisterDevice}" />
|
||||
<Button Grid.Column="1" Classes="danger" Height="38" Content="STOP UNLOCKING HERE"
|
||||
Command="{Binding ForgetDeviceCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
IsVisible="{Binding CanForgetDevice}"
|
||||
ToolTip.Tip="Withdraws this machine's device key, here and from your account, so it goes back to asking for your passphrase. Do this to a machine you have lost." />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="CONNECTING" />
|
||||
|
||||
<Border Classes="settingscard">
|
||||
<StackPanel>
|
||||
<Border Classes="settingsrow last">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0" MaxWidth="640">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Approved host keys" />
|
||||
<TextBlock Classes="settingsrowcaption" Text="{Binding KnownHostsScreen.Summary}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="ghost" Height="38" Content="OPEN HOST KEYS"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.KnownHosts}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="RECENT SECURITY EVENTS" />
|
||||
|
||||
<Border Classes="settingscard" Margin="0,12,0,40">
|
||||
<StackPanel>
|
||||
<Border Classes="settingsrow last">
|
||||
<Button Classes="flat" Padding="0" HorizontalAlignment="Left"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Logs}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="See all activity in Logs" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="14"
|
||||
Foreground="{StaticResource AccentText}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Settings mode's Security page: the E2E explainer, Windows Hello, and approved host keys.</summary>
|
||||
internal sealed partial class SettingsSecurityPage : UserControl
|
||||
{
|
||||
public SettingsSecurityPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<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.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsTagsPage"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
x:Name="Root">
|
||||
|
||||
<!--
|
||||
v5c-2: Tags, against Settings-Tags.dc.html — a real management page over VaultViewModel's own tag
|
||||
commands (Tags, NewTag, EditTagRow, DeleteTagRow), which used to be reachable only from the keychain
|
||||
screen's TAGS category. Both doors stay open — the keychain screen's own panel is untouched — because
|
||||
they lead to the same commands and the same rows; nothing here is a second implementation.
|
||||
|
||||
── THE INTRO SENTENCE IS TRUE, AND IT IS THE DESIGN'S OWN ─────────────────────────────────────────────
|
||||
"Renaming here is one write and every host follows" — verified against SaveTagAsync and TagRowViewModel's
|
||||
own remark: a host names a tag's id in its TagSet, never its label, so renaming touches nothing but the
|
||||
tag item itself. Kept rather than rewritten.
|
||||
|
||||
── EDIT AND DELETE ARE WRAPPER COMMANDS, UNLIKE THE GROUPS PAGE'S ──────────────────────────────────────
|
||||
EditTagCommand and DeleteTagCommand read VaultViewModel.SelectedTag rather than taking a row argument —
|
||||
they were built for the keychain screen's own ListBox selection, which this page has no equivalent of.
|
||||
VaultViewModel.EditTagRow/DeleteTagRow (v5c-2, additive) select the row and then call the real command, so
|
||||
every guard and every sentence either one already has is still the one that runs.
|
||||
|
||||
── WHAT THE DESIGN DREW AND THIS PAGE DOES NOT ─────────────────────────────────────────────────────────
|
||||
The LAST APPLIED column: no timestamp of when a tag was last put on a host exists anywhere in this
|
||||
client, and a column of "just now" / "2 min ago" would be inventing one.
|
||||
-->
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||
<TextBlock Classes="settingstitle" Text="Tags" />
|
||||
<Border Background="{StaticResource Chip}" CornerRadius="9" MinWidth="34" Height="30"
|
||||
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<TextBlock Text="{Binding Vault.Tags.Count}" FontSize="13.5" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextFaint}" Margin="8,0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="accent" Height="40" Content="+ NEW TAG"
|
||||
Command="{Binding Vault.NewTagCommand}"
|
||||
IsEnabled="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" Margin="0,12,0,0" TextWrapping="Wrap"
|
||||
Text="A tag is a name shared by every host that carries it. Renaming here is one write — every host, filter and snippet rule follows." />
|
||||
|
||||
<!-- The tag editor, in place — the same one-box form the keychain screen's own panel draws, restyled
|
||||
into this page's card idiom. -->
|
||||
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
|
||||
IsVisible="{Binding Vault.IsEditingTag}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Tag" />
|
||||
<TextBox PlaceholderText="name" Text="{Binding Vault.TagEditorLabel}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding Vault.SaveTagCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Renaming a tag changes it everywhere at once. No host is rewritten — each one names this tag rather than repeating its name." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Height="36" Content="SAVE" Command="{Binding Vault.SaveTagCommand}" />
|
||||
<Button Classes="ghost" Height="36" Content="CANCEL" Command="{Binding Vault.CancelTagEditCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- The delete confirmation, in place — the same shared control the groups and vaults pages reuse. -->
|
||||
<Border Margin="0,20,0,0" Padding="20" CornerRadius="12"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding Vault.PendingDeletion, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<views:ConfirmDeleteCard DataContext="{Binding Vault}" />
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingscard" Margin="0,24,0,40" Padding="0"
|
||||
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="1.4*,*,*,Auto" Margin="20,14,20,10">
|
||||
<TextBlock Grid.Column="0" Classes="label" Text="TAG" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="USED BY" />
|
||||
<TextBlock Grid.Column="2" Classes="label" Text="VAULT" />
|
||||
<TextBlock Grid.Column="3" Classes="label" Text=" " />
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Vault.Tags}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TagRowViewModel">
|
||||
<Border Classes="settingsrow last" Padding="20,10">
|
||||
<Grid ColumnDefinitions="1.4*,*,*,Auto">
|
||||
|
||||
<Border Grid.Column="0" Classes="chip" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="1" Classes="mono" FontSize="11.5"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Text="{Binding Description}" />
|
||||
|
||||
<!--
|
||||
No vault chip here — unlike a group, TagRowViewModel does not carry which vault it lives
|
||||
in (tags are read from the active vault alone; see VaultViewModel.NewTag's own remark),
|
||||
so there is no second vault this column could honestly name.
|
||||
-->
|
||||
<TextBlock Grid.Column="2" />
|
||||
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Classes="paneicon" Width="30" Height="30" FontSize="13"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vault.EditTagRowCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="Rename this tag.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
<Button Classes="paneicon danger" Width="30" Height="30" FontSize="13"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vault.DeleteTagRowCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="Delete this tag. Hosts wearing it simply stop showing the chip.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" Margin="20,14,20,18" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !Vault.HasTagItems}"
|
||||
Text="No tags yet. Add one from here, or from a host's own editor." />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" Margin="0,20,0,40" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Vault, Converter={x:Static ObjectConverters.IsNull}}"
|
||||
Text="Unlock your keychain to see and manage your tags." />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Tags: every tag, and how many hosts wear each — see the remark at the top of the markup.</summary>
|
||||
internal sealed partial class SettingsTagsPage : UserControl
|
||||
{
|
||||
public SettingsTagsPage() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsTitleBar"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: settings mode's own titlebar — SettingsNav.dc.html and the four Settings-*.dc.html sources all draw
|
||||
the same 53px bar: "Back to application" on the left, in place of the wordmark and the search box, and
|
||||
the same three window-control glyphs on the right TitleBar.axaml already draws.
|
||||
|
||||
A separate control rather than a variant of TitleBar itself, on the same reasoning SessionHeader and
|
||||
SessionStatusBar are their own files: nothing here can be measured by a test that hosts the real window,
|
||||
and a control that is either "the wordmark bar" or "the settings bar" depending on a bound flag would be
|
||||
two controls wearing one name. The dragging, maximising and closing logic is duplicated from TitleBar's
|
||||
own code-behind rather than shared through a base class — four short handlers, and the day one of the two
|
||||
bars needs its own window behaviour a shared base would have to be pulled apart first.
|
||||
|
||||
v5c-3: two back buttons rather than one whose text and command a converter swaps, toggled by
|
||||
MainWindowViewModel.IsImportOpen — Import.dc.html draws the same 53px bar with "Back to preferences" in
|
||||
place of "Back to application" while the importer is up, and CloseImportCommand closes it without leaving
|
||||
settings mode, unlike LeaveSettingsCommand.
|
||||
-->
|
||||
|
||||
<Border Height="53" Background="{StaticResource DeepChrome}"
|
||||
PointerPressed="OnDrag" DoubleTapped="OnToggleMaximised">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="26,0,20,0">
|
||||
|
||||
<Button Grid.Column="0" Classes="flat" HorizontalAlignment="Left"
|
||||
IsVisible="{Binding !IsImportOpen}"
|
||||
Command="{Binding LeaveSettingsCommand}"
|
||||
ToolTip.Tip="Back to the screen you were on before opening Settings">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="17"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Back to application" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="0" Classes="flat" HorizontalAlignment="Left"
|
||||
IsVisible="{Binding IsImportOpen}"
|
||||
Command="{Binding CloseImportCommand}"
|
||||
ToolTip.Tip="Back to preferences, without leaving Settings">
|
||||
<StackPanel Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="17"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Back to preferences" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!-- The window controls, identical to TitleBar's own — see the remark above on why they are repeated. -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="2">
|
||||
<Button Classes="flat" Width="26" Height="24" Click="OnMinimise"
|
||||
ToolTip.Tip="Minimise">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="14"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Classes="flat" Width="26" Height="24" Click="OnToggleMaximised"
|
||||
ToolTip.Tip="Maximise">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="13"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button Classes="flat close" Width="26" Height="24" Click="OnClose"
|
||||
ToolTip.Tip="Close DodoSSH. This ends every shell it has open.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="14"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,47 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Settings mode's own titlebar — see the remark in the markup for why it is not TitleBar itself.</summary>
|
||||
internal sealed partial class SettingsTitleBar : UserControl
|
||||
{
|
||||
public SettingsTitleBar() => InitializeComponent();
|
||||
|
||||
private Window? Host => TopLevel.GetTopLevel(this) as Window;
|
||||
|
||||
/// <summary>Left button only, and only on a press nothing inside the bar has already handled.</summary>
|
||||
private void OnDrag(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.Handled || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Host?.BeginMoveDrag(e);
|
||||
}
|
||||
|
||||
private void OnMinimise(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Host is { } window)
|
||||
{
|
||||
window.WindowState = WindowState.Minimized;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Both the button and a double-click on the bar arrive here, as Windows convention expects.</summary>
|
||||
private void OnToggleMaximised(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Host is not { } window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
window.WindowState = window.WindowState == WindowState.Maximized
|
||||
? WindowState.Normal
|
||||
: WindowState.Maximized;
|
||||
}
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Host?.Close();
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:contracts="using:DodoSSH.Contracts"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsVaultsPage"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
x:Name="Root">
|
||||
|
||||
<!--
|
||||
v5c-2: Vaults, against Settings-Vaults.dc.html. Replaces the old full-bleed VaultsScreen, which this
|
||||
settings page now covers completely — every command it had is reachable here exactly once, and
|
||||
VaultsScreen.axaml is gone. Its data is DataContext.Vaults (VaultsViewModel), reached as {Binding Vaults.*}
|
||||
throughout because this page's own DataContext is the shell, the same choice every other settings page
|
||||
makes.
|
||||
|
||||
── WHAT THE DESIGN DREW AND THIS PAGE DOES NOT ─────────────────────────────────────────────────────────
|
||||
"Manage devices" and "3 devices" in the sync line — no list-devices endpoint exists. The whole VAULT
|
||||
DEFAULTS card (auto-lock, require-password-on-unlock, relay toggle) and the whole RECOVERY card (kit,
|
||||
export) — none of the three exists; sync is always on. The magenta "Default" badge — the app does track a
|
||||
"new items go to" vault (VaultViewModel.TargetVaultId), but that lives on a different view model than the
|
||||
row being drawn here and cross-referencing the two per card would be more machinery than the badge is
|
||||
worth; dropped rather than faked. Per-vault UNLOCKED/LOCKED chips — this app locks the keychain as a
|
||||
whole, not one vault at a time, so there is no per-vault state for a chip to draw; the keychain-level fact
|
||||
lives in the sync card's dot instead. The "Unlock Rocateq" modal — same reason, there is nothing per-vault
|
||||
to unlock. Member avatar stacks on the card list — VaultRowViewModel carries a member *count*, not the
|
||||
members themselves; only the selected vault's Members collection is actually loaded, which is why avatars
|
||||
are drawn in the members panel below and nowhere else. The "Sorted by name ▾" control — decorative in the
|
||||
mock; Vaults.Vaults is already ordered personal-first-then-name and there is no second order to switch to.
|
||||
|
||||
── THE MEMBERS PANEL ────────────────────────────────────────────────────────────────────────────────────
|
||||
The design draws an 860px modal. This app's only overlay idiom below a whole-window mode is the scrim a
|
||||
Border with a translucent Background and a PointerPressed handler draws — see QuickConnect.axaml, the
|
||||
other place a click-away-to-close panel exists — so that is what this is, capped at MaxWidth 860 rather
|
||||
than fixed to it: the settings content column is narrower than 860 at the window's minimum, and a modal
|
||||
that insisted on the full width would arrange its own rows past the edge of what this page is given.
|
||||
|
||||
Inside it is the old screen's own right-hand column, restyled rather than reinvented: a selectable list of
|
||||
members, the SET ROLE buttons, the ADD row, REMOVE, HAND OVER, SHARE KEY/WITHDRAW KEY and the KEY HOLDERS
|
||||
list, on the same VaultsViewModel.SelectedMember selection the old screen used. Deleting a vault stays a
|
||||
card-level action (the danger icon below), not a members-panel one, so PendingAction is drawn in only one
|
||||
of the two places at a time — see the two IsVisible guards below, which key off IsMembersPanelOpen because
|
||||
HAND OVER can only ever be armed with the panel open (it needs a selected member) and DELETE only with it
|
||||
closed (the card's own icon is what arms it).
|
||||
|
||||
The sentence that must survive from the old screen: a vault cannot be deleted through the membership list
|
||||
behind it — there is no such call anywhere in the server, and archiving a team while its vault still
|
||||
exists is refused. That is not the same fact as "a vault cannot be deleted" — the DELETE icon below does
|
||||
delete the vault itself — and both sentences are kept, each where it is true.
|
||||
-->
|
||||
|
||||
<Panel>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||
<TextBlock Classes="settingstitle" Text="Vaults" />
|
||||
<Border Background="{StaticResource Chip}" CornerRadius="9" MinWidth="34" Height="30">
|
||||
<TextBlock Text="{Binding Vaults.Vaults.Count}" FontSize="13.5" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource TextFaint}" Margin="8,0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
|
||||
<Button Classes="ghost" Height="40" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
|
||||
<Button Classes="accent" Height="40" Content="+ NEW VAULT"
|
||||
Command="{Binding Vaults.NewVaultCommand}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- The name-a-vault and rename-a-vault forms, in place — this window has no modal idiom for a bare
|
||||
text field, and the members panel below is a different kind of overlay: over one vault's people,
|
||||
not over a form. -->
|
||||
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
|
||||
IsVisible="{Binding Vaults.IsCreatingVault}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settingsrowtitle" Text="New vault" />
|
||||
<TextBox PlaceholderText="Name" Text="{Binding Vaults.NewVaultName}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Its key is made on this machine and nobody else has it. Add people to it once it exists, then press SHARE KEY." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Height="36" Content="CREATE"
|
||||
Command="{Binding Vaults.CreateVaultCommand}" IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
<Button Classes="ghost" Height="36" Content="CANCEL"
|
||||
Command="{Binding Vaults.CancelNewVaultCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
|
||||
IsVisible="{Binding Vaults.IsRenamingVault}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settingsrowtitle" Text="Rename vault" />
|
||||
<TextBox PlaceholderText="Name" Text="{Binding Vaults.EditVaultName}" />
|
||||
<TextBlock Classes="settingsrowcaption"
|
||||
Text="Everybody who shares this vault sees the new name. Nothing is re-encrypted and no key changes — the name has always been stored in plain text, because a person has to be able to pick a vault before anything is decrypted." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Height="36" Content="SAVE"
|
||||
Command="{Binding Vaults.SaveVaultNameCommand}" IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
<Button Classes="ghost" Height="36" Content="CANCEL"
|
||||
Command="{Binding Vaults.CancelRenameVaultCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- A vault's own destructive confirmation. Only while the members panel is shut — see the remark
|
||||
above for why the two never compete for this one PendingAction. Both conditions on the same
|
||||
Border, not one nested inside the other: a Border visible with its content hidden would still
|
||||
draw an empty danger-tinted card while the members panel's own copy of this confirmation is
|
||||
showing. -->
|
||||
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20"
|
||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}">
|
||||
<Border.IsVisible>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<Binding Path="Vaults.IsConfirming" />
|
||||
<Binding Path="!Vaults.IsMembersPanelOpen" />
|
||||
</MultiBinding>
|
||||
</Border.IsVisible>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding Vaults.PendingAction.Question}" FontSize="14.5" FontWeight="Bold"
|
||||
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding Vaults.PendingAction.Consequence}" FontSize="12.5"
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="danger" Height="36" Content="CONFIRM"
|
||||
Command="{Binding Vaults.ConfirmActionCommand}" IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
<Button Classes="ghost" Height="36" Content="CANCEL"
|
||||
Command="{Binding Vaults.CancelActionCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Sync status: only what is real — no device count, no "ran N minutes ago" (this session tracks no
|
||||
such timestamp). The dot and the word are MainWindowViewModel.IsFullySynced/SyncLabel, the exact
|
||||
fact the titlebar's own dot already tells the truth with. -->
|
||||
<Border Classes="settingscard" Margin="0,20,0,0" Padding="20,18">
|
||||
<StackPanel Orientation="Horizontal" Spacing="14">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="9" Height="9"
|
||||
Margin="0,7,0,0" VerticalAlignment="Top" />
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding SyncLabel}" FontSize="13" FontWeight="Bold"
|
||||
LetterSpacing="0.5" Foreground="{StaticResource Text}" />
|
||||
<TextBlock Classes="settingsrowcaption" Text="End-to-end encrypted." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="YOUR VAULTS" Margin="0,32,0,12" />
|
||||
|
||||
<StackPanel Spacing="10" Margin="0,0,0,40" IsVisible="{Binding Vaults.HasVaults}">
|
||||
<ItemsControl ItemsSource="{Binding Vaults.Vaults}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultRowViewModel">
|
||||
<Border Classes="settingscard" Padding="22,18">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<Border Grid.Column="0" Width="52" Height="52" CornerRadius="12"
|
||||
Background="{StaticResource AvatarGradient}">
|
||||
<TextBlock Text="{Binding Initial}" FontWeight="Bold" FontSize="20"
|
||||
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="7" Margin="16,0" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Name}" FontSize="17" FontWeight="Bold"
|
||||
LetterSpacing="-0.25" Foreground="{StaticResource Text}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="chip">
|
||||
<TextBlock Text="{Binding RoleLabel}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextFaint}"
|
||||
Text="{Binding Detail}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="mono" FontSize="10.5" Foreground="{StaticResource WarnText}"
|
||||
Text="{Binding State}" IsVisible="{Binding HasState}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Classes="paneicon" Width="34" Height="34" FontSize="15"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vaults.OpenMembersPanelCommand}"
|
||||
CommandParameter="{Binding}" IsVisible="{Binding IsShared}"
|
||||
ToolTip.Tip="See and manage who is in this vault.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
<Button Classes="paneicon" Width="34" Height="34" FontSize="15"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vaults.RenameVaultRowCommand}"
|
||||
CommandParameter="{Binding}" IsVisible="{Binding CanAdminister}"
|
||||
ToolTip.Tip="Changes what this vault is called. The name is plaintext on the server, as it always was; nothing inside is re-encrypted.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
<Button Classes="paneicon danger" Width="34" Height="34" FontSize="15"
|
||||
Command="{Binding #Root.((vm:MainWindowViewModel)DataContext).Vaults.DeleteVaultRowCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding !IsPersonal}"
|
||||
ToolTip.Tip="Deletes this vault and withdraws everybody's key to it. It cannot reach a machine that has already synced it.">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" Margin="0,20,0,40" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !Vaults.HasVaults}"
|
||||
Text="No vaults yet. Unlock your keychain to see the personal one, or make a vault to share hosts and credentials with colleagues." />
|
||||
|
||||
<TextBlock Margin="0,0,0,40" FontSize="11.5" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource TextFaint}"
|
||||
IsVisible="{Binding Vaults.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||
Text="{Binding Vaults.Status}" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ THE MEMBERS PANEL ============ -->
|
||||
<Border x:Name="MembersBackdrop" Background="#9905050A"
|
||||
IsVisible="{Binding Vaults.IsMembersPanelOpen}" PointerPressed="OnBackdropPressed">
|
||||
<Border MaxWidth="860" MaxHeight="620" Margin="30"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1" CornerRadius="14">
|
||||
<Grid RowDefinitions="Auto,*" Margin="26,22">
|
||||
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,14">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Vaults.SelectedVault.Name, StringFormat='{}{0} · Members'}"
|
||||
FontSize="18" FontWeight="Bold" LetterSpacing="-0.25"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="1" Classes="paneicon" Width="26" Height="26"
|
||||
Command="{Binding Vaults.CloseMembersPanelCommand}" ToolTip.Tip="Close">
|
||||
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="15" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel Spacing="16">
|
||||
|
||||
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Vaults.HasSharedMembershipWarning}"
|
||||
Text="{Binding Vaults.SharedMembershipWarning}" />
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding !Vaults.SelectedIsPersonal}">
|
||||
|
||||
<TextBlock Classes="settingssection" Text="MEMBERS" Margin="0" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Vaults.Members}" SelectedItem="{Binding Vaults.SelectedMember}"
|
||||
Background="Transparent" BorderThickness="0" MaxHeight="220">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultMemberRowViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*,150,Auto" Margin="0,4">
|
||||
<Border Grid.Column="0" Width="30" Height="30" CornerRadius="15"
|
||||
Background="{StaticResource AvatarGradient}">
|
||||
<TextBlock Text="{Binding Initials}" FontWeight="Bold" FontSize="10"
|
||||
Foreground="White" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Spacing="2" Margin="10,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="settingsrowcaption" FontSize="11" Text="{Binding Email}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="settingsrowcaption" FontSize="10.5" Text="{Binding KeyState}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Classes="settingsrowcaption" FontSize="10" Text="{Binding LastActive}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Role}" FontSize="10.5"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Margin="10,0,0,0" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- The hand-over confirmation, only while this panel is the one that armed it. -->
|
||||
<Border Padding="14" CornerRadius="10" Background="{StaticResource DangerWash}"
|
||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="1"
|
||||
IsVisible="{Binding Vaults.IsConfirming}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding Vaults.PendingAction.Question}" FontSize="13.5" FontWeight="Bold"
|
||||
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding Vaults.PendingAction.Consequence}" FontSize="11.5"
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="danger" Content="CONFIRM" Command="{Binding Vaults.ConfirmActionCommand}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding Vaults.CancelActionCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding Vaults.CanAdministerSelected}">
|
||||
<TextBlock Classes="label" Text="SET THE SELECTED MEMBER'S ROLE" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="flat choice" Content="VIEWER" Command="{Binding Vaults.ChangeRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
<Button Classes="flat choice" Content="MEMBER" Command="{Binding Vaults.ChangeRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Member}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
<Button Classes="flat choice" Content="ADMIN" Command="{Binding Vaults.ChangeRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Admin}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="ghost" Content="HAND OVER" Command="{Binding Vaults.HandOverCommand}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}" IsVisible="{Binding Vaults.OwnsSelected}"
|
||||
ToolTip.Tip="Hands this vault to the selected member. They become its owner and you become an admin; only the new owner can hand it on again." />
|
||||
<Button Classes="danger" Content="REMOVE" Command="{Binding Vaults.RemoveMemberCommand}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}"
|
||||
ToolTip.Tip="Removes the selected member and withdraws every key they hold to this vault. It blocks future reads only." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding Vaults.CanAdministerSelected}">
|
||||
<TextBlock Classes="settingssection" Text="INVITE" Margin="0" />
|
||||
<Grid ColumnDefinitions="*,140,44">
|
||||
<TextBox Grid.Column="0" PlaceholderText="colleague@example.com"
|
||||
Text="{Binding Vaults.NewMemberEmail}" Margin="0,0,8,0" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
|
||||
<Button Classes="flat choice" Content="VIEWER"
|
||||
Classes.active="{Binding Vaults.AddsAsViewer}"
|
||||
Command="{Binding Vaults.ChooseNewMemberRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}" />
|
||||
<Button Classes="flat choice" Content="MEMBER"
|
||||
Classes.active="{Binding Vaults.AddsAsMember}"
|
||||
Command="{Binding Vaults.ChooseNewMemberRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Member}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2" Classes="accent" Width="44" Content="+"
|
||||
Command="{Binding Vaults.AddMemberCommand}" IsEnabled="{Binding !Vaults.IsBusy}"
|
||||
Margin="8,0,0,0"
|
||||
ToolTip.Tip="Adds the account that signs in with this address. An address with no account here is refused and says so." />
|
||||
</Grid>
|
||||
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
|
||||
Text="Adding somebody lets the server serve them this vault. It does not let them read it — that needs a vault key, which SHARE KEY below wraps for them." />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding Vaults.SelectedIsPersonal}">
|
||||
<TextBlock Classes="settingssection" Text="YOURS ALONE" Margin="0" />
|
||||
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
|
||||
Text="Nobody can be added to your personal vault, and the server refuses a key grant on one outright. Make a vault for the things you want to share, and put them in it." />
|
||||
</StackPanel>
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" />
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding Vaults.SelectedIsShared}">
|
||||
<Button Classes="accent" Content="SHARE KEY" Command="{Binding Vaults.ShareVaultCommand}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}"
|
||||
ToolTip.Tip="Wraps this vault's key to the selected member. Their published key is checked against the server's append-only key log first." />
|
||||
<Button Classes="danger" Content="WITHDRAW KEY" Command="{Binding Vaults.RevokeVaultCommand}"
|
||||
IsEnabled="{Binding !Vaults.IsBusy}"
|
||||
ToolTip.Tip="Withdraws the selected member's key to this vault. Blocks future reads only." />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="settingssection" Text="KEY HOLDERS" Margin="0" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Vaults.Grants}" Background="Transparent" BorderThickness="0"
|
||||
MaxHeight="120">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultGrantRowViewModel">
|
||||
<Grid ColumnDefinitions="10,*" Margin="0,3">
|
||||
<Ellipse Grid.Column="0" Width="6" Height="6" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsLive}" Fill="{StaticResource Live}" />
|
||||
<StackPanel Grid.Column="1" Spacing="2" Margin="6,0,0,0">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="settingsrowcaption" FontSize="11" Text="{Binding State}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!--
|
||||
Load-bearing, from the old screen: there is no DELETE here for a reason, and the reason is
|
||||
not "not built yet". The membership list behind a shared vault cannot be archived while the
|
||||
vault exists — that is a different fact from the vault itself, which the card's own DELETE
|
||||
icon does remove.
|
||||
-->
|
||||
<TextBlock Classes="settingsrowcaption" TextWrapping="Wrap"
|
||||
Text="A vault's membership list cannot be deleted on its own — the server refuses to archive it while the vault it carries still exists. Deleting the vault itself is the card's own icon, outside this panel." />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
</Panel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,29 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Vaults, restyled into the settings page idiom — see the remark at the top of the markup.</summary>
|
||||
internal sealed partial class SettingsVaultsPage : UserControl
|
||||
{
|
||||
public SettingsVaultsPage() => InitializeComponent();
|
||||
|
||||
private MainWindowViewModel? Shell => DataContext as MainWindowViewModel;
|
||||
|
||||
/// <remarks>
|
||||
/// Only a press on the wash itself, the same test <c>QuickConnect.OnBackdropPressed</c> makes: a press
|
||||
/// inside the card bubbles through here too, with its source the control that was actually hit rather
|
||||
/// than the backdrop, so closing on those would make the panel impossible to click into.
|
||||
/// </remarks>
|
||||
private void OnBackdropPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (!ReferenceEquals(e.Source, MembersBackdrop) || Shell is not { } shell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
shell.Vaults.CloseMembersPanelCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<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.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.SettingsView"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
v5c: the settings MODE — the full-window chrome swap the design calls Settings, built as one control so
|
||||
MainWindow.axaml can show or hide the whole thing behind MainWindowViewModel.IsSettingsMode with a single
|
||||
IsVisible, the same way it already does for the unlocked application and the setup half of the window.
|
||||
|
||||
Titlebar, then a row of the 340px SettingsNav beside whichever page ActiveSettingsPage names. Seven pages
|
||||
now, all of the design's — v5c-2 added Groups and Tags and retired the old VaultsScreen; see
|
||||
SettingsNav.axaml and design-notes/v5c-fidelity-notes.md.
|
||||
|
||||
Every page including Vaults is wrapped in the 1100px centred column now. SettingsVaultsPage replaces the
|
||||
old VaultsScreen's own 268px-list-beside-a-full-bleed-pane layout with the design's card idiom — see that
|
||||
page's own remark for what changed and why.
|
||||
|
||||
v5c-3: ImportScreen joined the Panel, drawn over SettingsPreferencesPage rather than beside it — see
|
||||
MainWindowViewModel.IsImportOpen. SettingsNav stays lit on Preferences the whole time, per Import.dc.html,
|
||||
because ActiveSettingsPage never actually leaves SettingsPage.Preferences; only IsImportOpen and the pair
|
||||
of IsVisible bindings below move. Its data context is ImportScreen rather than this control's own, the
|
||||
same split MainWindow.axaml drew before the importer moved in here.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Background="{StaticResource Canvas}">
|
||||
|
||||
<views:SettingsTitleBar Grid.Row="0" />
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||
|
||||
<views:SettingsNav Grid.Column="0" />
|
||||
|
||||
<Panel Grid.Column="1">
|
||||
|
||||
<views:SettingsGeneralPage IsVisible="{Binding IsSettingsGeneralPage}" />
|
||||
|
||||
<views:SettingsVaultsPage IsVisible="{Binding IsSettingsVaultsPage}" />
|
||||
|
||||
<views:SettingsAccountPage IsVisible="{Binding IsSettingsAccountPage}" />
|
||||
|
||||
<views:SettingsSecurityPage IsVisible="{Binding IsSettingsSecurityPage}" />
|
||||
|
||||
<views:SettingsPreferencesPage IsVisible="{Binding IsSettingsPreferencesContentShowing}" />
|
||||
|
||||
<views:SettingsGroupsPage IsVisible="{Binding IsSettingsGroupsPage}" />
|
||||
|
||||
<views:SettingsTagsPage IsVisible="{Binding IsSettingsTagsPage}" />
|
||||
|
||||
<!--
|
||||
Wrapped, like MainWindow.axaml wraps every screen whose data context is its own rather than this
|
||||
control's: IsVisible has to resolve against the ambient MainWindowViewModel, and putting it on the
|
||||
same element as the DataContext override below would have it resolve against ImportViewModel
|
||||
instead, where IsImportOpen does not exist.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsImportOpen}">
|
||||
<views:ImportScreen DataContext="{Binding ImportScreen}" />
|
||||
</Panel>
|
||||
|
||||
</Panel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The settings mode's whole chrome: its own titlebar, its own 340px rail, and whichever of its five pages
|
||||
/// <see cref="DodoSSH.Client.Shell.ViewModels.MainWindowViewModel.ActiveSettingsPage"/> names.
|
||||
/// </summary>
|
||||
internal sealed partial class SettingsView : UserControl
|
||||
{
|
||||
public SettingsView() => InitializeComponent();
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:contracts="using:DodoSSH.Contracts"
|
||||
x:Class="DodoSSH.Client.App.Views.VaultsScreen"
|
||||
x:DataType="vm:VaultsViewModel">
|
||||
|
||||
<!--
|
||||
Vaults, and the people in each of them.
|
||||
|
||||
── THIS WAS THE TEAMS SCREEN, AND THE TEAM IS NOW BEHIND THE VAULT. ─────────────────────────────────
|
||||
The left column used to list teams; a team owned vaults, and sharing meant creating a team, then a
|
||||
vault in it, then wrapping a key. Two of those three steps were about a concept nobody came here for.
|
||||
So the rows are vaults now: naming one makes the membership list that carries it, and everything on
|
||||
the right — members, key holders — is that vault's. The server still authorises against
|
||||
a team, because that is what VaultAccessService resolves; what went is the requirement that a person
|
||||
know it exists. The one case where it is still visible is a membership list carrying several vaults,
|
||||
which this screen cannot make and will not hide: see SharedMembershipWarning.
|
||||
|
||||
── THE ONE FACT THE WHOLE SCREEN IS BUILT AROUND ────────────────────────────────────────────────────
|
||||
Adding somebody to a vault and giving them its key are two different acts, and only the first is
|
||||
something a server can do. The second needs a machine that holds the key, because this server never
|
||||
does. So the members list and the key-holders list are both here and are not the same list, an
|
||||
addition says out loud that it granted nothing readable yet, and SHARE KEY is its own button rather
|
||||
than a checkbox on the member row.
|
||||
|
||||
What the design asked for and is still not here: two-factor state (no such concept exists anywhere in
|
||||
this product) and avatars (no picture is stored anywhere). There is no INVITED list either, and that
|
||||
one is a decision rather than a gap — an address is not a way into a vault, so only an account that
|
||||
already exists can be added and there is nothing pending to draw. See the ADD box below, which says
|
||||
what to do about somebody who has not signed in here yet. Last-active is recorded at most once per
|
||||
account per hour, so it is drawn coarsely. Nor is there a way to delete a vault: the server has no
|
||||
such call, and the screen says so rather than offering a button that refuses.
|
||||
-->
|
||||
|
||||
<Grid ColumnDefinitions="268,*">
|
||||
|
||||
<!-- ============ The vault list ============ -->
|
||||
<Border Grid.Column="0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
||||
<Grid RowDefinitions="44,*,Auto">
|
||||
|
||||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="VAULTS" FontSize="12" FontWeight="SemiBold"
|
||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="1" Classes="ghost" Content="NEW"
|
||||
Command="{Binding NewVaultCommand}" IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Makes a vault you can share. Its key is generated on this machine, and nobody else has it until you hand it out." />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel>
|
||||
<!--
|
||||
Read from this machine's own vault list rather than from the server, so the column is right
|
||||
with no connection. What is missing offline is who is in each one, which is why a row can
|
||||
say its membership is unknown rather than saying nothing at all.
|
||||
-->
|
||||
<ListBox ItemsSource="{Binding Vaults}" SelectedItem="{Binding SelectedVault}"
|
||||
Background="Transparent" BorderThickness="0">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultRowViewModel">
|
||||
<StackPanel Spacing="2" Margin="0,3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding RoleLabel}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding Detail}" />
|
||||
<!--
|
||||
Only when there is something to say. A vault waiting for a key and one owing a rekey
|
||||
are both temporary and both need somebody to act; a permanent "fine" beside them
|
||||
would teach people to stop reading the line.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="10.5" Text="{Binding State}"
|
||||
TextWrapping="Wrap" IsVisible="{Binding HasState}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" Margin="14,12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding !HasVaults}"
|
||||
Text="No vaults yet. Unlock your keychain to see the personal one, or make a vault to share hosts and credentials with colleagues." />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
The name-a-vault form, in place rather than in a modal: this window has no idiom for one. It is
|
||||
in this column rather than beside the pane on the right for one reason — that pane is bound to
|
||||
HasSelection, so with no vaults at all it is not on screen, and "no vaults at all" is exactly
|
||||
the state somebody arrives in from the tab strip's New vault entry.
|
||||
|
||||
One field. The membership list behind it is made with it and named after it, and its slug is
|
||||
derived — see VaultsViewModel.CreateVaultAsync. Asking for a URL handle would be asking for one
|
||||
from somebody who has not been told they are making anything but a vault.
|
||||
-->
|
||||
<Border Grid.Row="2" Padding="14,12" BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding IsCreatingVault}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="label" Text="NEW VAULT" />
|
||||
<TextBox PlaceholderText="Name" Text="{Binding NewVaultName}" />
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Its key is made on this machine and nobody else has it. Add people to it once it exists, then press SHARE KEY." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="CREATE" Command="{Binding CreateVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelNewVaultCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ The selected vault: who is in it, and who can open it ============ -->
|
||||
<Grid Grid.Column="1" RowDefinitions="44,*,Auto">
|
||||
|
||||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
|
||||
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SelectedVault.Name}" FontSize="12"
|
||||
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<!--
|
||||
The vault's own operations. RENAME is an admin's; handing it on is the owner's alone, and
|
||||
that is the line the server draws as well — an admin the owner promoted must not be able to
|
||||
take the vault from them.
|
||||
|
||||
DELETE is an admin's too, and it is last and red because it is the only one of the three that
|
||||
cannot be undone. It is absent on the personal vault rather than disabled: there is no version
|
||||
of this window in which that vault can go, so a greyed button would be an offer that never
|
||||
becomes real. Its question is the card below, as every destructive answer on this screen is.
|
||||
-->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="{Binding ShowsVaultActions}">
|
||||
<Button Classes="ghost" Content="RENAME" Command="{Binding RenameVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" IsVisible="{Binding CanAdministerSelected}"
|
||||
ToolTip.Tip="Changes what this vault is called. The name is plaintext on the server, as it always was; nothing inside is re-encrypted." />
|
||||
<Button Classes="ghost" Content="HAND OVER" Command="{Binding HandOverCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" IsVisible="{Binding OwnsSelected}"
|
||||
ToolTip.Tip="Hands this vault to the selected member. They become its owner and you become an admin; only the new owner can hand it on again." />
|
||||
<Button Classes="danger" Content="DELETE" Command="{Binding DeleteVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" IsVisible="{Binding CanDeleteSelected}"
|
||||
ToolTip.Tip="Deletes this vault and withdraws everybody's key to it. It cannot reach a machine that has already synced it." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer Grid.Row="1" IsVisible="{Binding HasSelection}">
|
||||
<StackPanel Margin="14,14" Spacing="18">
|
||||
|
||||
<!-- The rename form, in place, exactly as the create form on the left is. -->
|
||||
<Border Padding="12" CornerRadius="4" BorderThickness="1"
|
||||
BorderBrush="{StaticResource Border}" IsVisible="{Binding IsRenamingVault}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="label" Text="RENAME VAULT" />
|
||||
<TextBox PlaceholderText="Name" Text="{Binding EditVaultName}" />
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="Everybody who shares this vault sees the new name. Nothing is re-encrypted and no key changes; the name has always been stored in plain text, because a person has to be able to pick a vault before anything is decrypted." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveVaultNameCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelRenameVaultCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
The armed confirmation, drawn where the buttons that armed it were. The keychain screen's
|
||||
idiom, and for the same reason: there is no modal anywhere in this window.
|
||||
-->
|
||||
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="12"
|
||||
IsVisible="{Binding IsConfirming}">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding PendingAction.Question}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="{Binding PendingAction.Consequence}" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="danger" Content="CONFIRM" Command="{Binding ConfirmActionCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelActionCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
The personal vault, which is the one vault sharing cannot reach. Said here rather than by
|
||||
drawing the members section empty: an empty MEMBERS heading over a vault that can never have
|
||||
any reads as a feature that has not loaded.
|
||||
-->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding SelectedIsPersonal}">
|
||||
<TextBlock Classes="label" Text="YOURS ALONE" />
|
||||
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
||||
Text="Nobody can be added to your personal vault, and the server refuses a key grant on one outright — a key wrapped to somebody it will go on refusing to serve would look like sharing and would not be. Make a vault above for the things you want to share, and put them in it." />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Members -->
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding SelectedIsShared}">
|
||||
<TextBlock Classes="label" Text="MEMBERS" />
|
||||
|
||||
<!--
|
||||
Only ever non-empty for a membership list this screen did not make. Adding somebody to one
|
||||
vault and silently adding them to three others is precisely the fact a vault-shaped screen
|
||||
is in a position to hide, so it says it instead.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
IsVisible="{Binding HasSharedMembershipWarning}"
|
||||
Text="{Binding SharedMembershipWarning}" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Members}" SelectedItem="{Binding SelectedMember}"
|
||||
Background="Transparent" BorderThickness="0" MaxHeight="240">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultMemberRowViewModel">
|
||||
<Grid ColumnDefinitions="*,168,Auto" Margin="0,3">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding Email}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Spacing="2" VerticalAlignment="Center">
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding KeyState}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="10.5" Text="{Binding LastActive}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Role}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||
Margin="10,0,0,0" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!--
|
||||
Buttons and a command rather than a selector bound to the role, which is the choice the
|
||||
key editor and the category rail already make and for the reason they record: a selector
|
||||
moves its own highlight before anything can refuse, so it can end up showing a role
|
||||
nobody was given. OWNER is absent because it is not a role that can be assigned —
|
||||
handing the vault over is its own act, with its own confirmation.
|
||||
-->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding CanAdministerSelected}">
|
||||
<TextBlock Classes="label" Text="SET THE SELECTED MEMBER'S ROLE" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="flat choice" Content="VIEWER" Command="{Binding ChangeRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="May pull this vault and may not push. It does not withdraw a key they already hold." />
|
||||
<Button Classes="flat choice" Content="MEMBER" Command="{Binding ChangeRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Member}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="May read and change what is in this vault." />
|
||||
<Button Classes="flat choice" Content="ADMIN" Command="{Binding ChangeRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Admin}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="May also add and remove people, rename the vault, and share its key." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" IsVisible="{Binding CanAdministerSelected}">
|
||||
<TextBox Grid.Column="0" PlaceholderText="colleague@example.com"
|
||||
Text="{Binding NewMemberEmail}" Margin="0,0,6,0" />
|
||||
<Button Grid.Column="1" Classes="accent" Content="ADD"
|
||||
Command="{Binding AddMemberCommand}" IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Adds the account that signs in with this address. An address with no account here is refused and says so — ask them to sign in to this server once, which is what creates the account, and then add them." />
|
||||
<Button Grid.Column="2" Classes="danger" Content="REMOVE" Margin="6,0,0,0"
|
||||
Command="{Binding RemoveMemberCommand}" IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Removes the selected member and withdraws every key they hold to this vault. It blocks future reads only — anything already on their machine stays there, so rotate the credentials that matter." />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding CanAdministerSelected}">
|
||||
<TextBlock Classes="label" Text="THEY ARRIVE AS" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="flat choice" Content="VIEWER" Classes.active="{Binding AddsAsViewer}"
|
||||
Command="{Binding ChooseNewMemberRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Viewer}" />
|
||||
<Button Classes="flat choice" Content="MEMBER" Classes.active="{Binding AddsAsMember}"
|
||||
Command="{Binding ChooseNewMemberRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Member}" />
|
||||
<Button Classes="flat choice" Content="ADMIN" Classes.active="{Binding AddsAsAdmin}"
|
||||
Command="{Binding ChooseNewMemberRoleCommand}"
|
||||
CommandParameter="{x:Static contracts:TeamMemberRole.Admin}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
IsVisible="{Binding CanAdministerSelected}"
|
||||
Text="Adding somebody lets the server serve them this vault. It does not let them read it: a vault key can only be wrapped by a machine that already holds it, which is what SHARE KEY below does." />
|
||||
</StackPanel>
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" />
|
||||
|
||||
<!-- Who holds the key -->
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding SelectedIsShared}">
|
||||
<Button Classes="accent" Content="SHARE KEY" Command="{Binding ShareVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Wraps this vault's key to the selected member. 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." />
|
||||
<Button Classes="danger" Content="WITHDRAW KEY" Command="{Binding RevokeVaultCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Withdraws the selected member's key to this vault. Blocks future reads only." />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Who can open this vault — the design's "shared with" avatars, as names and a state.
|
||||
Withdrawn and stale grants stay listed and say which they are, because a list that quietly
|
||||
dropped them would show a departed colleague as merely absent rather than as somebody whose
|
||||
key was taken away. The dot is Live and means exactly what it says: this person can open
|
||||
this vault right now.
|
||||
-->
|
||||
<TextBlock Classes="label" Text="KEY HOLDERS" />
|
||||
|
||||
<ListBox ItemsSource="{Binding Grants}" Background="Transparent" BorderThickness="0"
|
||||
MaxHeight="150">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:VaultGrantRowViewModel">
|
||||
<Grid ColumnDefinitions="10,*" Margin="0,3">
|
||||
<Ellipse Grid.Column="0" Width="6" Height="6" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsLive}" Fill="{StaticResource Live}" />
|
||||
<StackPanel Grid.Column="1" Spacing="2" Margin="6,0,0,0">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="Medium"
|
||||
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Classes="hint" FontSize="11" Text="{Binding State}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
IsVisible="{Binding SelectedIsShared}"
|
||||
Text="Sharing verifies the recipient's key against the key log, which proves this 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." />
|
||||
|
||||
<!--
|
||||
Said once, where somebody would otherwise go looking for a DELETE button. There is no call
|
||||
for it anywhere in the server, and archiving the membership list behind a vault is refused
|
||||
while the vault exists — so a button here would be one that always refuses.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
||||
Text="A vault cannot be deleted. Nothing in this product removes one, and the server refuses to archive the membership list behind it while it still exists." />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="hint" FontSize="12" Margin="20" TextWrapping="Wrap"
|
||||
VerticalAlignment="Top" IsVisible="{Binding !HasSelection}"
|
||||
Text="Make a vault on the left, or wait for somebody to add you to one. A vault holds hosts and credentials that a group of people share; its key is what makes those readable, and that key is handed out by people rather than by the server." />
|
||||
|
||||
<Border Grid.Row="2" Padding="14,10" BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Classes="hint" FontSize="11.5" Text="{Binding Status}" TextWrapping="Wrap" />
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,9 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DodoSSH.Client.App.Views;
|
||||
|
||||
/// <summary>Vaults: which there are, who is in each, and who holds a key to it.</summary>
|
||||
internal sealed partial class VaultsScreen : UserControl
|
||||
{
|
||||
public VaultsScreen() => InitializeComponent();
|
||||
}
|
||||
Reference in New Issue
Block a user