diff --git a/azure-pipelines.yml b/azure-pipelines.yml index edf3c3d819..52fc8db53c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -59,7 +59,7 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' pool: - vmImage: 'macOS-10.15' + vmImage: 'macos-12' steps: - task: UseDotNet@2 displayName: 'Use .NET Core SDK 3.1.418' @@ -91,10 +91,10 @@ jobs: inputs: actions: 'build' scheme: '' - sdk: 'macosx11.1' + sdk: 'macosx12.3' configuration: 'Release' xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace' - xcodeVersion: '12' # Options: 8, 9, default, specifyPath + xcodeVersion: '13' # Options: 8, 9, default, specifyPath args: '-derivedDataPath ./' - task: CmdLine@2 diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b6db1255..036dccde0e 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -195,10 +195,10 @@ namespace ControlCatalog.Pages { // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER - await using var stream = await file.OpenWrite(); + await using var stream = await file.OpenWriteAsync(); await using var reader = new System.IO.StreamWriter(stream); #else - using var stream = await file.OpenWrite(); + using var stream = await file.OpenWriteAsync(); using var reader = new System.IO.StreamWriter(stream); #endif await reader.WriteLineAsync(openedFileContent.Text); @@ -243,8 +243,8 @@ namespace ControlCatalog.Pages async Task SetPickerResult(IReadOnlyCollection? items) { items ??= Array.Empty(); - var mappedResults = items.Select(FullPathOrName).ToList(); - bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark"; + bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmarkAsync() : "Can't bookmark"; + var mappedResults = new List(); if (items.FirstOrDefault() is IStorageItem item) { @@ -267,9 +267,9 @@ Content: if (file.CanOpenRead) { #if NET6_0_OR_GREATER - await using var stream = await file.OpenRead(); + await using var stream = await file.OpenReadAsync(); #else - using var stream = await file.OpenRead(); + using var stream = await file.OpenReadAsync(); #endif using var reader = new System.IO.StreamReader(stream); @@ -293,7 +293,19 @@ Content: lastSelectedDirectory = await item.GetParentAsync(); if (lastSelectedDirectory is not null) { - mappedResults.Insert(0, "Parent: " + FullPathOrName(lastSelectedDirectory)); + mappedResults.Add(FullPathOrName(lastSelectedDirectory)); + } + + foreach (var selectedItem in items) + { + mappedResults.Add("+> " + FullPathOrName(selectedItem)); + if (selectedItem is IStorageFolder folder) + { + foreach (var innerItems in await folder.GetItemsAsync()) + { + mappedResults.Add("++> " + FullPathOrName(innerItems)); + } + } } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 50581d47b1..a9b2e16d43 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -35,13 +36,13 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.FromResult(Uri.ToString()); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); return Task.CompletedTask; @@ -106,6 +107,30 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar { return Task.FromResult(new StorageItemProperties()); } + + public async Task> GetItemsAsync() + { + using var javaFile = new JavaFile(Uri.Path!); + + // Java file represents files AND directories. Don't be confused. + var files = await javaFile.ListFilesAsync().ConfigureAwait(false); + if (files is null) + { + return Array.Empty(); + } + + return files + .Select(f => (file: f, uri: AndroidUri.FromFile(f))) + .Where(t => t.uri is not null) + .Select(t => t.file switch + { + { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), + { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } } internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile @@ -118,10 +143,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public bool CanOpenWrite => true; - public Task OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false) + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); - public Task OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true) + public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) ?? throw new InvalidOperationException("Failed to open content stream")); private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5af02219ce..cf21e9b8b5 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -47,22 +47,22 @@ public class BclStorageFile : IStorageBookmarkFile return Task.FromResult(null); } - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(_fileInfo.OpenRead()); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(_fileInfo.OpenWrite()); } - public virtual Task SaveBookmark() + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_fileInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 7267017eaf..cd6c8be1ae 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Security; using System.Threading.Tasks; using Avalonia.Metadata; @@ -43,12 +45,22 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } - public virtual Task SaveBookmark() + public Task> GetItemsAsync() + { + var items = _directoryInfo.GetDirectories() + .Select(d => (IStorageItem)new BclStorageFolder(d)) + .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) + .ToArray(); + + return Task.FromResult>(items); + } + + public virtual Task SaveBookmarkAsync() { return Task.FromResult(_directoryInfo.FullName); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // No-op return Task.CompletedTask; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs index d21c950862..40f2720ee8 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageBookmarkItem : IStorageItem { - Task ReleaseBookmark(); + Task ReleaseBookmarkAsync(); } [NotClientImplementable] diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 965caf8216..46aa6efa72 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,7 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// - Task OpenRead(); + Task OpenReadAsync(); /// /// Returns true, if file is writeable. @@ -28,5 +28,5 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// - Task OpenWrite(); + Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 25b9f01a92..0ffb9f41c6 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -1,4 +1,6 @@ -using Avalonia.Metadata; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; namespace Avalonia.Platform.Storage; @@ -8,4 +10,11 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFolder : IStorageItem { + /// + /// Gets the files and subfolders in the current folder. + /// + /// + /// When this method completes successfully, it returns a list of the files and folders in the current folder. Each item in the list is represented by an implementation object. + /// + Task> GetItemsAsync(); } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index 8513ebc7d9..f5469d31c9 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -44,7 +44,7 @@ public interface IStorageItem : IDisposable /// /// Returns identifier of a bookmark. Can be null if OS denied request. /// - Task SaveBookmark(); + Task SaveBookmarkAsync(); /// /// Gets the parent folder of the current storage item. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6dba33516b..8e5d4e1e06 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Windows.Input; using Avalonia.Automation.Peers; @@ -281,24 +282,29 @@ namespace Avalonia.Controls /// protected override void OnKeyDown(KeyEventArgs e) { - if (e.Key == Key.Enter) + switch (e.Key) { - OnClick(); - e.Handled = true; - } - else if (e.Key == Key.Space) - { - if (ClickMode == ClickMode.Press) - { + case Key.Enter: OnClick(); + e.Handled = true; + break; + + case Key.Space: + { + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + + IsPressed = true; + e.Handled = true; + break; } - IsPressed = true; - e.Handled = true; - } - else if (e.Key == Key.Escape && Flyout != null) - { - // If Flyout doesn't have focusable content, close the flyout here - Flyout.Hide(); + + case Key.Escape when Flyout != null: + // If Flyout doesn't have focusable content, close the flyout here + CloseFlyout(); + break; } base.OnKeyDown(e); @@ -327,7 +333,14 @@ namespace Avalonia.Controls { if (IsEffectivelyEnabled) { - OpenFlyout(); + if (_isFlyoutOpen) + { + CloseFlyout(); + } + else + { + OpenFlyout(); + } var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); @@ -348,6 +361,14 @@ namespace Avalonia.Controls Flyout?.ShowAt(this); } + /// + /// Closes the button's flyout. + /// + protected virtual void CloseFlyout() + { + Flyout?.Hide(); + } + /// /// Invoked when the button's flyout is opened. /// @@ -494,8 +515,7 @@ namespace Avalonia.Controls // If flyout is changed while one is already open, make sure we // close the old one first - if (oldFlyout != null && - oldFlyout.IsOpen) + if (oldFlyout != null && oldFlyout.IsOpen) { oldFlyout.Hide(); } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 1504d2b25f..00ebcab70e 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -12,17 +12,12 @@ namespace Avalonia.Controls.Primitives { public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider { - static FlyoutBase() - { - Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); - } - /// /// Defines the property /// public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsOpen), - x => x.IsOpen); + AvaloniaProperty.RegisterDirect(nameof(IsOpen), + x => x.IsOpen); /// /// Defines the property @@ -43,6 +38,14 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.RegisterDirect(nameof(ShowMode), x => x.ShowMode, (x, v) => x.ShowMode = v); + /// + /// Defines the property + /// + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); + /// /// Defines the AttachedFlyout property /// @@ -57,6 +60,12 @@ namespace Avalonia.Controls.Primitives private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; private Action? _popupHostChangedHandler; + private IInputElement? _overlayInputPassThroughElement; + + static FlyoutBase() + { + Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); + } public FlyoutBase() { @@ -101,11 +110,21 @@ namespace Avalonia.Controls.Primitives private set => SetAndRaise(TargetProperty, ref _target, value); } + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. + /// + public IInputElement? OverlayInputPassThroughElement + { + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; remove => _popupHostChangedHandler -= value; } @@ -175,8 +194,9 @@ namespace Avalonia.Controls.Primitives IsOpen = false; Popup.IsOpen = false; + ((ISetLogicalParent)Popup).SetParent(null); - + // Ensure this isn't active _transientDisposable?.Dispose(); _transientDisposable = null; @@ -231,6 +251,8 @@ namespace Avalonia.Controls.Primitives Popup.Child = CreatePresenter(); } + Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; + if (CancelOpening()) { return false; @@ -356,10 +378,13 @@ namespace Avalonia.Controls.Primitives private Popup CreatePopup() { - var popup = new Popup(); - popup.WindowManagerAddShadowHint = false; - popup.IsLightDismissEnabled = true; - popup.OverlayDismissEventPassThrough = true; + var popup = new Popup + { + WindowManagerAddShadowHint = false, + IsLightDismissEnabled = true, + //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss. + OverlayDismissEventPassThrough = false + }; popup.Opened += OnPopupOpened; popup.Closed += OnPopupClosed; @@ -372,7 +397,7 @@ namespace Avalonia.Controls.Primitives { IsOpen = true; - _popupHostChangedHandler?.Invoke(Popup!.Host); + _popupHostChangedHandler?.Invoke(Popup.Host); } private void OnPopupClosing(object? sender, CancelEventArgs e) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1501d97470..3573ad9aaa 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -501,7 +501,7 @@ namespace Avalonia.Controls.Primitives if (dismissLayer != null) { dismissLayer.IsVisible = true; - dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; + dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement; Disposable.Create(() => { diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs index 14dc53d7b5..2bc46e97b5 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs @@ -145,7 +145,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage public bool CanBookmark => true; - public Task SaveBookmark() + public Task SaveBookmarkAsync() { return FileHandle.InvokeAsync("saveBookmark").AsTask(); } @@ -155,7 +155,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage return Task.FromResult(null); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { return FileHandle.InvokeAsync("deleteBookmark").AsTask(); } @@ -174,7 +174,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenRead => true; - public async Task OpenRead() + public async Task OpenReadAsync() { var stream = await FileHandle.InvokeAsync("openRead"); // Remove maxAllowedSize limit, as developer can decide if they read only small part or everything. @@ -182,7 +182,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage } public bool CanOpenWrite => true; - public async Task OpenWrite() + public async Task OpenWriteAsync() { var properties = await FileHandle.InvokeAsync("getProperties"); var streamWriter = await FileHandle.InvokeAsync("openWrite"); @@ -196,5 +196,30 @@ namespace Avalonia.Web.Blazor.Interop.Storage public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) { } + + public async Task> GetItemsAsync() + { + var items = await FileHandle.InvokeAsync("getItems"); + if (items is null) + { + return Array.Empty(); + } + + var count = items.Invoke("count"); + + return Enumerable.Range(0, count) + .Select(index => + { + var reference = items.Invoke("at", index); + return reference.Invoke("getKind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }; + }) + .Where(i => i is not null) + .ToArray()!; + } } } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts index c32eef3226..aee74b9067 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts +++ b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts @@ -14,6 +14,8 @@ declare global { queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; + + entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>; } type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; @@ -53,7 +55,7 @@ class IndexedDbWrapper { } public connect(): Promise { - var conn = window.indexedDB.open(this.databaseName, 1); + const conn = window.indexedDB.open(this.databaseName, 1); conn.onupgradeneeded = event => { const db = (>event.target).result; @@ -85,7 +87,7 @@ class InnerDbConnection { const os = this.openStore(store, "readwrite"); return new Promise((resolve, reject) => { - var response = os.put(obj, key); + const response = os.put(obj, key); response.onsuccess = () => { resolve(response.result); }; @@ -99,7 +101,7 @@ class InnerDbConnection { const os = this.openStore(store, "readonly"); return new Promise((resolve, reject) => { - var response = os.get(key); + const response = os.get(key); response.onsuccess = () => { resolve(response.result); }; @@ -113,7 +115,7 @@ class InnerDbConnection { const os = this.openStore(store, "readwrite"); return new Promise((resolve, reject) => { - var response = os.delete(key); + const response = os.delete(key); response.onsuccess = () => { resolve(); }; @@ -134,17 +136,20 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ ]) class StorageItem { - constructor(private handle: FileSystemFileHandle, private bookmarkId?: string) { } + constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { } public getName(): string { return this.handle.name } + public getKind(): string { + return this.handle.kind; + } + public async openRead(): Promise { await this.verityPermissions('read'); - var file = await this.handle.getFile(); - return file; + return await this.handle.getFile(); } public async openWrite(): Promise { @@ -154,7 +159,7 @@ class StorageItem { } public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> { - var file = this.handle.getFile && await this.handle.getFile(); + const file = this.handle.getFile && await this.handle.getFile(); return file && { Size: file.size, @@ -163,6 +168,18 @@ class StorageItem { } } + public async getItems(): Promise { + if (this.handle.kind !== "directory"){ + return new StorageItems([]); + } + + const items: StorageItem[] = []; + for await (const [key, value] of this.handle.entries()) { + items.push(new StorageItem(value)); + } + return new StorageItems(items); + } + private async verityPermissions(mode: PermissionsMode): Promise { if (await this.handle.queryPermission({ mode }) === 'granted') { return; @@ -235,12 +252,12 @@ export class StorageProvider { } public static async selectFolderDialog( - startIn: StartInDirectory | null) + startIn: StorageItem | null) : Promise { // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. const options: DirectoryPickerOptions = { - startIn: (startIn || undefined) + startIn: (startIn?.handle || undefined) }; const handle = await window.showDirectoryPicker(options); @@ -248,12 +265,12 @@ export class StorageProvider { } public static async openFileDialog( - startIn: StartInDirectory | null, multiple: boolean, + startIn: StorageItem | null, multiple: boolean, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) : Promise { const options: OpenFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), multiple, excludeAcceptAllOption, types: (types || undefined) @@ -264,12 +281,12 @@ export class StorageProvider { } public static async saveFileDialog( - startIn: StartInDirectory | null, suggestedName: string | null, + startIn: StorageItem | null, suggestedName: string | null, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) : Promise { const options: SaveFilePickerOptions = { - startIn: (startIn || undefined), + startIn: (startIn?.handle || undefined), suggestedName: (suggestedName || undefined), excludeAcceptAllOption, types: (types || undefined) diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 6fb296d0e0..a801e83562 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform.Storage; @@ -49,13 +51,13 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem return Task.FromResult(new IOSStorageFolder(Url.RemoveLastPathComponent())); } - public Task ReleaseBookmark() + public Task ReleaseBookmarkAsync() { // no-op return Task.CompletedTask; } - public Task SaveBookmark() + public Task SaveBookmarkAsync() { try { @@ -102,12 +104,12 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public bool CanOpenWrite => true; - public Task OpenRead() + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); } - public Task OpenWrite() + public Task OpenWriteAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Write)); } @@ -118,4 +120,19 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder public IOSStorageFolder(NSUrl url) : base(url) { } + + public Task> GetItemsAsync() + { + var content = NSFileManager.DefaultManager.GetDirectoryContent(Url, null, NSDirectoryEnumerationOptions.None, out var error); + if (error is not null) + { + return Task.FromException>(new NSErrorException(error)); + } + + var items = content + .Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u)) + .ToArray(); + + return Task.FromResult>(items); + } }