From 59688302967ebbf541053b8d0392db84e8bec82c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 23:36:19 +0900 Subject: [PATCH 1/3] Update API, samples and BCL impl --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 4 ++-- samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs | 8 ++++++-- .../Platform/Storage/FileIO/BclStorageFolder.cs | 12 +++++++----- src/Avalonia.Base/Platform/Storage/IStorageFolder.cs | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e5f29abb68..02e3027aa6 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -324,9 +324,9 @@ namespace ControlCatalog.Pages mappedResults.Add("+> " + FullPathOrName(selectedItem)); if (selectedItem is IStorageFolder folder) { - foreach (var innerItems in await folder.GetItemsAsync()) + await foreach (var innerItem in folder.GetItemsAsync()) { - mappedResults.Add("++> " + FullPathOrName(innerItems)); + mappedResults.Add("++> " + FullPathOrName(innerItem)); } } } diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 26430b4b61..7fb5bec589 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -104,8 +104,12 @@ namespace ControlCatalog.Pages } else if (item is IStorageFolder folder) { - var items = await folder.GetItemsAsync(); - contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + var childrenCount = 0; + await foreach (var _ in folder.GetItemsAsync()) + { + childrenCount++; + } + contentStr += $"Folder {item.Name}: items {childrenCount}{Environment.NewLine}{Environment.NewLine}"; } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index d8e3d91f75..e6551390d6 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -57,14 +57,16 @@ internal class BclStorageFolder : IStorageBookmarkFolder return Task.FromResult(null); } - public Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { - var items = DirectoryInfo.GetDirectories() + var items = DirectoryInfo.EnumerateDirectories() .Select(d => (IStorageItem)new BclStorageFolder(d)) - .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f))) - .ToArray(); + .Concat(DirectoryInfo.EnumerateFiles().Select(f => new BclStorageFile(f))); - return Task.FromResult>(items); + foreach (var item in items) + { + yield return item; + } } public virtual Task SaveBookmarkAsync() diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs index 0ffb9f41c6..52b3256387 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -16,5 +16,5 @@ public interface IStorageFolder : IStorageItem /// /// 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(); + IAsyncEnumerable GetItemsAsync(); } From 6412da10cf5f04bdf75f0eec57dbe7570fb99791 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 23:36:27 +0900 Subject: [PATCH 2/3] Update mobile impl --- .../Platform/Storage/AndroidStorageItem.cs | 16 ++++++---------- src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs | 9 +++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 0a34e6077c..8052b3911a 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -131,19 +131,17 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder return Task.FromResult(new StorageItemProperties()); } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { if (!await EnsureExternalFilesPermission(false)) { - return Array.Empty(); + yield break; } - - List files = new List(); - + var contentResolver = Activity.ContentResolver; if (contentResolver == null) { - return files; + yield break; } var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri)); @@ -168,12 +166,10 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder continue; } - files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : - new AndroidStorageFile(Activity, uri)); + yield return mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) : + new AndroidStorageFile(Activity, uri); } } - - return files; } } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 27bd8faf64..baf31ebc73 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -114,8 +114,9 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder { } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { + // TODO: find out if it can be lazily enumerated. var tcs = new TaskCompletionSource>(); new NSFileCoordinator().CoordinateRead(Url, @@ -142,6 +143,10 @@ internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder throw new NSErrorException(error); } - return await tcs.Task; + var items = await tcs.Task; + foreach (var item in items) + { + yield return item; + } } } From 2fd5e6b9d9248195b1248036d9d2fc60b04cc65b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 23:36:41 +0900 Subject: [PATCH 3/3] Make browser GetItemsAsync trully async and lazy --- .../Interop/GeneralHelpers.cs | 16 +++++-- .../Avalonia.Browser/Interop/StorageHelper.cs | 6 +-- .../Storage/BrowserStorageProvider.cs | 47 ++++++++++++++----- .../webapp/modules/avalonia/generalHelpers.ts | 7 ++- .../webapp/modules/storage/storageItem.ts | 10 ++-- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs index 6e3b41c05b..67d1cfb776 100644 --- a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs +++ b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; namespace Avalonia.Browser.Interop; @@ -8,15 +9,22 @@ internal static partial class GeneralHelpers public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key); public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key); + [JSImport("GeneralHelpers.itemAt", AvaloniaModule.MainModuleName)] + public static partial JSObject ItemAtInt(JSObject jsObject, int key); + public static JSObject GetArrayItem(this JSObject jsObject, int key) => ItemAtInt(jsObject, key); + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key); public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key); [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] - public static partial string IntCallMethodString(JSObject jsObject, string name); + public static partial string IntCallMethodStr(JSObject jsObject, string name); + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodStrStr(JSObject jsObject, string name, string arg1); [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] - public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1); + public static partial Task IntCallMethodPromiseObj(JSObject jsObject, string name); - public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name); - public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1); + public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodStr(jsObject, name); + public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStrStr(jsObject, name, arg1); + public static Task CallMethodObjectAsync(this JSObject jsObject, string name) => IntCallMethodPromiseObj(jsObject, name); } diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 2d96ee8d1f..dc3372d2d0 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -40,9 +40,9 @@ internal static partial class StorageHelper [JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)] public static partial Task OpenRead(JSObject item); - [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)] - [return: JSMarshalAs>] - public static partial Task GetItems(JSObject item); + [JSImport("StorageItem.getItemsIterator", AvaloniaModule.StorageModuleName)] + [return: JSMarshalAs] + public static partial JSObject? GetItemsIterator(JSObject item); [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index fc32b3b4f7..fcb956f294 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -258,24 +258,45 @@ internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder { } - public async Task> GetItemsAsync() + public async IAsyncEnumerable GetItemsAsync() { - using var items = await StorageHelper.GetItems(FileHandle); - if (items is null) + using var itemsIterator = StorageHelper.GetItemsIterator(FileHandle); + if (itemsIterator is null) { - return Array.Empty(); + yield break; } - var itemsArray = StorageHelper.ItemsArray(items); + while (true) + { + var nextResult = await itemsIterator.CallMethodObjectAsync("next"); + if (nextResult is null) + { + yield break; + } + + var isDone = nextResult.GetPropertyAsBoolean("done"); + if (isDone) + { + yield break; + } - return itemsArray - .Select(reference => reference.GetPropertyAsString("kind") switch + var valArray = nextResult.GetPropertyAsJSObject("value"); + var storageItem = valArray?.GetArrayItem(1); // 0 - item name, 1 - item instance + if (storageItem is null) { - "directory" => (IStorageItem)new JSStorageFolder(reference), - "file" => new JSStorageFile(reference), - _ => null - }) - .Where(i => i is not null) - .ToArray()!; + yield break; + } + + var kind = storageItem.GetPropertyAsString("kind"); + switch (kind) + { + case "directory": + yield return new JSStorageFolder(storageItem); + break; + case "file": + yield return new JSStorageFile(storageItem); + break; + } + } } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts index fa001006ab..31d167e38d 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts @@ -1,5 +1,5 @@ export class GeneralHelpers { - public static itemsArrayAt(instance: any, key: string): any[] { + public static itemsArrayAt(instance: any, key: any): any[] { const items = instance[key]; if (!items) { return []; @@ -12,6 +12,11 @@ export class GeneralHelpers { return retItems; } + public static itemAt(instance: any, key: any): any { + const item = instance[key]; + return item; + } + public static callMethod(instance: any, name: string /*, args */): any { const args = Array.prototype.slice.call(arguments, 2); return instance[name].apply(instance, args); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index f444717094..399e268915 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -89,16 +89,12 @@ export class StorageItem { } } - public static async getItems(item: StorageItem): Promise { + public static getItemsIterator(item: StorageItem): any | null { if (item.kind !== "directory" || !item.handle) { - return new StorageItems([]); + return null; } - const items: StorageItem[] = []; - for await (const [, value] of (item.handle as any).entries()) { - items.push(new StorageItem(value)); - } - return new StorageItems(items); + return (item.handle as any).entries(); } private async verityPermissions(mode: "read" | "readwrite"): Promise {