Public Access
Give the application a settings area built from what really exists
This commit is contained in:
@@ -123,6 +123,9 @@ internal sealed class HostGroupRowViewModel(VaultGroupItem group, int hostCount)
|
||||
|
||||
/// <summary>What the row says under the name.</summary>
|
||||
internal string Description => hostCount == 1 ? "1 host" : $"{hostCount} hosts";
|
||||
|
||||
/// <summary>The single letter the settings page's card draws on this group's tile.</summary>
|
||||
internal string Initial => Label.Length > 0 ? Label[..1].ToUpperInvariant() : "?";
|
||||
}
|
||||
|
||||
/// <summary>An entry in the host editor's group picker.</summary>
|
||||
@@ -4667,8 +4670,20 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
|
||||
SelectedTag = Tags.FirstOrDefault(row => row.EntityId == selectedId);
|
||||
|
||||
OnPropertyChanged(nameof(HasTagItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether there is at least one tag to manage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// v5c-2: the settings Tags page's empty state. Named apart from <c>HostRowViewModel.HasTags</c>, which
|
||||
/// answers a different question — whether one host wears any — rather than reusing that name at this
|
||||
/// level and relying on which <c>x:DataType</c> a binding happens to be inside to tell the two apart.
|
||||
/// </remarks>
|
||||
internal bool HasTagItems => Tags.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// A host with its group chain applied: the port to dial, the user to log in as, and how to
|
||||
/// authenticate.
|
||||
@@ -4763,6 +4778,28 @@ internal sealed partial class VaultViewModel(
|
||||
GroupFilter = Groups.FirstOrDefault(row => row.EntityId == filteredId);
|
||||
|
||||
OnPropertyChanged(nameof(HasGroups));
|
||||
OnPropertyChanged(nameof(UngroupedHostCount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many hosts, across every shown vault, carry no group that still exists.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// v5c: the settings Groups page's "No group" footer row. The same dangling-reference reading
|
||||
/// <see cref="FlattenIntoSections"/> gives the sidebar's own UNGROUPED heading — a host naming a group
|
||||
/// this vault no longer has counts as ungrouped rather than vanishing — but counted over every shown
|
||||
/// vault's hosts rather than over a find-box-filtered subset, because a settings page has no find box
|
||||
/// and a count that shrank while somebody typed in one would be answering the wrong question.
|
||||
/// </remarks>
|
||||
internal int UngroupedHostCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var known = Groups.Select(group => group.EntityId).ToHashSet();
|
||||
|
||||
return Hosts.Count(row =>
|
||||
IsVaultShown(row.VaultId) && (row.Host.GroupId is not { } id || !known.Contains(id)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -9881,6 +9918,35 @@ internal sealed partial class VaultViewModel(
|
||||
Status = $"Renaming {row.Label}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the tag editor for a specific row, from the settings page's per-row edit icon.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A thin wrapper around <see cref="EditTag"/> rather than a second implementation of what it does. The
|
||||
/// keychain screen's own table drives <see cref="EditTag"/> off a <c>ListBox</c> selection; the settings
|
||||
/// page draws one card per tag with no such selection to lean on, so this puts the row on
|
||||
/// <see cref="SelectedTag"/> first and then asks the command that already knows how to open it — the
|
||||
/// same trick <see cref="EditGroupFromHeading"/> plays for a group, except that command took the row as
|
||||
/// an argument from the start and this one did not, because nothing needed it to until now.
|
||||
/// </remarks>
|
||||
/// <param name="row">The tag to edit.</param>
|
||||
[RelayCommand]
|
||||
private void EditTagRow(TagRowViewModel? row)
|
||||
{
|
||||
SelectedTag = row;
|
||||
EditTagCommand.Execute(null);
|
||||
}
|
||||
|
||||
/// <summary>Arms the delete confirmation for a specific row, from the settings page's per-row delete icon.</summary>
|
||||
/// <inheritdoc cref="EditTagRow" path="/remarks" />
|
||||
/// <param name="row">The tag to ask about deleting.</param>
|
||||
[RelayCommand]
|
||||
private void DeleteTagRow(TagRowViewModel? row)
|
||||
{
|
||||
SelectedTag = row;
|
||||
DeleteTagCommand.Execute(null);
|
||||
}
|
||||
|
||||
/// <summary>Abandons the tag editor.</summary>
|
||||
[RelayCommand]
|
||||
private void CancelTagEdit()
|
||||
@@ -10341,6 +10407,35 @@ internal sealed partial class VaultViewModel(
|
||||
await AutoSyncAsync(cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts the selected pin's fingerprint on the clipboard.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The twin of <see cref="CopyPublicKeyAsync"/> and the opposite call about secrecy: a host key
|
||||
/// fingerprint is not one. Operators publish theirs on purpose, and the whole workflow this screen exists
|
||||
/// for is comparing a pinned one against what was published — which is a copy-and-paste somebody should
|
||||
/// not have to retype by hand out of a box that refuses to trim it.
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private async Task CopyPinFingerprintAsync()
|
||||
{
|
||||
if (SelectedKnownHost is not { } row)
|
||||
{
|
||||
Status = "Choose a pinned key first.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (copyToClipboard is null)
|
||||
{
|
||||
Status = "This machine has no clipboard.";
|
||||
return;
|
||||
}
|
||||
|
||||
await copyToClipboard(row.Fingerprint).ConfigureAwait(true);
|
||||
|
||||
Status = $"Copied the fingerprint for {row.Host}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a terminal on the selected host.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user