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 System.Collections.Generic; |
||||
using Avalonia.Skia; |
using Avalonia.Skia; |
||||
|
|
||||
namespace Avalonia.Web |
namespace Avalonia.Web.Skia |
||||
{ |
{ |
||||
public class BrowserSkiaGpu : ISkiaGpu |
public class BrowserSkiaGpu : ISkiaGpu |
||||
{ |
{ |
||||
@ -1,7 +1,7 @@ |
|||||
using Avalonia.Skia; |
using Avalonia.Skia; |
||||
using SkiaSharp; |
using SkiaSharp; |
||||
|
|
||||
namespace Avalonia.Web |
namespace Avalonia.Web.Skia |
||||
{ |
{ |
||||
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession |
internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession |
||||
{ |
{ |
||||
@ -1,7 +1,7 @@ |
|||||
using Avalonia.Skia; |
using Avalonia.Skia; |
||||
using SkiaSharp; |
using SkiaSharp; |
||||
|
|
||||
namespace Avalonia.Web |
namespace Avalonia.Web.Skia |
||||
{ |
{ |
||||
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget |
||||
{ |
{ |
||||
@ -1,4 +1,4 @@ |
|||||
namespace Avalonia.Web |
namespace Avalonia.Web.Skia |
||||
{ |
{ |
||||
internal interface IBrowserSkiaSurface |
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 { RuntimeAPI } from "../types/dotnet"; |
||||
import { SizeWatcher } from "./avalonia/canvas"; |
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; |
||||
import { DpiWatcher } from "./avalonia/canvas"; |
|
||||
import { Canvas } from "./avalonia/canvas"; |
|
||||
import { InputHelper } from "./avalonia/input"; |
import { InputHelper } from "./avalonia/input"; |
||||
import { AvaloniaDOM } from "./avalonia/dom"; |
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> { |
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> { |
||||
api.setModuleImports("avalonia.ts", { |
api.setModuleImports("avalonia.ts", { |
||||
|
Caniuse, |
||||
Canvas, |
Canvas, |
||||
InputHelper, |
InputHelper, |
||||
SizeWatcher, |
SizeWatcher, |
||||
DpiWatcher, |
DpiWatcher, |
||||
AvaloniaDOM, |
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 { |
export { StorageItem, StorageItems } from "./storage/storageItem"; |
||||
static isFileApiSupported(): boolean { |
export { StorageProvider } from "./storage/storageProvider"; |
||||
return (globalThis as any).showOpenFilePicker !== undefined; |
|
||||
} |
|
||||
} |
|
||||
|
|||||
@ -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", |
"name": "avalonia.web", |
||||
"scripts": { |
"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" |
"build": "node build.js" |
||||
}, |
}, |
||||
"devDependencies": { |
"devDependencies": { |
||||
"@types/emscripten": "^1.39.6", |
"@types/emscripten": "^1.39.6", |
||||
"@types/wicg-file-system-access": "^2020.9.5", |
"@types/wicg-file-system-access": "^2020.9.5", |
||||
"typescript": "^4.7.4", |
"@typescript-eslint/eslint-plugin": "^5.38.1", |
||||
"esbuild": "^0.15.7" |
"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