using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using DodoSSH.Client.Android.Platform; using DodoSSH.Client.Shell.ViewModels; namespace DodoSSH.Client.Android.Views; /// Designs 06 and 08 — one remote pane, over SFTP or over a bucket. internal sealed partial class FilesScreen : UserControl { public FilesScreen() => AvaloniaXamlLoader.Load(this); /// /// Opens a directory on a single tap. /// /// /// /// The desktop pane opens on a double tap and on Enter. A phone has neither, so the gesture has to be /// the one gesture there is — and that is why this is here rather than in the shared view model: making /// selection navigate would give the desktop a single click that walks into a directory. /// /// /// rather than a command on the row, because the row must stay a /// plain : a button as the item template swallows the press before the list sees it /// — it marks the pointer event handled, and the list's own handler is not registered for handled /// events — leaving nothing selected and every command that reads the selection doing nothing. Tapped /// fires after the list has moved its selection, which is what lets this read it. /// /// /// A file is left selected rather than downloaded, and that is deliberate now rather than forced: the /// actions below act on the selection, and a tap that started a transfer would make selecting a row to /// read its size the same gesture as fetching it. /// /// /// Tells the screen a row's tick moved, so the count above the listing keeps up. /// /// The desktop's own handler, for the reason given there: the count is a property of the collection /// rather than of any row, and each listing replaces every row it has. See /// TransfersViewModel.RemoteChecksChanged. /// /// An entry in the actions sheet was chosen: the sheet goes down as its command runs. /// /// Three of the four open a question under the listing, and the sheet left over it would be the one thing /// in the way of answering. The command is bound in the markup; this only lowers the sheet. /// private void OnEntryMenuChosen(object? sender, RoutedEventArgs e) { if (DataContext is TransfersViewModel transfers) { transfers.IsEntryMenuOpen = false; } } private void OnRemoteRowChecked(object? sender, RoutedEventArgs e) => (DataContext as TransfersViewModel)?.RemoteChecksChanged(); private void OnRemoteEntryTapped(object? sender, TappedEventArgs e) { if (DataContext is TransfersViewModel { SelectedRemoteEntry.IsNavigable: true } transfers) { transfers.OpenRemoteCommand.Execute(null); } } /// /// Picks documents in the system picker and queues them for upload. /// /// /// /// In the head rather than in the shared view model, for the reason every other platform difference is: /// the picker is Android's, the staging directory is this application's cache, and the desktop reaches /// its local files by browsing a pane that does not exist here. What crosses back into shared code is /// what the queue understands — paths — through . /// /// /// Failures land in the screen's own status line, which is where every other refusal on this /// screen already is. The picker itself is a trip out to another application, and it can come back with /// a document that has since been deleted or a grant that was revoked; the exception's message is more /// use than "the upload failed", and a phone has nowhere else to put it. /// /// private async void OnAddFiles(object? sender, RoutedEventArgs e) { if (DataContext is not TransfersViewModel transfers || TopLevel.GetTopLevel(this) is not { } top) { return; } try { var staged = await DocumentStaging.PickAsync(top, CancellationToken.None).ConfigureAwait(true); if (staged.Count == 0) { return; } transfers.QueueStagedUploads(staged); } catch (Exception exception) when (exception is not OutOfMemoryException) { transfers.Status = $"Those files could not be read: {exception.Message}"; } } /// /// Asks where the chosen remote file should be saved, then queues it. /// /// /// /// The other direction, and the asymmetry with is the platform's rather than /// this screen's: coming in, several documents can be pointed at in one trip; going out, the save picker /// names one destination, so this acts on the selected row. Asking for five destinations in a row to /// download five files would be a worse screen than pressing the button five times. /// /// /// Nothing is queued when the picker is dismissed. The document it makes when it is *not* dismissed /// exists from that moment, which is why the queueing follows immediately — see /// DocumentStaging.PickDestinationAsync. /// /// private async void OnSaveFile(object? sender, RoutedEventArgs e) { if (DataContext is not TransfersViewModel transfers || TopLevel.GetTopLevel(this) is not { } top) { return; } // From the actions sheet as well as the bar, and the sheet goes down either way: the save picker is // about to cover the screen, and coming back to the sheet still up would be coming back to a question // already answered. transfers.IsEntryMenuOpen = false; if (transfers.SelectedRemoteEntry is not { IsFile: true } row) { transfers.Status = "Choose a file on the host to save."; return; } try { if (await DocumentStaging.PickDestinationAsync(top, row.Name).ConfigureAwait(true) is not { } destination) { return; } transfers.QueueDeliveredDownload( row, DocumentStaging.NewStagingPath(row.Name), path => DocumentStaging.DeliverAsync(destination, path)); } catch (Exception exception) when (exception is not OutOfMemoryException) { transfers.Status = $"That file could not be saved: {exception.Message}"; } } }