Public Access
Merge branch 'claude/phone-pins'
This commit is contained in:
@@ -48,7 +48,7 @@
|
||||
the refusal has no continue button here either.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*,Auto">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<!-- ============ header ============ -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Height="56" Margin="8,0">
|
||||
@@ -199,8 +199,48 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ pins ============ -->
|
||||
<!--
|
||||
◆ The phone's answer to the desktop's QUICK ACCESS sidebar — the same PinnedPathList this host was
|
||||
connected with, drawn where this head actually browses files rather than beside a terminal it has no
|
||||
strip for. See TransfersViewModel.ConnectedPinnedPaths for why this is a snapshot taken at connect
|
||||
rather than a live follow of the host row: a pin added or removed mid-session shows up here on the
|
||||
next connect, not while this one is still open.
|
||||
|
||||
Its own row rather than folded into the breadcrumb above: the two scroll independently and mean
|
||||
different things — the breadcrumb is where this pane is, the chips are places it can jump to — and a
|
||||
shared row would make ↑ look like one more pin among several.
|
||||
|
||||
Gone rather than empty when nothing is pinned or nothing is connected, which HasConnectedPins already
|
||||
answers: an empty scrolling strip under the breadcrumb would read as a loading row rather than as
|
||||
"this host has nothing pinned".
|
||||
-->
|
||||
<ScrollViewer Grid.Row="4" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
IsVisible="{Binding HasConnectedPins}" Margin="0,0,0,4">
|
||||
<ItemsControl ItemsSource="{Binding ConnectedPinnedPaths}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" Margin="14,0" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<!--
|
||||
Classes="row" at its default 44dp MinHeight rather than the breadcrumb's own 36 — these chips
|
||||
are the destination, not the trail behind it, and a target worth a deliberate tap gets the
|
||||
full touch floor this head holds everything else to. Not trimmed: a chip scrolls sideways with
|
||||
the row rather than being squeezed to fit it, so the full path is always what a tap commits to.
|
||||
-->
|
||||
<Button Classes="row" MinHeight="44" Padding="11,0" CornerRadius="10"
|
||||
Command="{Binding $parent[views:FilesScreen].((vm:TransfersViewModel)DataContext).GoRemoteCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Classes="mono" FontSize="12" Text="{Binding}" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ the listing ============ -->
|
||||
<Panel Grid.Row="4">
|
||||
<Panel Grid.Row="5">
|
||||
|
||||
<TextBlock Classes="body" IsVisible="{Binding !IsConnected}" Margin="24,12"
|
||||
VerticalAlignment="Top" Text="{Binding Status}" />
|
||||
@@ -266,7 +306,7 @@
|
||||
|
||||
<!-- ============ what to do with the chosen entry ============ -->
|
||||
<!-- DeepChrome rather than Chrome, since v5 — see the remark on PhoneShell's vault header. -->
|
||||
<Border Grid.Row="5" IsVisible="{Binding IsConnected}" Background="{StaticResource DeepChrome}"
|
||||
<Border Grid.Row="6" IsVisible="{Binding IsConnected}" Background="{StaticResource DeepChrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Padding="12,10">
|
||||
<StackPanel Spacing="9">
|
||||
|
||||
|
||||
@@ -1098,6 +1098,60 @@
|
||||
Command="{Binding AddEditorTagCommand}" />
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
◆ QUICK ACCESS. Staged on the shared VaultViewModel.EditorPinnedPaths the way EditorTagChoices
|
||||
stages tags above — populated when the editor opens, read back by BuildHost on SAVE, left alone
|
||||
by CANCEL. Nothing here is phone-only: a pin added on this page is the same PinnedPathList entry
|
||||
the desktop's drawer stages under the same field, so it syncs to every other client the moment
|
||||
this vault does, exactly as a tag would.
|
||||
|
||||
No folder glyph in the row, unlike the desktop's — this head carries MonoFont and a heading font
|
||||
(see Theme/Phone.axaml) and no icon font, so the desktop's  would draw as a tofu box
|
||||
rather than a folder here. The path text carries the row on its own, trimmed in a Grid's star
|
||||
column rather than a StackPanel's — a StackPanel measures its children at infinity, so
|
||||
TextTrimming never actually engages inside one; this is the same fix TransfersScreen's pane
|
||||
headers needed for the same reason.
|
||||
|
||||
The remove button is Classes="icon" at its default 44x44, not the desktop's 22x22 close box —
|
||||
see this file's touch-floor rule at the top of the class: a target sized for a mouse pointer is
|
||||
not one a thumb can reliably land on.
|
||||
|
||||
See surface B, the Files screen's pin chips, for where these pins actually get used on this
|
||||
head — the hint sentence below names it rather than the desktop's terminal strip, which does
|
||||
not exist here.
|
||||
-->
|
||||
<TextBlock Classes="label" Text="QUICK ACCESS" Margin="0,4,0,0" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding EditorPinnedPaths}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Spacing="6" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Border MinHeight="44" CornerRadius="10" Background="{StaticResource Field}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12,0">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="mono" FontSize="12.5" Text="{Binding}"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||||
<Button Grid.Column="1" Classes="icon" Content="✕"
|
||||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).RemoveEditorPinCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="Unpins this path" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<TextBox Grid.Column="0" Classes="field" Text="{Binding EditorNewPin}"
|
||||
PlaceholderText="/path/to/folder" />
|
||||
<Button Grid.Column="2" Classes="secondary" Height="44" Width="72" Content="ADD"
|
||||
Command="{Binding AddEditorPinCommand}" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Classes="body"
|
||||
Text="Pins show up as chips on the Files screen while you are connected to this host — tap one to jump straight there." />
|
||||
|
||||
<!--
|
||||
◆ The one control here that publishes something. Turning it on copies this host's address and
|
||||
port into plaintext columns the server can read, which is the single deliberate concession in
|
||||
@@ -1117,6 +1171,18 @@
|
||||
<TextBlock Classes="body"
|
||||
Text="Not built yet: this app always dials the host itself, so ticking this stores the address on the server and changes nothing about how the host is reached. When it works, the relay will dial on your behalf — which is why the address and port have to be stored in the clear. Everything else about the host stays encrypted either way, and a relayed host needs a port of its own rather than its group's." />
|
||||
|
||||
<!--
|
||||
This page covers the whole screen while it is open, which is why it needs a Status line of its
|
||||
own rather than relying on the one the list behind it draws at the top of HOSTS (see this file's
|
||||
other Classes="detail" Text="{Binding Status}") — that TextBlock is off-screen for as long as
|
||||
IsEditing is true. AddEditorTagCommand and AddEditorPinCommand both refuse silently into Status
|
||||
rather than throwing, so without a line to draw it here the honesty rule would be broken: a
|
||||
refusal that writes a message nothing on screen shows is the same, to whoever pressed ADD, as no
|
||||
refusal at all.
|
||||
-->
|
||||
<TextBlock Classes="detail" Text="{Binding Status}" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*" Margin="0,6,0,0">
|
||||
<Button Grid.Column="0" Classes="primary" Height="44" Content="SAVE"
|
||||
Command="{Binding SaveHostCommand}" />
|
||||
|
||||
@@ -637,6 +637,31 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
[ObservableProperty]
|
||||
private string? connectedIdentityLabel;
|
||||
|
||||
/// <summary>
|
||||
/// The paths pinned on the connected host, for the phone's Files-screen chip row — the desktop draws
|
||||
/// the same list in its QUICK ACCESS sidebar, over the terminal surface rather than this one. See
|
||||
/// <see cref="VaultViewModel.EditorPinnedPaths"/> for where a pin is actually added or removed; this is
|
||||
/// a read of what was already saved there.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Captured at connect, the same moment <see cref="ConnectedTo"/> is, rather than followed live off the
|
||||
/// host row's own <c>PinnedPaths</c>. A pin edited while this session stays open shows up on the next
|
||||
/// connect rather than mid-session — the same lag <see cref="ConnectedTo"/> itself already carries for
|
||||
/// a relabel — because this screen reads the vault once, at the moment it dials, rather than staying
|
||||
/// wired to a collection it otherwise never has a reason to watch. A bucket has no pins at all:
|
||||
/// <see cref="OpenBucketAsync"/> leaves this empty rather than reading as "not yet known", which is what
|
||||
/// empty already means for a host that connected with none pinned.
|
||||
/// </remarks>
|
||||
internal ObservableCollection<string> ConnectedPinnedPaths { get; } = [];
|
||||
|
||||
/// <summary>Whether the connected host or bucket has any pins to draw as chips.</summary>
|
||||
/// <remarks>
|
||||
/// A read of <see cref="ConnectedPinnedPaths"/> rather than an <c>[ObservableProperty]</c> of its own,
|
||||
/// so it is raised by hand at each of the three places that collection is repopulated or cleared —
|
||||
/// <see cref="MarkHostConnected"/>, <see cref="OpenBucketAsync"/> and <see cref="CloseSessionAsync"/>.
|
||||
/// </remarks>
|
||||
internal bool HasConnectedPins => ConnectedPinnedPaths.Count > 0;
|
||||
|
||||
[ObservableProperty]
|
||||
private HostKeyPresentation? pendingHostKey;
|
||||
|
||||
@@ -947,6 +972,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
ConnectedHostKeyAlgorithm = null;
|
||||
ConnectedIdentityLabel = null;
|
||||
|
||||
// And no pins either — see ConnectedPinnedPaths's own remark.
|
||||
ConnectedPinnedPaths.Clear();
|
||||
OnPropertyChanged(nameof(HasConnectedPins));
|
||||
|
||||
connected = (ConnectedTo, row.Label, row.EntityId, TimeProvider.System.GetUtcNow());
|
||||
|
||||
await ListRemoteAsync(session.HomeDirectory, cancellationToken).ConfigureAwait(true);
|
||||
@@ -1033,6 +1062,15 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
ConnectedHostKeyAlgorithm = opened.HostKey.Algorithm;
|
||||
ConnectedIdentityLabel = identityLabel;
|
||||
|
||||
// See ConnectedPinnedPaths's own remark for why this is a snapshot rather than a live follow.
|
||||
ConnectedPinnedPaths.Clear();
|
||||
foreach (var path in row.Host.PinnedPaths)
|
||||
{
|
||||
ConnectedPinnedPaths.Add(path);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasConnectedPins));
|
||||
|
||||
// Recorded, and not hidden because it is "only" the file browser. Opening this is a second login as
|
||||
// far as the remote's own auth.log is concerned, so a log of ours that omitted it would disagree with
|
||||
// the host's — and anybody comparing the two would be right to believe the host.
|
||||
@@ -1811,6 +1849,8 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
||||
ConnectedCipher = null;
|
||||
ConnectedHostKeyAlgorithm = null;
|
||||
ConnectedIdentityLabel = null;
|
||||
ConnectedPinnedPaths.Clear();
|
||||
OnPropertyChanged(nameof(HasConnectedPins));
|
||||
RemotePath = string.Empty;
|
||||
RemoteEntries.Clear();
|
||||
RemoteTrail.Clear();
|
||||
|
||||
Reference in New Issue
Block a user