70 changed files with 3369 additions and 905 deletions
@ -1,148 +0,0 @@ |
|||
{ |
|||
"$schema": "http://json-schema.org/draft-04/schema#", |
|||
"title": "Build Schema", |
|||
"$ref": "#/definitions/build", |
|||
"definitions": { |
|||
"build": { |
|||
"type": "object", |
|||
"properties": { |
|||
"Configuration": { |
|||
"type": "string", |
|||
"description": "configuration" |
|||
}, |
|||
"Continue": { |
|||
"type": "boolean", |
|||
"description": "Indicates to continue a previously failed build attempt" |
|||
}, |
|||
"ForceNugetVersion": { |
|||
"type": "string", |
|||
"description": "force-nuget-version" |
|||
}, |
|||
"Help": { |
|||
"type": "boolean", |
|||
"description": "Shows the help text for this build assembly" |
|||
}, |
|||
"Host": { |
|||
"type": "string", |
|||
"description": "Host for execution. Default is 'automatic'", |
|||
"enum": [ |
|||
"AppVeyor", |
|||
"AzurePipelines", |
|||
"Bamboo", |
|||
"Bitbucket", |
|||
"Bitrise", |
|||
"GitHubActions", |
|||
"GitLab", |
|||
"Jenkins", |
|||
"Rider", |
|||
"SpaceAutomation", |
|||
"TeamCity", |
|||
"Terminal", |
|||
"TravisCI", |
|||
"VisualStudio", |
|||
"VSCode" |
|||
] |
|||
}, |
|||
"NoLogo": { |
|||
"type": "boolean", |
|||
"description": "Disables displaying the NUKE logo" |
|||
}, |
|||
"Partition": { |
|||
"type": "string", |
|||
"description": "Partition to use on CI" |
|||
}, |
|||
"Plan": { |
|||
"type": "boolean", |
|||
"description": "Shows the execution plan (HTML)" |
|||
}, |
|||
"Profile": { |
|||
"type": "array", |
|||
"description": "Defines the profiles to load", |
|||
"items": { |
|||
"type": "string" |
|||
} |
|||
}, |
|||
"Root": { |
|||
"type": "string", |
|||
"description": "Root directory during build execution" |
|||
}, |
|||
"Skip": { |
|||
"type": "array", |
|||
"description": "List of targets to be skipped. Empty list skips all dependencies", |
|||
"items": { |
|||
"type": "string", |
|||
"enum": [ |
|||
"CiAzureLinux", |
|||
"CiAzureOSX", |
|||
"CiAzureWindows", |
|||
"Clean", |
|||
"Compile", |
|||
"CompileHtmlPreviewer", |
|||
"CompileNative", |
|||
"CreateIntermediateNugetPackages", |
|||
"CreateNugetPackages", |
|||
"GenerateCppHeaders", |
|||
"Package", |
|||
"RunCoreLibsTests", |
|||
"RunDesignerTests", |
|||
"RunHtmlPreviewerTests", |
|||
"RunLeakTests", |
|||
"RunRenderTests", |
|||
"RunTests", |
|||
"ZipFiles" |
|||
] |
|||
} |
|||
}, |
|||
"SkipPreviewer": { |
|||
"type": "boolean", |
|||
"description": "skip-previewer" |
|||
}, |
|||
"SkipTests": { |
|||
"type": "boolean", |
|||
"description": "skip-tests" |
|||
}, |
|||
"Solution": { |
|||
"type": "string", |
|||
"description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" |
|||
}, |
|||
"Target": { |
|||
"type": "array", |
|||
"description": "List of targets to be invoked. Default is '{default_target}'", |
|||
"items": { |
|||
"type": "string", |
|||
"enum": [ |
|||
"CiAzureLinux", |
|||
"CiAzureOSX", |
|||
"CiAzureWindows", |
|||
"Clean", |
|||
"Compile", |
|||
"CompileHtmlPreviewer", |
|||
"CompileNative", |
|||
"CreateIntermediateNugetPackages", |
|||
"CreateNugetPackages", |
|||
"GenerateCppHeaders", |
|||
"Package", |
|||
"RunCoreLibsTests", |
|||
"RunDesignerTests", |
|||
"RunHtmlPreviewerTests", |
|||
"RunLeakTests", |
|||
"RunRenderTests", |
|||
"RunTests", |
|||
"ZipFiles" |
|||
] |
|||
} |
|||
}, |
|||
"Verbosity": { |
|||
"type": "string", |
|||
"description": "Logging verbosity during build execution. Default is 'Normal'", |
|||
"enum": [ |
|||
"Minimal", |
|||
"Normal", |
|||
"Quiet", |
|||
"Verbose" |
|||
] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,4 +0,0 @@ |
|||
{ |
|||
"$schema": "./build.schema.json", |
|||
"Solution": "" |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
47be5be6de651ed0c6d6996318ce431a |
|||
GenerateCppHeaders |
|||
Clean |
|||
CompileNative |
|||
CompileHtmlPreviewer |
|||
@ -1,30 +0,0 @@ |
|||
V | | ArgumentsFromParametersFile.OnBuildCreated (150) |
|||
V | | Passing value for Build.Solution () |
|||
V | | InjectParameterValues.OnBuildCreated (100) |
|||
W | | Could not inject value for Build.Solution |
|||
System.ArgumentException: Solution 'C:\Users\User\repos\Avalonia\Avalonia.sln' contains duplicated NestedProjects entries: |
|||
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} |
|||
at Nuke.Common.Assert.True(Boolean condition, String message, String argumentExpression) in /_/source/Nuke.Common/Assert.cs:line 34 |
|||
at Nuke.Common.Utilities.Collections.EnumerableExtensions.ToDictionarySafe[T,TKey,TValue](IEnumerable`1 enumerable, Func`2 keySelector, Func`2 valueSelector, String duplicationMessage) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ToDictionary.cs:line 46 |
|||
at Nuke.Common.ProjectModel.SolutionSerializer.GetGlobalSection(String[] lines, String name, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 92 |
|||
at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromContent[T](String[] content, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 63 |
|||
at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromFile[T](String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 23 |
|||
V | | HandleShellCompletion.OnBuildCreated (75) |
|||
V | | GenerateBuildServerConfigurations.OnBuildCreated (50) |
|||
V | | InvokeBuildServerConfigurationGeneration.OnBuildCreated (45) |
|||
V | | UpdateNotification.OnBuildCreated (10) |
|||
V | | UnsetVisualStudioEnvironmentVariables.OnBuildCreated (0) |
|||
V | | HandleVisualStudioDebugging.OnBuildCreated (0) |
|||
V | | HandleSingleFileExecution.OnBuildCreated (-50) |
|||
V | | EventInvoker.OnBuildCreated (-3.4028235E+38) |
|||
V | | HandleHelpRequests.OnBuildInitialized (5) |
|||
V | | Telemetry.OnBuildInitialized (0) |
|||
V | | InjectNonParameterValues.OnBuildInitialized (-100) |
|||
V | | EventInvoker.OnBuildInitialized (-3.4028235E+38) |
|||
V | | Target-unrelated exception was thrown |
|||
System.NullReferenceException: Object reference not set to an instance of an object. |
|||
at Build.OnBuildInitialized() in C:\Users\User\repos\Avalonia\nukebuild\Build.cs:line 42 |
|||
at Nuke.Common.Execution.EventInvoker.OnBuildInitialized(NukeBuild build, IReadOnlyCollection`1 executableTargets, IReadOnlyCollection`1 executionPlan) in /_/source/Nuke.Common/Execution/EventInvoker.cs:line 27 |
|||
at Nuke.Common.Utilities.Collections.EnumerableExtensions.ForEach[T](IEnumerable`1 enumerable, Action`1 action) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ForEach.cs:line 17 |
|||
at Nuke.Common.NukeBuild.ExecuteExtension[TExtension](Expression`1 action) |
|||
at Nuke.Common.Execution.BuildManager.Execute[T](Expression`1[] defaultTargetExpressions) in /_/source/Nuke.Common/Execution/BuildManager.cs:line 58 |
|||
@ -1,7 +0,0 @@ |
|||
:; set -eo pipefail |
|||
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) |
|||
:; ${SCRIPT_DIR}/build.sh "$@" |
|||
:; exit $? |
|||
|
|||
@ECHO OFF |
|||
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Web; |
|||
|
|||
using ControlCatalog.Pages; |
|||
|
|||
namespace ControlCatalog.Web; |
|||
|
|||
public class EmbedSampleWeb : INativeDemoControl |
|||
{ |
|||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
|||
{ |
|||
if (isSecond) |
|||
{ |
|||
var iframe = EmbedInterop.CreateElement("iframe"); |
|||
iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70"); |
|||
|
|||
return new JSObjectControlHandle(iframe); |
|||
} |
|||
else |
|||
{ |
|||
var defaultHandle = (JSObjectControlHandle)createDefault(); |
|||
|
|||
_ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ => |
|||
{ |
|||
EmbedInterop.AddAppButton(defaultHandle.Object); |
|||
}); |
|||
|
|||
return defaultHandle; |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal static partial class EmbedInterop |
|||
{ |
|||
[JSImport("globalThis.document.createElement")] |
|||
public static partial JSObject CreateElement(string tagName); |
|||
|
|||
[JSImport("addAppButton", "embed.js")] |
|||
public static partial void AddAppButton(JSObject parentObject); |
|||
} |
|||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,38 @@ |
|||
#out { |
|||
height: 100vh; |
|||
width: 100vw |
|||
} |
|||
|
|||
#avalonia-splash { |
|||
position: absolute; |
|||
height: 100%; |
|||
width: 100%; |
|||
color: whitesmoke; |
|||
background: #171C2C; |
|||
font-family: 'Nunito', sans-serif; |
|||
} |
|||
|
|||
#avalonia-splash a{ |
|||
color: whitesmoke; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.center { |
|||
display: flex; |
|||
justify-content: center; |
|||
height: 250px; |
|||
} |
|||
|
|||
.splash-close { |
|||
animation: fadeOut 1s forwards; |
|||
} |
|||
|
|||
@keyframes fadeOut { |
|||
from { |
|||
opacity: 1; |
|||
} |
|||
|
|||
to { |
|||
opacity: 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
export function addAppButton(parent) { |
|||
var button = globalThis.document.createElement('button'); |
|||
button.innerText = 'Hello world'; |
|||
var clickCount = 0; |
|||
button.onclick = () => { |
|||
clickCount++; |
|||
button.innerText = 'Click count ' + clickCount; |
|||
}; |
|||
parent.appendChild(button); |
|||
return button; |
|||
} |
|||
|
After Width: | Height: | Size: 172 KiB |
@ -0,0 +1,136 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
|
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Web.Interop; |
|||
|
|||
namespace Avalonia.Web |
|||
{ |
|||
internal class BrowserNativeControlHost : INativeControlHostImpl |
|||
{ |
|||
private readonly JSObject _hostElement; |
|||
|
|||
public BrowserNativeControlHost(JSObject element) |
|||
{ |
|||
_hostElement = element; |
|||
} |
|||
|
|||
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) |
|||
{ |
|||
var element = NativeControlHostHelper.CreateDefaultChild(null); |
|||
return new JSObjectControlHandle(element); |
|||
} |
|||
|
|||
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create) |
|||
{ |
|||
Attachment? a = null; |
|||
try |
|||
{ |
|||
var child = create(new JSObjectControlHandle(_hostElement)); |
|||
var attachmenetReference = NativeControlHostHelper.CreateAttachment(); |
|||
// It has to be assigned to the variable before property setter is called so we dispose it on exception
|
|||
#pragma warning disable IDE0017 // Simplify object initialization
|
|||
a = new Attachment(attachmenetReference, child); |
|||
#pragma warning restore IDE0017 // Simplify object initialization
|
|||
a.AttachedTo = this; |
|||
return a; |
|||
} |
|||
catch |
|||
{ |
|||
a?.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) |
|||
{ |
|||
var attachmenetReference = NativeControlHostHelper.CreateAttachment(); |
|||
var a = new Attachment(attachmenetReference, handle); |
|||
a.AttachedTo = this; |
|||
return a; |
|||
} |
|||
|
|||
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; |
|||
|
|||
private class Attachment : INativeControlHostControlTopLevelAttachment |
|||
{ |
|||
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; |
|||
private const string AttachToSymbol = "AttachTo"; |
|||
private const string ShowInBoundsSymbol = "ShowInBounds"; |
|||
private const string HideWithSizeSymbol = "HideWithSize"; |
|||
private const string ReleaseChildSymbol = "ReleaseChild"; |
|||
|
|||
private JSObject? _native; |
|||
private BrowserNativeControlHost? _attachedTo; |
|||
|
|||
public Attachment(JSObject native, IPlatformHandle handle) |
|||
{ |
|||
_native = native; |
|||
NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_native != null) |
|||
{ |
|||
NativeControlHostHelper.ReleaseChild(_native); |
|||
_native.Dispose(); |
|||
_native = null; |
|||
} |
|||
} |
|||
|
|||
public INativeControlHostImpl? AttachedTo |
|||
{ |
|||
get => _attachedTo!; |
|||
set |
|||
{ |
|||
CheckDisposed(); |
|||
|
|||
var host = (BrowserNativeControlHost?)value; |
|||
if (host == null) |
|||
{ |
|||
NativeControlHostHelper.AttachTo(_native, null); |
|||
} |
|||
else |
|||
{ |
|||
NativeControlHostHelper.AttachTo(_native, host._hostElement); |
|||
} |
|||
_attachedTo = host; |
|||
} |
|||
} |
|||
|
|||
public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost; |
|||
|
|||
public void HideWithSize(Size size) |
|||
{ |
|||
CheckDisposed(); |
|||
if (_attachedTo == null) |
|||
return; |
|||
|
|||
NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, size.Height)); |
|||
} |
|||
|
|||
public void ShowInBounds(Rect bounds) |
|||
{ |
|||
CheckDisposed(); |
|||
|
|||
if (_attachedTo == null) |
|||
throw new InvalidOperationException("Native control isn't attached to a toplevel"); |
|||
|
|||
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), |
|||
Math.Max(1, bounds.Height)); |
|||
|
|||
NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height); |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_native))] |
|||
private void CheckDisposed() |
|||
{ |
|||
if (_native == null) |
|||
throw new ObjectDisposedException(nameof(Attachment)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
|
|||
namespace Avalonia.Web.Interop; |
|||
|
|||
internal static partial class NativeControlHostHelper |
|||
{ |
|||
[JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")] |
|||
internal static partial JSObject CreateDefaultChild(JSObject? parent); |
|||
|
|||
[JSImport("NativeControlHost.createAttachment", "avalonia.ts")] |
|||
internal static partial JSObject CreateAttachment(); |
|||
|
|||
[JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")] |
|||
internal static partial void InitializeWithChildHandle(JSObject element, JSObject child); |
|||
|
|||
[JSImport("NativeControlHost.attachTo", "avalonia.ts")] |
|||
internal static partial void AttachTo(JSObject element, JSObject? host); |
|||
|
|||
[JSImport("NativeControlHost.showInBounds", "avalonia.ts")] |
|||
internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height); |
|||
|
|||
[JSImport("NativeControlHost.hideWithSize", "avalonia.ts")] |
|||
internal static partial void HideWithSize(JSObject element, double width, double height); |
|||
|
|||
[JSImport("NativeControlHost.releaseChild", "avalonia.ts")] |
|||
internal static partial void ReleaseChild(JSObject element); |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Web.Interop; |
|||
|
|||
internal static partial class StorageHelper |
|||
{ |
|||
[JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")] |
|||
public static partial bool CanShowOpenFilePicker(); |
|||
|
|||
[JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")] |
|||
public static partial bool CanShowSaveFilePicker(); |
|||
|
|||
[JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")] |
|||
public static partial bool CanShowDirectoryPicker(); |
|||
|
|||
[JSImport("StorageProvider.selectFolderDialog", "storage.ts")] |
|||
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn); |
|||
|
|||
[JSImport("StorageProvider.openFileDialog", "storage.ts")] |
|||
public static partial Task<JSObject?> OpenFileDialog(JSObject? startIn, bool multiple, |
|||
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption); |
|||
|
|||
[JSImport("StorageProvider.saveFileDialog", "storage.ts")] |
|||
public static partial Task<JSObject?> SaveFileDialog(JSObject? startIn, string? suggestedName, |
|||
[JSMarshalAs<JSType.Array<JSType.Any>>] object[]? types, bool excludeAcceptAllOption); |
|||
|
|||
[JSImport("StorageProvider.openBookmark", "storage.ts")] |
|||
public static partial Task<JSObject?> OpenBookmark(string key); |
|||
|
|||
[JSImport("StorageItem.saveBookmark", "storage.ts")] |
|||
public static partial Task<string?> SaveBookmark(JSObject item); |
|||
|
|||
[JSImport("StorageItem.deleteBookmark", "storage.ts")] |
|||
public static partial Task DeleteBookmark(JSObject item); |
|||
|
|||
[JSImport("StorageItem.getProperties", "storage.ts")] |
|||
public static partial Task<JSObject?> GetProperties(JSObject item); |
|||
|
|||
[JSImport("StorageItem.openWrite", "storage.ts")] |
|||
public static partial Task<JSObject> OpenWrite(JSObject item); |
|||
|
|||
[JSImport("StorageItem.openRead", "storage.ts")] |
|||
public static partial Task<JSObject> OpenRead(JSObject item); |
|||
|
|||
[JSImport("StorageItem.getItems", "storage.ts")] |
|||
[return: JSMarshalAs<JSType.Promise<JSType.Object>>] |
|||
public static partial Task<JSObject> GetItems(JSObject item); |
|||
|
|||
[JSImport("StorageItems.itemsArray", "storage.ts")] |
|||
public static partial JSObject[] ItemsArray(JSObject item); |
|||
|
|||
[JSImport("StorageProvider.createAcceptType", "storage.ts")] |
|||
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
|
|||
using Avalonia.Controls.Platform; |
|||
|
|||
namespace Avalonia.Web; |
|||
|
|||
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle |
|||
{ |
|||
internal const string ElementReferenceDescriptor = "JSObject"; |
|||
|
|||
public JSObjectControlHandle(JSObject reference) |
|||
{ |
|||
Object = reference; |
|||
} |
|||
|
|||
public JSObject Object { get; } |
|||
|
|||
public IntPtr Handle => throw new NotSupportedException(); |
|||
|
|||
public string? HandleDescriptor => ElementReferenceDescriptor; |
|||
|
|||
public void Destroy() |
|||
{ |
|||
if (Object is JSObject inProcess && !inProcess.IsDisposed) |
|||
{ |
|||
inProcess.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Skia; |
|||
|
|||
namespace Avalonia.Web |
|||
namespace Avalonia.Web.Skia |
|||
{ |
|||
public class BrowserSkiaGpu : ISkiaGpu |
|||
{ |
|||
@ -1,7 +1,7 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web |
|||
namespace Avalonia.Web.Skia |
|||
{ |
|||
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession |
|||
{ |
|||
@ -1,7 +1,7 @@ |
|||
using Avalonia.Skia; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Web |
|||
namespace Avalonia.Web.Skia |
|||
{ |
|||
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
|||
{ |
|||
@ -1,4 +1,4 @@ |
|||
namespace Avalonia.Web |
|||
namespace Avalonia.Web.Skia |
|||
{ |
|||
internal interface IBrowserSkiaSurface |
|||
{ |
|||
@ -0,0 +1,257 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Runtime.Versioning; |
|||
using System.Threading.Tasks; |
|||
|
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Web.Interop; |
|||
|
|||
namespace Avalonia.Web.Storage; |
|||
|
|||
internal record FilePickerAcceptType(string Description, IReadOnlyDictionary<string, IReadOnlyList<string>> Accept); |
|||
|
|||
[SupportedOSPlatform("browser")] |
|||
internal class BrowserStorageProvider : IStorageProvider |
|||
{ |
|||
internal const string PickerCancelMessage = "The user aborted a request"; |
|||
internal const string NoPermissionsMessage = "Permissions denied"; |
|||
|
|||
private readonly Lazy<Task<JSObject>> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js")); |
|||
|
|||
public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); |
|||
public bool CanSave => StorageHelper.CanShowSaveFilePicker(); |
|||
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); |
|||
|
|||
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); |
|||
|
|||
try |
|||
{ |
|||
using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); |
|||
if (items is null) |
|||
{ |
|||
return Array.Empty<IStorageFile>(); |
|||
} |
|||
|
|||
var itemsArray = StorageHelper.ItemsArray(items); |
|||
return itemsArray.Select(item => new JSStorageFile(item)).ToArray(); |
|||
} |
|||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
|||
{ |
|||
return Array.Empty<IStorageFile>(); |
|||
} |
|||
finally |
|||
{ |
|||
if (types is not null) |
|||
{ |
|||
foreach (var type in types) |
|||
{ |
|||
type.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); |
|||
|
|||
try |
|||
{ |
|||
var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll); |
|||
return item is not null ? new JSStorageFile(item) : null; |
|||
} |
|||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
|||
{ |
|||
return null; |
|||
} |
|||
finally |
|||
{ |
|||
if (types is not null) |
|||
{ |
|||
foreach (var type in types) |
|||
{ |
|||
type.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; |
|||
|
|||
try |
|||
{ |
|||
var item = await StorageHelper.SelectFolderDialog(startIn); |
|||
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty<IStorageFolder>(); |
|||
} |
|||
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) |
|||
{ |
|||
return Array.Empty<IStorageFolder>(); |
|||
} |
|||
} |
|||
|
|||
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var item = await StorageHelper.OpenBookmark(bookmark); |
|||
return item is not null ? new JSStorageFile(item) : null; |
|||
} |
|||
|
|||
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) |
|||
{ |
|||
_ = await _lazyModule.Value; |
|||
var item = await StorageHelper.OpenBookmark(bookmark); |
|||
return item is not null ? new JSStorageFolder(item) : null; |
|||
} |
|||
|
|||
private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable<FilePickerFileType>? input) |
|||
{ |
|||
var types = input? |
|||
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) |
|||
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) |
|||
.ToArray(); |
|||
if (types?.Length == 0) |
|||
{ |
|||
types = null; |
|||
} |
|||
|
|||
var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; |
|||
|
|||
return (types, !inlcudeAll); |
|||
} |
|||
} |
|||
|
|||
internal abstract class JSStorageItem : IStorageBookmarkItem |
|||
{ |
|||
internal JSObject? _fileHandle; |
|||
|
|||
protected JSStorageItem(JSObject fileHandle) |
|||
{ |
|||
_fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); |
|||
} |
|||
|
|||
internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); |
|||
|
|||
public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty; |
|||
|
|||
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) |
|||
{ |
|||
uri = new Uri(Name, UriKind.Relative); |
|||
return false; |
|||
} |
|||
|
|||
public async Task<StorageItemProperties> GetBasicPropertiesAsync() |
|||
{ |
|||
using var properties = await StorageHelper.GetProperties(FileHandle); |
|||
var size = (long?)properties?.GetPropertyAsDouble("Size"); |
|||
var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified"); |
|||
|
|||
return new StorageItemProperties( |
|||
(ulong?)size, |
|||
dateCreated: null, |
|||
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); |
|||
} |
|||
|
|||
public bool CanBookmark => true; |
|||
|
|||
public Task<string?> SaveBookmarkAsync() |
|||
{ |
|||
return StorageHelper.SaveBookmark(FileHandle); |
|||
} |
|||
|
|||
public Task<IStorageFolder?> GetParentAsync() |
|||
{ |
|||
return Task.FromResult<IStorageFolder?>(null); |
|||
} |
|||
|
|||
public Task ReleaseBookmarkAsync() |
|||
{ |
|||
return StorageHelper.DeleteBookmark(FileHandle); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_fileHandle?.Dispose(); |
|||
_fileHandle = null; |
|||
} |
|||
} |
|||
|
|||
internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile |
|||
{ |
|||
public JSStorageFile(JSObject fileHandle) : base(fileHandle) |
|||
{ |
|||
} |
|||
|
|||
public bool CanOpenRead => true; |
|||
public async Task<Stream> OpenReadAsync() |
|||
{ |
|||
try |
|||
{ |
|||
var blob = await StorageHelper.OpenRead(FileHandle); |
|||
return new BlobReadableStream(blob); |
|||
} |
|||
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) |
|||
{ |
|||
throw new UnauthorizedAccessException("User denied permissions to open the file", ex); |
|||
} |
|||
} |
|||
|
|||
public bool CanOpenWrite => true; |
|||
public async Task<Stream> OpenWriteAsync() |
|||
{ |
|||
try |
|||
{ |
|||
using var properties = await StorageHelper.GetProperties(FileHandle); |
|||
var streamWriter = await StorageHelper.OpenWrite(FileHandle); |
|||
var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0; |
|||
|
|||
return new WriteableStream(streamWriter, size); |
|||
} |
|||
catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) |
|||
{ |
|||
throw new UnauthorizedAccessException("User denied permissions to open the file", ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder |
|||
{ |
|||
public JSStorageFolder(JSObject fileHandle) : base(fileHandle) |
|||
{ |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() |
|||
{ |
|||
using var items = await StorageHelper.GetItems(FileHandle); |
|||
if (items is null) |
|||
{ |
|||
return Array.Empty<IStorageItem>(); |
|||
} |
|||
|
|||
var itemsArray = StorageHelper.ItemsArray(items); |
|||
|
|||
return itemsArray |
|||
.Select(reference => reference.GetPropertyAsString("kind") switch |
|||
{ |
|||
"directory" => (IStorageItem)new JSStorageFolder(reference), |
|||
"file" => new JSStorageFile(reference), |
|||
_ => null |
|||
}) |
|||
.Where(i => i is not null) |
|||
.ToArray()!; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
using System; |
|||
using Avalonia.Controls.Embedding; |
|||
using Avalonia.Media; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Rendering.SceneGraph; |
|||
|
|||
namespace Avalonia.Web |
|||
{ |
|||
internal class WebEmbeddableControlRoot : EmbeddableControlRoot |
|||
{ |
|||
class SplashScreenCloseCustomDrawingOperation : ICustomDrawOperation |
|||
{ |
|||
private bool _hasRendered; |
|||
private Action _onFirstRender; |
|||
|
|||
public SplashScreenCloseCustomDrawingOperation(Action onFirstRender) |
|||
{ |
|||
_onFirstRender = onFirstRender; |
|||
} |
|||
|
|||
public Rect Bounds => Rect.Empty; |
|||
|
|||
public bool HasRendered => _hasRendered; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public bool Equals(ICustomDrawOperation? other) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
public bool HitTest(Point p) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
public void Render(IDrawingContextImpl context) |
|||
{ |
|||
_hasRendered = true; |
|||
_onFirstRender(); |
|||
} |
|||
} |
|||
|
|||
public WebEmbeddableControlRoot(ITopLevelImpl impl, Action onFirstRender) : base(impl) |
|||
{ |
|||
_splashCloseOp = new SplashScreenCloseCustomDrawingOperation(() => |
|||
{ |
|||
_splashCloseOp = null; |
|||
onFirstRender(); |
|||
}); |
|||
} |
|||
|
|||
private SplashScreenCloseCustomDrawingOperation? _splashCloseOp; |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
base.Render(context); |
|||
|
|||
if (_splashCloseOp != null) |
|||
{ |
|||
context.Custom(_splashCloseOp); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
{ |
|||
"env": { |
|||
"browser": true, |
|||
"es6": true |
|||
}, |
|||
"extends": "standard-with-typescript", |
|||
"overrides": [], |
|||
"parserOptions": { |
|||
"ecmaVersion": "latest", |
|||
"sourceType": "module", |
|||
"project": [ |
|||
"tsconfig.json" |
|||
] |
|||
}, |
|||
"rules": { |
|||
"indent": [ |
|||
"warn", |
|||
4 |
|||
], |
|||
"@typescript-eslint/indent": [ |
|||
"warn", |
|||
4 |
|||
], |
|||
"quotes": ["warn", "double"], |
|||
"semi": ["error", "always"], |
|||
"@typescript-eslint/quotes": ["warn", "double"], |
|||
"@typescript-eslint/explicit-function-return-type": "off", |
|||
"@typescript-eslint/no-extraneous-class": "off", |
|||
"@typescript-eslint/strict-boolean-expressions": "off", |
|||
"@typescript-eslint/space-before-function-paren": "off", |
|||
"@typescript-eslint/semi": ["error", "always"], |
|||
"@typescript-eslint/member-delimiter-style": [ |
|||
"error", |
|||
{ |
|||
"multiline": { |
|||
"delimiter": "semi", |
|||
"requireLast": true |
|||
}, |
|||
"singleline": { |
|||
"delimiter": "semi", |
|||
"requireLast": false |
|||
} |
|||
} |
|||
] |
|||
}, |
|||
"ignorePatterns": ["types/*"] |
|||
} |
|||
@ -1,18 +1,20 @@ |
|||
import { RuntimeAPI } from "../types/dotnet"; |
|||
import { SizeWatcher } from "./avalonia/canvas"; |
|||
import { DpiWatcher } from "./avalonia/canvas"; |
|||
import { Canvas } from "./avalonia/canvas"; |
|||
import { RuntimeAPI } from "../types/dotnet"; |
|||
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; |
|||
import { InputHelper } from "./avalonia/input"; |
|||
import { AvaloniaDOM } from "./avalonia/dom"; |
|||
import { CaretHelper } from "./avalonia/CaretHelper" |
|||
import { Caniuse } from "./avalonia/caniuse"; |
|||
import { StreamHelper } from "./avalonia/stream"; |
|||
import { NativeControlHost } from "./avalonia/NativeControlHost"; |
|||
|
|||
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> { |
|||
api.setModuleImports("avalonia.ts", { |
|||
Caniuse, |
|||
Canvas, |
|||
InputHelper, |
|||
SizeWatcher, |
|||
DpiWatcher, |
|||
AvaloniaDOM, |
|||
CaretHelper |
|||
StreamHelper, |
|||
NativeControlHost |
|||
}); |
|||
} |
|||
|
|||
@ -0,0 +1,13 @@ |
|||
export class Caniuse { |
|||
public static canShowOpenFilePicker(): boolean { |
|||
return typeof window.showOpenFilePicker !== "undefined"; |
|||
} |
|||
|
|||
public static canShowSaveFilePicker(): boolean { |
|||
return typeof window.showSaveFilePicker !== "undefined"; |
|||
} |
|||
|
|||
public static canShowDirectoryPicker(): boolean { |
|||
return typeof window.showDirectoryPicker !== "undefined"; |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
class NativeControlHostTopLevelAttachment { |
|||
_child?: HTMLElement; |
|||
_host?: HTMLElement; |
|||
} |
|||
|
|||
export class NativeControlHost { |
|||
public static createDefaultChild(parent?: HTMLElement): HTMLElement { |
|||
return document.createElement("div"); |
|||
} |
|||
|
|||
public static createAttachment(): NativeControlHostTopLevelAttachment { |
|||
return new NativeControlHostTopLevelAttachment(); |
|||
} |
|||
|
|||
public static initializeWithChildHandle(element: NativeControlHostTopLevelAttachment, child: HTMLElement): void { |
|||
element._child = child; |
|||
element._child.style.position = "absolute"; |
|||
} |
|||
|
|||
public static attachTo(element: NativeControlHostTopLevelAttachment, host?: HTMLElement): void { |
|||
if (element._host && element._child) { |
|||
element._host.removeChild(element._child); |
|||
} |
|||
|
|||
element._host = host; |
|||
|
|||
if (element._host && element._child) { |
|||
element._host.appendChild(element._child); |
|||
} |
|||
} |
|||
|
|||
public static showInBounds(element: NativeControlHostTopLevelAttachment, x: number, y: number, width: number, height: number): void { |
|||
if (element._child) { |
|||
element._child.style.top = `${y}px`; |
|||
element._child.style.left = `${x}px`; |
|||
element._child.style.width = `${width}px`; |
|||
element._child.style.height = `${height}px`; |
|||
element._child.style.display = "block"; |
|||
} |
|||
} |
|||
|
|||
public static hideWithSize(element: NativeControlHostTopLevelAttachment, width: number, height: number): void { |
|||
if (element._child) { |
|||
element._child.style.width = `${width}px`; |
|||
element._child.style.height = `${height}px`; |
|||
element._child.style.display = "none"; |
|||
} |
|||
} |
|||
|
|||
public static releaseChild(element: NativeControlHostTopLevelAttachment): void { |
|||
if (element._child) { |
|||
element._child = undefined; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
import { IMemoryView } from "../../types/dotnet"; |
|||
|
|||
export class StreamHelper { |
|||
public static async seek(stream: FileSystemWritableFileStream, position: number) { |
|||
return await stream.seek(position); |
|||
} |
|||
|
|||
public static async truncate(stream: FileSystemWritableFileStream, size: number) { |
|||
return await stream.truncate(size); |
|||
} |
|||
|
|||
public static async close(stream: FileSystemWritableFileStream) { |
|||
return await stream.close(); |
|||
} |
|||
|
|||
public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) { |
|||
const array = new Uint8Array(span.byteLength); |
|||
span.copyTo(array); |
|||
|
|||
const data: WriteParams = { |
|||
type: "write", |
|||
data: array |
|||
}; |
|||
|
|||
return await stream.write(data); |
|||
} |
|||
|
|||
public static byteLength(stream: Blob) { |
|||
return stream.size; |
|||
} |
|||
|
|||
public static async sliceArrayBuffer(stream: Blob, offset: number, count: number) { |
|||
const buffer = await stream.slice(offset, offset + count).arrayBuffer(); |
|||
return new Uint8Array(buffer); |
|||
} |
|||
|
|||
public static toMemoryView(buffer: Uint8Array): Uint8Array { |
|||
return buffer; |
|||
} |
|||
} |
|||
@ -1,5 +1,2 @@ |
|||
export class StorageProvider { |
|||
static isFileApiSupported(): boolean { |
|||
return (globalThis as any).showOpenFilePicker !== undefined; |
|||
} |
|||
} |
|||
export { StorageItem, StorageItems } from "./storage/storageItem"; |
|||
export { StorageProvider } from "./storage/storageProvider"; |
|||
|
|||
@ -0,0 +1,84 @@ |
|||
class InnerDbConnection { |
|||
constructor(private readonly database: IDBDatabase) { } |
|||
|
|||
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { |
|||
const tx = this.database.transaction(store, mode); |
|||
return tx.objectStore(store); |
|||
} |
|||
|
|||
public async put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return await new Promise((resolve, reject) => { |
|||
const response = os.put(obj, key); |
|||
response.onsuccess = () => { |
|||
resolve(response.result); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public get(store: string, key: IDBValidKey): any { |
|||
const os = this.openStore(store, "readonly"); |
|||
|
|||
return new Promise((resolve, reject) => { |
|||
const response = os.get(key); |
|||
response.onsuccess = () => { |
|||
resolve(response.result); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public async delete(store: string, key: IDBValidKey): Promise<void> { |
|||
const os = this.openStore(store, "readwrite"); |
|||
|
|||
return await new Promise((resolve, reject) => { |
|||
const response = os.delete(key); |
|||
response.onsuccess = () => { |
|||
resolve(); |
|||
}; |
|||
response.onerror = () => { |
|||
reject(response.error); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
public close() { |
|||
this.database.close(); |
|||
} |
|||
} |
|||
|
|||
class IndexedDbWrapper { |
|||
constructor(private readonly databaseName: string, private readonly objectStores: [string]) { |
|||
} |
|||
|
|||
public async connect(): Promise<InnerDbConnection> { |
|||
const conn = window.indexedDB.open(this.databaseName, 1); |
|||
|
|||
conn.onupgradeneeded = event => { |
|||
const db = (event.target as IDBRequest<IDBDatabase>).result; |
|||
this.objectStores.forEach(store => { |
|||
db.createObjectStore(store); |
|||
}); |
|||
}; |
|||
|
|||
return await new Promise((resolve, reject) => { |
|||
conn.onsuccess = event => { |
|||
resolve(new InnerDbConnection((event.target as IDBRequest<IDBDatabase>).result)); |
|||
}; |
|||
conn.onerror = event => { |
|||
reject((event.target as IDBRequest<IDBDatabase>).error); |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export const fileBookmarksStore: string = "fileBookmarks"; |
|||
export const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ |
|||
fileBookmarksStore |
|||
]); |
|||
@ -0,0 +1,111 @@ |
|||
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; |
|||
|
|||
export class StorageItem { |
|||
constructor(public handle: FileSystemHandle, private readonly bookmarkId?: string) { } |
|||
|
|||
public get name(): string { |
|||
return this.handle.name; |
|||
} |
|||
|
|||
public get kind(): string { |
|||
return this.handle.kind; |
|||
} |
|||
|
|||
public static async openRead(item: StorageItem): Promise<Blob> { |
|||
if (!(item.handle instanceof FileSystemFileHandle)) { |
|||
throw new Error("StorageItem is not a file"); |
|||
} |
|||
|
|||
await item.verityPermissions("read"); |
|||
|
|||
const file = await item.handle.getFile(); |
|||
return file; |
|||
} |
|||
|
|||
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> { |
|||
if (!(item.handle instanceof FileSystemFileHandle)) { |
|||
throw new Error("StorageItem is not a file"); |
|||
} |
|||
|
|||
await item.verityPermissions("readwrite"); |
|||
|
|||
return await item.handle.createWritable({ keepExistingData: true }); |
|||
} |
|||
|
|||
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { |
|||
const file = item.handle instanceof FileSystemFileHandle && |
|||
await item.handle.getFile(); |
|||
|
|||
if (!file) { |
|||
return null; |
|||
} |
|||
|
|||
return { |
|||
Size: file.size, |
|||
LastModified: file.lastModified, |
|||
Type: file.type |
|||
}; |
|||
} |
|||
|
|||
public static async getItems(item: StorageItem): Promise<StorageItems> { |
|||
if (item.handle.kind !== "directory") { |
|||
return new StorageItems([]); |
|||
} |
|||
|
|||
const items: StorageItem[] = []; |
|||
for await (const [, value] of (item.handle as any).entries()) { |
|||
items.push(new StorageItem(value)); |
|||
} |
|||
return new StorageItems(items); |
|||
} |
|||
|
|||
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> { |
|||
if (await this.handle.queryPermission({ mode }) === "granted") { |
|||
return; |
|||
} |
|||
|
|||
if (await this.handle.requestPermission({ mode }) === "denied") { |
|||
throw new Error("Permissions denied"); |
|||
} |
|||
} |
|||
|
|||
public static async saveBookmark(item: StorageItem): Promise<string> { |
|||
// If file was previously bookmarked, just return old one.
|
|||
if (item.bookmarkId) { |
|||
return item.bookmarkId; |
|||
} |
|||
|
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId()); |
|||
return key as string; |
|||
} finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
public static async deleteBookmark(item: StorageItem): Promise<void> { |
|||
if (!item.bookmarkId) { |
|||
return; |
|||
} |
|||
|
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
await connection.delete(fileBookmarksStore, item.bookmarkId); |
|||
} finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
private generateBookmarkId(): string { |
|||
return Date.now().toString(36) + Math.random().toString(36).substring(2); |
|||
} |
|||
} |
|||
|
|||
export class StorageItems { |
|||
constructor(private readonly items: StorageItem[]) { } |
|||
|
|||
public static itemsArray(instance: StorageItems): StorageItem[] { |
|||
return instance.items; |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; |
|||
import { StorageItem, StorageItems } from "./storageItem"; |
|||
|
|||
declare global { |
|||
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; |
|||
type StartInDirectory = WellKnownDirectory | FileSystemHandle; |
|||
interface OpenFilePickerOptions { |
|||
startIn?: StartInDirectory; |
|||
} |
|||
interface SaveFilePickerOptions { |
|||
startIn?: StartInDirectory; |
|||
} |
|||
} |
|||
|
|||
export class StorageProvider { |
|||
public static async selectFolderDialog( |
|||
startIn: StorageItem | null): Promise<StorageItem> { |
|||
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
|
|||
const options: DirectoryPickerOptions = { |
|||
startIn: (startIn?.handle ?? undefined) |
|||
}; |
|||
|
|||
const handle = await window.showDirectoryPicker(options); |
|||
return new StorageItem(handle); |
|||
} |
|||
|
|||
public static async openFileDialog( |
|||
startIn: StorageItem | null, multiple: boolean, |
|||
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> { |
|||
const options: OpenFilePickerOptions = { |
|||
startIn: (startIn?.handle ?? undefined), |
|||
multiple, |
|||
excludeAcceptAllOption, |
|||
types: (types ?? undefined) |
|||
}; |
|||
|
|||
const handles = await window.showOpenFilePicker(options); |
|||
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); |
|||
} |
|||
|
|||
public static async saveFileDialog( |
|||
startIn: StorageItem | null, suggestedName: string | null, |
|||
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> { |
|||
const options: SaveFilePickerOptions = { |
|||
startIn: (startIn?.handle ?? undefined), |
|||
suggestedName: (suggestedName ?? undefined), |
|||
excludeAcceptAllOption, |
|||
types: (types ?? undefined) |
|||
}; |
|||
|
|||
const handle = await window.showSaveFilePicker(options); |
|||
return new StorageItem(handle); |
|||
} |
|||
|
|||
public static async openBookmark(key: string): Promise<StorageItem | null> { |
|||
const connection = await avaloniaDb.connect(); |
|||
try { |
|||
const handle = await connection.get(fileBookmarksStore, key); |
|||
return handle && new StorageItem(handle, key); |
|||
} finally { |
|||
connection.close(); |
|||
} |
|||
} |
|||
|
|||
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { |
|||
const accept: Record<string, string[]> = {}; |
|||
mimeTypes.forEach(a => { accept[a] = []; }); |
|||
return { description, accept }; |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -1,13 +1,22 @@ |
|||
{ |
|||
"name": "avalonia.web", |
|||
"scripts": { |
|||
"prebuild": "node ./node_modules/typescript/bin/tsc -noEmit", |
|||
"typecheck": "npx tsc -noEmit", |
|||
"eslint": "npx eslint . --fix", |
|||
"prebuild": "npm-run-all typecheck eslint", |
|||
"build": "node build.js" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/emscripten": "^1.39.6", |
|||
"@types/wicg-file-system-access": "^2020.9.5", |
|||
"typescript": "^4.7.4", |
|||
"esbuild": "^0.15.7" |
|||
"@typescript-eslint/eslint-plugin": "^5.38.1", |
|||
"esbuild": "^0.15.7", |
|||
"eslint": "^8.24.0", |
|||
"eslint-config-standard-with-typescript": "^23.0.0", |
|||
"eslint-plugin-import": "^2.26.0", |
|||
"eslint-plugin-n": "^15.3.0", |
|||
"eslint-plugin-promise": "^6.0.1", |
|||
"npm-run-all": "^4.1.5", |
|||
"typescript": "^4.8.3" |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue