Public Access
369 lines
23 KiB
XML
369 lines
23 KiB
XML
<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.SnippetsScreen"
|
|
x:DataType="vm:SnippetsViewModel">
|
|
|
|
<!--
|
|
Commands somebody has saved, and how to get one into a terminal.
|
|
|
|
The list and the writing belong to the vault, as every other item kind's do; this screen is the filter,
|
|
the editor and the insert over the top. See SnippetsViewModel.
|
|
|
|
The two buttons at the bottom right of the detail pane are the whole safety design, and their wording is
|
|
load-bearing. A terminal is one input stream with no notion of being at a prompt — the remote may be
|
|
inside vi, or at a sudo password prompt with echo off — so this application cannot say "run this
|
|
command", only "type this into whatever is there". RUN appears solely for a snippet whose own flag says
|
|
it runs, which makes that a decision taken once while writing it rather than a button beside every one
|
|
of them.
|
|
|
|
── v5b — Snippets.dc.html ──────────────────────────────────────────────────────────────────────────────
|
|
A fidelity pass, not a new shape. What moved:
|
|
|
|
◆ THE SCREEN'S OWN TITLE. The design renames this screen "Snips" — adopted here on screen, matching the
|
|
nav rail, which already says Snips (see NavRail.axaml's own remark on the mock's wording for this
|
|
destination).
|
|
|
|
◆ THE HEADER. "Snips" 33 bold, a count chip, the existing filter box restyled to 280px, and a
|
|
"+ New snip" accent button in place of the old ghost "+ NEW SNIPPET" — same NewCommand.
|
|
|
|
◆ THE LIST. Card rows rather than a flat list — radius 10, Track fill and an accent ring on the selected
|
|
one, Track on hover — with a { } glyph, the mono name, the "runs immediately" chip (real: it is
|
|
RunsOnInsert, already tracked per snippet), the vault chip, and the mono command preview beneath. No
|
|
modified date: SnippetSecret carries none, and neither does any other item kind — VaultItem is (id,
|
|
secret, version, three sync flags) and nothing else; see docs/design-import-gaps.md. The bottom action
|
|
row keeps EDIT / MOVE TO VAULT… / DELETE, restyled and DELETE now danger-coloured to match.
|
|
|
|
◆ THE DETAIL/EDITOR SIDEBAR, widened to 340px per the design. Two modes exist as they always have — the
|
|
editor while IsEditing, the selected snippet's own facts and its insert controls otherwise — because the
|
|
two are genuinely different forms doing different things, not a cosmetic split; merging them would let
|
|
somebody fixing a typo re-file a snippet by leaving a vault picker where they found it. VAULT draws a
|
|
lock glyph beside the vault name, per the design. "Runs on insert"/"runs immediately" keeps this screen's
|
|
own longer caption rather than the design's shorter one — it states the operational consequence
|
|
("typed at the prompt and waits for you") where the design's states only the rationale for having the
|
|
setting once; both are true, the existing one is more actionable. The insert button keeps its own dynamic
|
|
label — "TYPE INTO {tab}" or "NO TERMINAL OPEN" — over the design's static "Type into terminal", because
|
|
naming the destination tab is a fact the design's caption does not carry and this screen already had.
|
|
Its own caption similarly stays: it names vi and a sudo prompt with echo off as the two ways "at a
|
|
prompt" can be wrong, which is more than the design's own sentence says.
|
|
|
|
◆ THE DELETE CONFIRMATION. Snippets.dc.html's own modal — vault-wide reach, a tombstone, no undo — is
|
|
exactly what VaultViewModel.HowFarADeletionGoes already says for every other kind of item, so
|
|
SnippetsViewModel.RequestDelete/ConfirmDelete/CancelDelete say it in the same words. Additive next to the
|
|
existing uncounted DeleteCommand rather than a change to it — that command is what the phone's own DELETE
|
|
row still calls, with no confirmation card on that screen to answer one; see SnippetsViewModel's own
|
|
remarks on both commands.
|
|
-->
|
|
|
|
<UserControl.Styles>
|
|
<!--
|
|
A card, not a full-bleed row: Track fill on hover, Track fill plus an accent ring on the selected one,
|
|
matching the design's own "selected = Track bg + accent ring, hover Track" — distinct from
|
|
ListBox.filerows, which draws no ring, because a snippet card is chosen for one editor at a time rather
|
|
than opened directly.
|
|
-->
|
|
<Style Selector="ListBox.snipcards > ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="BorderBrush" Value="Transparent" />
|
|
</Style>
|
|
<Style Selector="ListBox.snipcards > ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Track}" />
|
|
</Style>
|
|
<Style Selector="ListBox.snipcards > ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Track}" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
|
</Style>
|
|
<Style Selector="ListBox.snipcards > ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Track}" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
|
</Style>
|
|
</UserControl.Styles>
|
|
|
|
<Grid RowDefinitions="Auto,*" Margin="26">
|
|
|
|
<!-- ============ THE HEADER ============ -->
|
|
<Grid Grid.Row="0" Margin="0,0,0,20" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
|
|
|
<TextBlock Grid.Column="0" Text="Snips" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
|
|
|
<Border Grid.Column="1" Margin="12,0,0,0" MinWidth="34" Height="30" CornerRadius="9" Padding="8,0"
|
|
Background="{StaticResource Chip}" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" Text="{Binding Visible.Count}" FontSize="12.5"
|
|
Foreground="{StaticResource TextDim}" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" />
|
|
</Border>
|
|
|
|
<!--
|
|
The command is searched as well as the name: half of what anybody remembers about a saved command is
|
|
a word that was inside it.
|
|
-->
|
|
<TextBox Grid.Column="3" x:Name="SnippetFilter" Text="{Binding Filter}" Width="280" Height="40"
|
|
CornerRadius="10" Margin="0,0,10,0"
|
|
PlaceholderText="filter by name or command" VerticalAlignment="Center" />
|
|
|
|
<Button Grid.Column="4" Classes="accent" Height="40" FontSize="13.5" Content="+ New snip"
|
|
Command="{Binding NewCommand}" />
|
|
|
|
</Grid>
|
|
|
|
<!-- ============ THE BORDERED BODY ============ -->
|
|
<Border Grid.Row="1" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
|
|
ClipToBounds="True">
|
|
<Grid ColumnDefinitions="*,340">
|
|
|
|
<Grid Grid.Column="0" RowDefinitions="*,Auto" Background="{StaticResource Pane}">
|
|
|
|
<ListBox Grid.Row="0" x:Name="SnippetList" Classes="snipcards" Focusable="True"
|
|
ItemsSource="{Binding Visible}"
|
|
SelectedItem="{Binding Selected}"
|
|
Margin="10,10,10,4">
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:SnippetRowViewModel">
|
|
<StackPanel Margin="6,7" Spacing="6">
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<TextBlock Classes="mono" Text="{}{ }" FontSize="11" FontWeight="Bold"
|
|
Foreground="{StaticResource AccentText}" VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="13" FontWeight="Medium"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<!--
|
|
The flag, where the decision is made. A snippet that presses Enter for you is not the
|
|
same kind of thing as one that does not, and the list is where somebody chooses between
|
|
them.
|
|
-->
|
|
<Border Classes="chip warn" Padding="7,0" Height="18" VerticalAlignment="Center"
|
|
IsVisible="{Binding RunsOnInsert}">
|
|
<TextBlock Text="runs immediately" FontSize="9.5" />
|
|
</Border>
|
|
<Border Classes="chip warn" Padding="7,0" Height="18" VerticalAlignment="Center"
|
|
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
|
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
|
</Border>
|
|
<!--
|
|
Which vault this one is in, drawn only where there is more than one to be in. A shared
|
|
snippet is a command the rest of a team can read and insert; the row is where somebody
|
|
decides whether the thing they are about to edit is theirs alone.
|
|
-->
|
|
<Border Classes="chip" Padding="7,0" Height="18" VerticalAlignment="Center"
|
|
IsVisible="{Binding HasVaultBadge}">
|
|
<TextBlock Text="{Binding VaultBadge}" FontSize="9.5" />
|
|
</Border>
|
|
</StackPanel>
|
|
<!--
|
|
Newlines shown as ⏎ rather than dropped. A three-line snippet flattened into one run of
|
|
text reads as a single command, which is the thing being decided about on this row.
|
|
-->
|
|
<TextBlock Classes="mono" Text="{Binding Preview}" FontSize="11.5"
|
|
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
|
|
</StackPanel>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
|
|
<TextBlock Grid.Row="0" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
|
|
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
|
TextAlignment="Center" MaxWidth="360"
|
|
IsVisible="{Binding !HasVisible}" />
|
|
|
|
<Border Grid.Row="1" Padding="16,10" BorderBrush="{StaticResource BorderSubtle}"
|
|
BorderThickness="0,1,0,0">
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<Button Classes="ghost" Content="Edit" Command="{Binding EditCommand}"
|
|
IsEnabled="{Binding HasSelection}" />
|
|
<!--
|
|
Sharing a snippet, which is what moving one into a team's vault is. Beside EDIT rather than
|
|
inside it: the two vaults are encrypted under different keys, so this is a re-seal into one
|
|
and a tombstone in the other — nothing a SAVE could do — and a picker inside the form would
|
|
let somebody correcting a typo hand a command to a team by leaving it where they found it.
|
|
The picker itself opens beside the snippet, in the pane on the right.
|
|
-->
|
|
<Button Classes="ghost" Content="Move to vault…" Command="{Binding MoveCommand}"
|
|
IsEnabled="{Binding CanMove}"
|
|
ToolTip.Tip="Re-encrypts this snippet with another vault's key. Everybody who holds that key can then read and insert it." />
|
|
<Button Classes="danger" Content="Delete" Command="{Binding RequestDeleteCommand}"
|
|
IsEnabled="{Binding HasSelection}" />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
<Border Grid.Column="1" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
|
|
<ScrollViewer>
|
|
<StackPanel Margin="18,20" Spacing="10">
|
|
|
|
<!-- ============ The editor ============ -->
|
|
<StackPanel Spacing="8" IsVisible="{Binding IsEditing}">
|
|
<TextBlock Classes="label" Text="NAME" FontSize="10" />
|
|
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" FontFamily="{StaticResource MonoFont}" />
|
|
|
|
<!--
|
|
Which vault a *new* snippet is filed into, asked on the form it is being typed into rather
|
|
than through a standing preference elsewhere. Not drawn for an existing snippet — its vault
|
|
is not a field of this form, and changing it is MOVE — and not drawn at all where there is
|
|
only one vault to choose between, because a control offering one option is a question
|
|
nobody asked.
|
|
-->
|
|
<StackPanel Spacing="4" IsVisible="{Binding ShowsEditorVaultChoice}">
|
|
<TextBlock Classes="label" Text="VAULT" FontSize="10" />
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding EditorVaultChoices}"
|
|
SelectedItem="{Binding EditorSelectedVault}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="A shared vault means everybody holding its key can read this command and insert it into their own terminals." />
|
|
</StackPanel>
|
|
|
|
<TextBlock Classes="label" Text="COMMAND" FontSize="10" />
|
|
<!--
|
|
Stored exactly as typed — no trimming, no newline normalisation. A here-document's
|
|
terminator has to arrive on a line of its own, and tidying the trailing newline off it
|
|
leaves the shell waiting for one that never comes.
|
|
-->
|
|
<TextBox Text="{Binding EditorCommand}" PlaceholderText="the command" AcceptsReturn="True"
|
|
Height="140" TextWrapping="NoWrap" FontFamily="{StaticResource MonoFont}"
|
|
FontSize="12" />
|
|
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="48" TextWrapping="Wrap" />
|
|
|
|
<Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
|
|
<StackPanel Grid.Column="0" Spacing="3">
|
|
<TextBlock Text="Runs on insert" FontSize="12.5" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextDim}" />
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="Off means the command is typed at the prompt and waits for you. That single Enter is the only thing standing between a saved command and a running one, so leave it off unless you meant it." />
|
|
</StackPanel>
|
|
<CheckBox Grid.Column="1" IsChecked="{Binding EditorRunsOnInsert}"
|
|
VerticalAlignment="Top" Margin="10,0,0,0" />
|
|
</Grid>
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
|
<Button Classes="accent" Content="Save" Command="{Binding SaveCommand}" />
|
|
<Button Classes="ghost" Content="Cancel" Command="{Binding CancelCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- ============ The selected snippet ============ -->
|
|
<StackPanel Spacing="10" IsVisible="{Binding !IsEditing}">
|
|
|
|
<TextBlock Classes="hint" FontSize="12"
|
|
Text="Choose a snippet to see it in full and put it into a terminal."
|
|
IsVisible="{Binding !HasSelection}" />
|
|
|
|
<StackPanel Spacing="10" IsVisible="{Binding HasSelection}">
|
|
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="15"
|
|
FontWeight="Bold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
|
|
|
<TextBlock Classes="label" Text="COMMAND" FontSize="10" />
|
|
<Border CornerRadius="10" Background="{StaticResource Pane}"
|
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12">
|
|
<SelectableTextBlock Classes="mono" Text="{Binding Selected.Snippet.Command}"
|
|
FontSize="11.5" LineHeight="19"
|
|
Foreground="{StaticResource TextFaint}" TextWrapping="Wrap" />
|
|
</Border>
|
|
|
|
<TextBlock Classes="mono" Text="{Binding Selected.Snippet.Notes}" FontSize="11"
|
|
Foreground="{StaticResource TextFaint}" TextWrapping="Wrap"
|
|
IsVisible="{Binding Selected.Snippet.Notes, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
|
|
<!--
|
|
VAULT, with the design's own lock glyph. Named here as well as on the row, because this
|
|
pane is where somebody decides to put a command into a production terminal, and who else
|
|
holds a key to it is part of that decision.
|
|
-->
|
|
<StackPanel Spacing="4" IsVisible="{Binding HasSelectionVaultBadge}">
|
|
<TextBlock Classes="label" Text="VAULT" FontSize="10" />
|
|
<Border Height="38" CornerRadius="10" Background="{StaticResource Pane}"
|
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12,0">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SelectionVaultBadge}"
|
|
FontSize="11.5" Foreground="{StaticResource TextDim}"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="1" FontFamily="{StaticResource IconFont}" Text=""
|
|
FontSize="14" Foreground="{StaticResource TextFaint}"
|
|
VerticalAlignment="Center" />
|
|
</Grid>
|
|
</Border>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The button names the tab it will type into, over the design's static "Type into
|
|
terminal" — see the file-level remark above for why the dynamic label is the truer one.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding ShowsSelectionActions}">
|
|
<Button Classes="accent" Content="{Binding InsertLabel}" HorizontalAlignment="Stretch"
|
|
HorizontalContentAlignment="Center"
|
|
Command="{Binding InsertCommand}" IsEnabled="{Binding CanInsert}"
|
|
ToolTip.Tip="Types the command at the prompt and stops. Nothing runs until you press Enter there." />
|
|
|
|
<Button Classes="danger" Content="{Binding RunLabel}" HorizontalAlignment="Stretch"
|
|
HorizontalContentAlignment="Center"
|
|
Command="{Binding RunCommand}"
|
|
IsVisible="{Binding SelectionRuns}" IsEnabled="{Binding CanInsert}"
|
|
ToolTip.Tip="Types the command and presses Enter. Offered because this snippet is marked as one that runs." />
|
|
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" TextAlignment="Center"
|
|
Text="Whatever is in the terminal receives this. Nothing here can tell whether that is a shell prompt, an editor, or a password prompt with the echo off — so check the tab before you insert." />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
◆ SHARING THE SNIPPET, which is what moving it into a team's vault amounts to. A picker
|
|
and two buttons, not a question with a yes.
|
|
-->
|
|
<StackPanel Spacing="8" IsVisible="{Binding IsMoving}">
|
|
<TextBlock Classes="label" Text="MOVE TO VAULT" FontSize="10" />
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveVaultChoices}"
|
|
SelectedItem="{Binding SelectedMoveVault}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="The snippet is re-encrypted with the other vault's key, so everybody who holds that key can read this command and insert it — and nobody else can. Nothing else about it changes." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="Move" Command="{Binding ConfirmMoveCommand}" />
|
|
<Button Classes="ghost" Content="Cancel" Command="{Binding CancelMoveCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
◆ THE DELETE CONFIRMATION. See RequestDelete's own remarks for why this is additive
|
|
beside DeleteCommand rather than a change to it.
|
|
-->
|
|
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
|
BorderThickness="1" CornerRadius="10" Padding="12"
|
|
IsVisible="{Binding IsConfirmingDelete}">
|
|
<StackPanel Spacing="8">
|
|
<TextBlock Classes="heading" FontSize="14" TextWrapping="Wrap"
|
|
Text="{Binding DeleteQuestion}" />
|
|
<TextBlock Foreground="{StaticResource WarnText}" FontSize="12" TextWrapping="Wrap"
|
|
Text="{Binding DeleteConsequence}" />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="danger" Content="Delete snippet"
|
|
Command="{Binding ConfirmDeleteCommand}" />
|
|
<Button Classes="ghost" Content="Cancel" Command="{Binding CancelDeleteCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Border>
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
</Grid>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|