diff --git a/.gitignore b/.gitignore
index 44fe5e4ba4..0f0e153e9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -212,3 +212,5 @@ coc-settings.json
*.map
src/Web/Avalonia.Web.Blazor/wwwroot/*.js
src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js
+node_modules
+src/Web/Avalonia.Web.Blazor/webapp/package-lock.json
diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
index 2d598687a9..3fcfaa1c9c 100644
--- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
+++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
@@ -8,31 +8,16 @@
false
true
-
+
-
- wwwroot
- true
- true
-
-
-
- false
- true
-
-
- true
- false
-
-
-
+
true
@@ -47,10 +32,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
deleted file mode 100644
index 72baf14d8f..0000000000
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-
-export class DpiWatcher {
- static lastDpi: number;
- static timerId: number;
- static callback: DotNet.DotNetObjectReference;
-
- public static getDpi() {
- return window.devicePixelRatio;
- }
-
- public static start(callback: DotNet.DotNetObjectReference): number {
- //console.info(`Starting DPI watcher with callback ${callback._id}...`);
-
- DpiWatcher.lastDpi = window.devicePixelRatio;
- DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
- DpiWatcher.callback = callback;
-
- return DpiWatcher.lastDpi;
- }
-
- public static stop() {
- //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
-
- window.clearInterval(DpiWatcher.timerId);
-
- DpiWatcher.callback = undefined;
- }
-
- static update() {
- if (!DpiWatcher.callback)
- return;
-
- const currentDpi = window.devicePixelRatio;
- const lastDpi = DpiWatcher.lastDpi;
- DpiWatcher.lastDpi = currentDpi;
-
- if (Math.abs(lastDpi - currentDpi) > 0.001) {
- DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
- }
- }
-}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
deleted file mode 100644
index a3bf9de31d..0000000000
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-
-export class InputHelper {
- public static clear (inputElement: HTMLInputElement){
- inputElement.value = "";
- }
-
- public static focus (inputElement: HTMLInputElement){
- inputElement.focus();
- inputElement.setSelectionRange(0, 0);
- }
-
- public static setCursor (inputElement: HTMLInputElement, kind: string) {
- inputElement.style.cursor = kind;
- }
-
- public static hide (inputElement: HTMLInputElement){
- inputElement.style.display = 'none';
- }
-
- public static show (inputElement: HTMLInputElement){
- inputElement.style.display = 'block';
- }
-}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
deleted file mode 100644
index 04d57a7756..0000000000
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-// aliases for emscripten
-declare let GL: any;
-declare let GLctx: WebGLRenderingContext;
-declare let Module: EmscriptenModule;
-
-// container for gl info
-type SKGLViewInfo = {
- context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
- fboId: number;
- stencil: number;
- sample: number;
- depth: number;
-}
-
-// alias for a potential skia html canvas
-type SKHtmlCanvasElement = {
- SKHtmlCanvas: SKHtmlCanvas
-} & HTMLCanvasElement
-
-export class SKHtmlCanvas {
- static elements: Map;
-
- htmlCanvas: HTMLCanvasElement;
- glInfo: SKGLViewInfo;
- renderFrameCallback: DotNet.DotNetObjectReference;
- renderLoopEnabled: boolean = false;
- renderLoopRequest: number = 0;
- newWidth: number;
- newHeight: number;
-
- public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
- var view = SKHtmlCanvas.init(true, element, elementId, callback);
- if (!view || !view.glInfo)
- return null;
-
- return view.glInfo;
- }
-
- public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean {
- var view = SKHtmlCanvas.init(false, element, elementId, callback);
- if (!view)
- return false;
-
- return true;
- }
-
- static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas {
- var htmlCanvas = element as SKHtmlCanvasElement;
- if (!htmlCanvas) {
- console.error(`No canvas element was provided.`);
- return null;
- }
-
- if (!SKHtmlCanvas.elements)
- SKHtmlCanvas.elements = new Map();
- SKHtmlCanvas.elements[elementId] = element;
-
- const view = new SKHtmlCanvas(useGL, element, callback);
-
- htmlCanvas.SKHtmlCanvas = view;
-
- return view;
- }
-
- public static deinit(elementId: string) {
- if (!elementId)
- return;
-
- const element = SKHtmlCanvas.elements[elementId];
- SKHtmlCanvas.elements.delete(elementId);
-
- const htmlCanvas = element as SKHtmlCanvasElement;
- if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
- return;
-
- htmlCanvas.SKHtmlCanvas.deinit();
- htmlCanvas.SKHtmlCanvas = undefined;
- }
-
- public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
- const htmlCanvas = element as SKHtmlCanvasElement;
- if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
- return;
-
- htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
- }
-
- public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number)
- {
- const htmlCanvas = element as SKHtmlCanvasElement;
- if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
- return;
-
- htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
- }
-
- public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
- const htmlCanvas = element as SKHtmlCanvasElement;
- if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
- return;
-
- htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
- }
-
- public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
- const htmlCanvas = element as SKHtmlCanvasElement;
- if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
- return;
-
- htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
- }
-
- public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) {
- this.htmlCanvas = element;
- this.renderFrameCallback = callback;
-
- if (useGL) {
- const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
- if (!ctx) {
- console.error(`Failed to create WebGL context: err ${ctx}`);
- return null;
- }
-
- // make current
- GL.makeContextCurrent(ctx);
-
- // read values
- const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
- this.glInfo = {
- context: ctx,
- fboId: fbo ? fbo.id : 0,
- stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
- sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
- depth: GLctx.getParameter(GLctx.DEPTH_BITS),
- };
- }
- }
-
- public deinit() {
- this.setEnableRenderLoop(false);
- }
-
- public setCanvasSize(width: number, height: number)
- {
- this.newWidth = width;
- this.newHeight = height;
-
- if(this.htmlCanvas.width != this.newWidth)
- {
- this.htmlCanvas.width = this.newWidth;
- }
-
- if(this.htmlCanvas.height != this.newHeight)
- {
- this.htmlCanvas.height = this.newHeight;
- }
-
- if (this.glInfo) {
- // make current
- GL.makeContextCurrent(this.glInfo.context);
- }
- }
-
- public requestAnimationFrame(renderLoop?: boolean) {
- // optionally update the render loop
- if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
- this.setEnableRenderLoop(renderLoop);
-
- // skip because we have a render loop
- if (this.renderLoopRequest !== 0)
- return;
-
- // add the draw to the next frame
- this.renderLoopRequest = window.requestAnimationFrame(() => {
- if (this.glInfo) {
- // make current
- GL.makeContextCurrent(this.glInfo.context);
- }
-
- if(this.htmlCanvas.width != this.newWidth)
- {
- this.htmlCanvas.width = this.newWidth;
- }
-
- if(this.htmlCanvas.height != this.newHeight)
- {
- this.htmlCanvas.height = this.newHeight;
- }
-
- this.renderFrameCallback.invokeMethod('Invoke');
- this.renderLoopRequest = 0;
-
- // we may want to draw the next frame
- if (this.renderLoopEnabled)
- this.requestAnimationFrame();
- });
- }
-
- public setEnableRenderLoop(enable: boolean) {
- this.renderLoopEnabled = enable;
-
- // either start the new frame or cancel the existing one
- if (enable) {
- //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
- this.requestAnimationFrame();
- } else if (this.renderLoopRequest !== 0) {
- window.cancelAnimationFrame(this.renderLoopRequest);
- this.renderLoopRequest = 0;
- }
- }
-
- public putImageData(pData: number, width: number, height: number): boolean {
- if (this.glInfo || !pData || width <= 0 || width <= 0)
- return false;
-
- var ctx = this.htmlCanvas.getContext('2d');
- if (!ctx) {
- console.error(`Failed to obtain 2D canvas context.`);
- return false;
- }
-
- // make sure the canvas is scaled correctly for the drawing
- this.htmlCanvas.width = width;
- this.htmlCanvas.height = height;
-
- // set the canvas to be the bytes
- var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
- var imageData = new ImageData(buffer, width, height);
- ctx.putImageData(imageData, 0, 0);
-
- return true;
- }
-
- static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
- const contextAttributes = {
- alpha: 1,
- depth: 1,
- stencil: 8,
- antialias: 0,
- premultipliedAlpha: 1,
- preserveDrawingBuffer: 0,
- preferLowPowerToHighPerformance: 0,
- failIfMajorPerformanceCaveat: 0,
- majorVersion: 2,
- minorVersion: 0,
- enableExtensionsByDefault: 1,
- explicitSwapControl: 0,
- renderViaOffscreenBackBuffer: 1,
- };
-
- let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
- if (!ctx && contextAttributes.majorVersion > 1) {
- console.warn('Falling back to WebGL 1.0');
- contextAttributes.majorVersion = 1;
- contextAttributes.minorVersion = 0;
- ctx = GL.createContext(htmlCanvas, contextAttributes);
- }
-
- return ctx;
- }
-}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
deleted file mode 100644
index 88b94f3a80..0000000000
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-
-type SizeWatcherElement = {
- SizeWatcher: SizeWatcherInstance;
-} & HTMLElement
-
-type SizeWatcherInstance = {
- callback: DotNet.DotNetObjectReference;
-}
-
-export class SizeWatcher {
- static observer: ResizeObserver;
- static elements: Map;
-
- public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) {
- if (!element || !callback)
- return;
-
- //console.info(`Adding size watcher observation with callback ${callback._id}...`);
-
- SizeWatcher.init();
-
- const watcherElement = element as SizeWatcherElement;
- watcherElement.SizeWatcher = {
- callback: callback
- };
-
- SizeWatcher.elements[elementId] = element;
- SizeWatcher.observer.observe(element);
-
- SizeWatcher.invoke(element);
- }
-
- public static unobserve(elementId: string) {
- if (!elementId || !SizeWatcher.observer)
- return;
-
- //console.info('Removing size watcher observation...');
-
- const element = SizeWatcher.elements[elementId];
-
- SizeWatcher.elements.delete(elementId);
- SizeWatcher.observer.unobserve(element);
- }
-
- static init() {
- if (SizeWatcher.observer)
- return;
-
- //console.info('Starting size watcher...');
-
- SizeWatcher.elements = new Map();
- SizeWatcher.observer = new ResizeObserver((entries) => {
- for (let entry of entries) {
- SizeWatcher.invoke(entry.target);
- }
- });
- }
-
- static invoke(element: Element) {
- const watcherElement = element as SizeWatcherElement;
- const instance = watcherElement.SizeWatcher;
-
- if (!instance || !instance.callback)
- return;
-
- return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
- }
-}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
deleted file mode 100644
index 4a3d71e034..0000000000
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-
-declare namespace DotNet {
- interface DotNetObjectReference extends DotNet.DotNetObject {
- _id: number;
- dispose();
- }
-}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
deleted file mode 100644
index e3829d4db1..0000000000
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-// Type definitions for Emscripten 1.39.16
-// Project: https://emscripten.org
-// Definitions by: Kensuke Matsuzaki
-// Periklis Tsirakidis
-// Bumsik Kim
-// Louis DeScioli
-// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
-// TypeScript Version: 2.2
-
-/** Other WebAssembly declarations, for compatibility with older versions of Typescript */
-declare namespace WebAssembly {
- interface Module {}
-}
-
-declare namespace Emscripten {
- interface FileSystemType {}
- type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
-
- type JSType = 'number' | 'string' | 'array' | 'boolean';
- type TypeCompatibleWithC = number | string | any[] | boolean;
-
- type CIntType = 'i8' | 'i16' | 'i32' | 'i64';
- type CFloatType = 'float' | 'double';
- type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*';
- type CType = CIntType | CFloatType | CPointerType;
-
- type WebAssemblyImports = Array<{
- name: string;
- kind: string;
- }>;
-
- type WebAssemblyExports = Array<{
- module: string;
- name: string;
- kind: string;
- }>;
-
- interface CCallOpts {
- async?: boolean | undefined;
- }
-}
-
-interface EmscriptenModule {
- print(str: string): void;
- printErr(str: string): void;
- arguments: string[];
- environment: Emscripten.EnvironmentType;
- preInit: Array<{ (): void }>;
- preRun: Array<{ (): void }>;
- postRun: Array<{ (): void }>;
- onAbort: { (what: any): void };
- onRuntimeInitialized: { (): void };
- preinitializedWebGLContext: WebGLRenderingContext;
- noInitialRun: boolean;
- noExitRuntime: boolean;
- logReadFiles: boolean;
- filePackagePrefixURL: string;
- wasmBinary: ArrayBuffer;
-
- destroy(object: object): void;
- getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
- instantiateWasm(
- imports: Emscripten.WebAssemblyImports,
- successCallback: (module: WebAssembly.Module) => void,
- ): Emscripten.WebAssemblyExports;
- locateFile(url: string, scriptDirectory: string): string;
- onCustomMessage(event: MessageEvent): void;
-
- // USE_TYPED_ARRAYS == 1
- HEAP: Int32Array;
- IHEAP: Int32Array;
- FHEAP: Float64Array;
-
- // USE_TYPED_ARRAYS == 2
- HEAP8: Int8Array;
- HEAP16: Int16Array;
- HEAP32: Int32Array;
- HEAPU8: Uint8Array;
- HEAPU16: Uint16Array;
- HEAPU32: Uint32Array;
- HEAPF32: Float32Array;
- HEAPF64: Float64Array;
-
- TOTAL_STACK: number;
- TOTAL_MEMORY: number;
- FAST_MEMORY: number;
-
- addOnPreRun(cb: () => any): void;
- addOnInit(cb: () => any): void;
- addOnPreMain(cb: () => any): void;
- addOnExit(cb: () => any): void;
- addOnPostRun(cb: () => any): void;
-
- preloadedImages: any;
- preloadedAudios: any;
-
- _malloc(size: number): number;
- _free(ptr: number): void;
-}
-
-/**
- * A factory function is generated when setting the `MODULARIZE` build option
- * to `1` in your Emscripten build. It return a Promise that resolves to an
- * initialized, ready-to-call `EmscriptenModule` instance.
- *
- * By default, the factory function will be named `Module`. It's recommended to
- * use the `EXPORT_ES6` option, in which the factory function will be the
- * default export. If used without `EXPORT_ES6`, the factory function will be a
- * global variable. You can rename the variable using the `EXPORT_NAME` build
- * option. It's left to you to declare any global variables as needed in your
- * application's types.
- * @param moduleOverrides Default properties for the initialized module.
- */
-type EmscriptenModuleFactory = (
- moduleOverrides?: Partial,
-) => Promise;
-
-declare namespace FS {
- interface Lookup {
- path: string;
- node: FSNode;
- }
-
- interface FSStream {}
- interface FSNode {}
- interface ErrnoError {}
-
- let ignorePermissions: boolean;
- let trackingDelegate: any;
- let tracking: any;
- let genericErrors: any;
-
- //
- // paths
- //
- function lookupPath(path: string, opts: any): Lookup;
- function getPath(node: FSNode): string;
-
- //
- // nodes
- //
- function isFile(mode: number): boolean;
- function isDir(mode: number): boolean;
- function isLink(mode: number): boolean;
- function isChrdev(mode: number): boolean;
- function isBlkdev(mode: number): boolean;
- function isFIFO(mode: number): boolean;
- function isSocket(mode: number): boolean;
-
- //
- // devices
- //
- function major(dev: number): number;
- function minor(dev: number): number;
- function makedev(ma: number, mi: number): number;
- function registerDevice(dev: number, ops: any): void;
-
- //
- // core
- //
- function syncfs(populate: boolean, callback: (e: any) => any): void;
- function syncfs(callback: (e: any) => any, populate?: boolean): void;
- function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
- function unmount(mountpoint: string): void;
-
- function mkdir(path: string, mode?: number): any;
- function mkdev(path: string, mode?: number, dev?: number): any;
- function symlink(oldpath: string, newpath: string): any;
- function rename(old_path: string, new_path: string): void;
- function rmdir(path: string): void;
- function readdir(path: string): any;
- function unlink(path: string): void;
- function readlink(path: string): string;
- function stat(path: string, dontFollow?: boolean): any;
- function lstat(path: string): any;
- function chmod(path: string, mode: number, dontFollow?: boolean): void;
- function lchmod(path: string, mode: number): void;
- function fchmod(fd: number, mode: number): void;
- function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
- function lchown(path: string, uid: number, gid: number): void;
- function fchown(fd: number, uid: number, gid: number): void;
- function truncate(path: string, len: number): void;
- function ftruncate(fd: number, len: number): void;
- function utime(path: string, atime: number, mtime: number): void;
- function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream;
- function close(stream: FSStream): void;
- function llseek(stream: FSStream, offset: number, whence: number): any;
- function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
- function write(
- stream: FSStream,
- buffer: ArrayBufferView,
- offset: number,
- length: number,
- position?: number,
- canOwn?: boolean,
- ): number;
- function allocate(stream: FSStream, offset: number, length: number): void;
- function mmap(
- stream: FSStream,
- buffer: ArrayBufferView,
- offset: number,
- length: number,
- position: number,
- prot: number,
- flags: number,
- ): any;
- function ioctl(stream: FSStream, cmd: any, arg: any): any;
- function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array;
- function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string;
- function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array;
- function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void;
-
- //
- // module-level FS code
- //
- function cwd(): string;
- function chdir(path: string): void;
- function init(
- input: null | (() => number | null),
- output: null | ((c: number) => any),
- error: null | ((c: number) => any),
- ): void;
-
- function createLazyFile(
- parent: string | FSNode,
- name: string,
- url: string,
- canRead: boolean,
- canWrite: boolean,
- ): FSNode;
- function createPreloadedFile(
- parent: string | FSNode,
- name: string,
- url: string,
- canRead: boolean,
- canWrite: boolean,
- onload?: () => void,
- onerror?: () => void,
- dontCreateFile?: boolean,
- canOwn?: boolean,
- ): void;
- function createDataFile(
- parent: string | FSNode,
- name: string,
- data: ArrayBufferView,
- canRead: boolean,
- canWrite: boolean,
- canOwn: boolean,
- ): FSNode;
-}
-
-declare var MEMFS: Emscripten.FileSystemType;
-declare var NODEFS: Emscripten.FileSystemType;
-declare var IDBFS: Emscripten.FileSystemType;
-
-// Below runtime function/variable declarations are exportable by
-// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
-// EmscriptenModule interface to add runtime functions.
-//
-// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
-// You can access ccall() via Module["ccall"]. In this case, you should
-// extend EmscriptenModule to pass the compiler check like the following:
-//
-// interface YourOwnEmscriptenModule extends EmscriptenModule {
-// ccall: typeof ccall;
-// }
-//
-// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
-
-declare function ccall(
- ident: string,
- returnType: Emscripten.JSType | null,
- argTypes: Emscripten.JSType[],
- args: Emscripten.TypeCompatibleWithC[],
- opts?: Emscripten.CCallOpts,
-): any;
-declare function cwrap(
- ident: string,
- returnType: Emscripten.JSType | null,
- argTypes: Emscripten.JSType[],
- opts?: Emscripten.CCallOpts,
-): (...args: any[]) => any;
-
-declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void;
-declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number;
-
-declare function allocate(
- slab: number[] | ArrayBufferView | number,
- types: Emscripten.CType | Emscripten.CType[],
- allocator: number,
- ptr?: number,
-): number;
-
-declare function stackAlloc(size: number): number;
-declare function stackSave(): number;
-declare function stackRestore(ptr: number): void;
-
-declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string;
-declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
-declare function lengthBytesUTF8(str: string): number;
-declare function allocateUTF8(str: string): number;
-declare function allocateUTF8OnStack(str: string): number;
-declare function UTF16ToString(ptr: number): string;
-declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
-declare function lengthBytesUTF16(str: string): number;
-declare function UTF32ToString(ptr: number): string;
-declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
-declare function lengthBytesUTF32(str: string): number;
-
-declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
-declare function intArrayToString(array: number[]): string;
-declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
-declare function writeArrayToMemory(array: number[], buffer: number): void;
-declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
-
-declare function addRunDependency(id: any): void;
-declare function removeRunDependency(id: any): void;
-
-declare function addFunction(func: (...args: any[]) => any, signature?: string): number;
-declare function removeFunction(funcPtr: number): void;
-
-declare var ALLOC_NORMAL: number;
-declare var ALLOC_STACK: number;
-declare var ALLOC_STATIC: number;
-declare var ALLOC_DYNAMIC: number;
-declare var ALLOC_NONE: number;
diff --git a/src/Web/Avalonia.Web.Blazor/tsconfig.json b/src/Web/Avalonia.Web.Blazor/tsconfig.json
deleted file mode 100644
index 71c462cd60..0000000000
--- a/src/Web/Avalonia.Web.Blazor/tsconfig.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "compilerOptions": {
- "noImplicitAny": false,
- "noEmitOnError": true,
- "removeComments": false,
- "sourceMap": true,
- "target": "ES2020",
- "module": "ES2020",
- "outDir": "wwwroot"
- },
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts
new file mode 100644
index 0000000000..5f282702bb
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts
new file mode 100644
index 0000000000..06235782f8
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts
@@ -0,0 +1,40 @@
+export class DpiWatcher {
+ static lastDpi: number;
+ static timerId: number;
+ static callback?: DotNet.DotNetObject;
+
+ public static getDpi() {
+ return window.devicePixelRatio;
+ }
+
+ public static start(callback: DotNet.DotNetObject): number {
+ //console.info(`Starting DPI watcher with callback ${callback._id}...`);
+
+ DpiWatcher.lastDpi = window.devicePixelRatio;
+ DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
+ DpiWatcher.callback = callback;
+
+ return DpiWatcher.lastDpi;
+ }
+
+ public static stop() {
+ //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
+
+ window.clearInterval(DpiWatcher.timerId);
+
+ DpiWatcher.callback = undefined;
+ }
+
+ static update() {
+ if (!DpiWatcher.callback)
+ return;
+
+ const currentDpi = window.devicePixelRatio;
+ const lastDpi = DpiWatcher.lastDpi;
+ DpiWatcher.lastDpi = currentDpi;
+
+ if (Math.abs(lastDpi - currentDpi) > 0.001) {
+ DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
+ }
+ }
+}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts
new file mode 100644
index 0000000000..2eaa8de2fe
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts
@@ -0,0 +1,79 @@
+class InnerDbConnection {
+ constructor(private database: IDBDatabase) { }
+
+ private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
+ const tx = this.database.transaction(store, mode);
+ return tx.objectStore(store);
+ }
+
+ public put(store: string, obj: any, key?: IDBValidKey): Promise {
+ const os = this.openStore(store, "readwrite");
+
+ return 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 delete(store: string, key: IDBValidKey): Promise {
+ const os = this.openStore(store, "readwrite");
+
+ return new Promise((resolve, reject) => {
+ const response = os.delete(key);
+ response.onsuccess = () => {
+ resolve();
+ };
+ response.onerror = () => {
+ reject(response.error);
+ };
+ });
+ }
+
+ public close() {
+ this.database.close();
+ }
+}
+
+export class IndexedDbWrapper {
+ constructor(private databaseName: string, private objectStores: [string]) {
+ }
+
+ public connect(): Promise {
+ const conn = window.indexedDB.open(this.databaseName, 1);
+
+ conn.onupgradeneeded = event => {
+ const db = (>event.target).result;
+ this.objectStores.forEach(store => {
+ db.createObjectStore(store);
+ });
+ };
+
+ return new Promise((resolve, reject) => {
+ conn.onsuccess = event => {
+ resolve(new InnerDbConnection((>event.target).result));
+ };
+ conn.onerror = event => {
+ reject((>event.target).error);
+ };
+ });
+ }
+}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts
new file mode 100644
index 0000000000..2cce411376
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts
@@ -0,0 +1,22 @@
+export class InputHelper {
+ public static clear(inputElement: HTMLInputElement) {
+ inputElement.value = "";
+ }
+
+ public static focus(inputElement: HTMLInputElement) {
+ inputElement.focus();
+ inputElement.setSelectionRange(0, 0);
+ }
+
+ public static setCursor(inputElement: HTMLInputElement, kind: string) {
+ inputElement.style.cursor = kind;
+ }
+
+ public static hide(inputElement: HTMLInputElement) {
+ inputElement.style.display = 'none';
+ }
+
+ public static show(inputElement: HTMLInputElement) {
+ inputElement.style.display = 'block';
+ }
+}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts
similarity index 57%
rename from src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts
rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts
index baa9191845..9e5c3843c8 100644
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts
@@ -14,10 +14,9 @@
}
}
-class NativeControlHostTopLevelAttachment
-{
- _child: HTMLElement;
- _host: HTMLElement;
+class NativeControlHostTopLevelAttachment {
+ _child?: HTMLElement;
+ _host?: HTMLElement;
InitializeWithChildHandle(child: HTMLElement) {
this._child = child;
@@ -25,32 +24,38 @@ class NativeControlHostTopLevelAttachment
}
AttachTo(host: HTMLElement): void {
- if (this._host) {
+ if (this._host && this._child) {
this._host.removeChild(this._child);
}
this._host = host;
- if (this._host) {
+ if (this._host && this._child) {
this._host.appendChild(this._child);
}
}
ShowInBounds(x: number, y: number, width: number, height: number): void {
- this._child.style.top = y + "px";
- this._child.style.left = x + "px";
- this._child.style.width = width + "px";
- this._child.style.height = height + "px";
- this._child.style.display = "block";
+ if (this._child) {
+ this._child.style.top = y + "px";
+ this._child.style.left = x + "px";
+ this._child.style.width = width + "px";
+ this._child.style.height = height + "px";
+ this._child.style.display = "block";
+ }
}
HideWithSize(width: number, height: number): void {
- this._child.style.width = width + "px";
- this._child.style.height = height + "px";
- this._child.style.display = "none";
+ if (this._child) {
+ this._child.style.width = width + "px";
+ this._child.style.height = height + "px";
+ this._child.style.display = "none";
+ }
}
ReleaseChild(): void {
- this._child = null;
+ if (this._child) {
+ this._child = undefined;
+ }
}
}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts
new file mode 100644
index 0000000000..e934f74807
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts
@@ -0,0 +1,255 @@
+// aliases for emscripten
+declare let GL: any;
+declare let GLctx: WebGLRenderingContext;
+declare let Module: EmscriptenModule;
+
+// container for gl info
+type SKGLViewInfo = {
+ context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
+ fboId: number;
+ stencil: number;
+ sample: number;
+ depth: number;
+}
+
+// alias for a potential skia html canvas
+type SKHtmlCanvasElement = {
+ SKHtmlCanvas: SKHtmlCanvas | undefined
+} & HTMLCanvasElement
+
+export class SKHtmlCanvas {
+ static elements: Map;
+
+ htmlCanvas: HTMLCanvasElement;
+ glInfo?: SKGLViewInfo;
+ renderFrameCallback: DotNet.DotNetObject;
+ renderLoopEnabled: boolean = false;
+ renderLoopRequest: number = 0;
+ newWidth?: number;
+ newHeight?: number;
+
+ public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null {
+ var view = SKHtmlCanvas.init(true, element, elementId, callback);
+ if (!view || !view.glInfo)
+ return null;
+
+ return view.glInfo;
+ }
+
+ public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean {
+ var view = SKHtmlCanvas.init(false, element, elementId, callback);
+ if (!view)
+ return false;
+
+ return true;
+ }
+
+ static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null {
+ var htmlCanvas = element as SKHtmlCanvasElement;
+ if (!htmlCanvas) {
+ console.error(`No canvas element was provided.`);
+ return null;
+ }
+
+ if (!SKHtmlCanvas.elements)
+ SKHtmlCanvas.elements = new Map();
+ SKHtmlCanvas.elements.set(elementId, element);
+
+ const view = new SKHtmlCanvas(useGL, element, callback);
+
+ htmlCanvas.SKHtmlCanvas = view;
+
+ return view;
+ }
+
+ public static deinit(elementId: string) {
+ if (!elementId)
+ return;
+
+ const element = SKHtmlCanvas.elements.get(elementId);
+ SKHtmlCanvas.elements.delete(elementId);
+
+ const htmlCanvas = element as SKHtmlCanvasElement;
+ if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+ return;
+
+ htmlCanvas.SKHtmlCanvas.deinit();
+ htmlCanvas.SKHtmlCanvas = undefined;
+ }
+
+ public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
+ const htmlCanvas = element as SKHtmlCanvasElement;
+ if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+ return;
+
+ htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
+ }
+
+ public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) {
+ const htmlCanvas = element as SKHtmlCanvasElement;
+ if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+ return;
+
+ htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
+ }
+
+ public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
+ const htmlCanvas = element as SKHtmlCanvasElement;
+ if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+ return;
+
+ htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
+ }
+
+ public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
+ const htmlCanvas = element as SKHtmlCanvasElement;
+ if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
+ return;
+
+ htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
+ }
+
+ public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) {
+ this.htmlCanvas = element;
+ this.renderFrameCallback = callback;
+
+ if (useGL) {
+ const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
+ if (!ctx) {
+ console.error(`Failed to create WebGL context: err ${ctx}`);
+ return;
+ }
+
+ // make current
+ GL.makeContextCurrent(ctx);
+
+ // read values
+ const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
+ this.glInfo = {
+ context: ctx,
+ fboId: fbo ? fbo.id : 0,
+ stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
+ sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
+ depth: GLctx.getParameter(GLctx.DEPTH_BITS),
+ };
+ }
+ }
+
+ public deinit() {
+ this.setEnableRenderLoop(false);
+ }
+
+ public setCanvasSize(width: number, height: number) {
+ this.newWidth = width;
+ this.newHeight = height;
+
+ if (this.htmlCanvas.width != this.newWidth) {
+ this.htmlCanvas.width = this.newWidth;
+ }
+
+ if (this.htmlCanvas.height != this.newHeight) {
+ this.htmlCanvas.height = this.newHeight;
+ }
+
+ if (this.glInfo) {
+ // make current
+ GL.makeContextCurrent(this.glInfo.context);
+ }
+ }
+
+ public requestAnimationFrame(renderLoop?: boolean) {
+ // optionally update the render loop
+ if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
+ this.setEnableRenderLoop(renderLoop);
+
+ // skip because we have a render loop
+ if (this.renderLoopRequest !== 0)
+ return;
+
+ // add the draw to the next frame
+ this.renderLoopRequest = window.requestAnimationFrame(() => {
+ if (this.glInfo) {
+ // make current
+ GL.makeContextCurrent(this.glInfo.context);
+ }
+
+ if (this.htmlCanvas.width != this.newWidth) {
+ this.htmlCanvas.width = this.newWidth || 0;
+ }
+
+ if (this.htmlCanvas.height != this.newHeight) {
+ this.htmlCanvas.height = this.newHeight || 0;
+ }
+
+ this.renderFrameCallback.invokeMethod('Invoke');
+ this.renderLoopRequest = 0;
+
+ // we may want to draw the next frame
+ if (this.renderLoopEnabled)
+ this.requestAnimationFrame();
+ });
+ }
+
+ public setEnableRenderLoop(enable: boolean) {
+ this.renderLoopEnabled = enable;
+
+ // either start the new frame or cancel the existing one
+ if (enable) {
+ //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
+ this.requestAnimationFrame();
+ } else if (this.renderLoopRequest !== 0) {
+ window.cancelAnimationFrame(this.renderLoopRequest);
+ this.renderLoopRequest = 0;
+ }
+ }
+
+ public putImageData(pData: number, width: number, height: number): boolean {
+ if (this.glInfo || !pData || width <= 0 || width <= 0)
+ return false;
+
+ var ctx = this.htmlCanvas.getContext('2d');
+ if (!ctx) {
+ console.error(`Failed to obtain 2D canvas context.`);
+ return false;
+ }
+
+ // make sure the canvas is scaled correctly for the drawing
+ this.htmlCanvas.width = width;
+ this.htmlCanvas.height = height;
+
+ // set the canvas to be the bytes
+ var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
+ var imageData = new ImageData(buffer, width, height);
+ ctx.putImageData(imageData, 0, 0);
+
+ return true;
+ }
+
+ static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
+ const contextAttributes = {
+ alpha: 1,
+ depth: 1,
+ stencil: 8,
+ antialias: 0,
+ premultipliedAlpha: 1,
+ preserveDrawingBuffer: 0,
+ preferLowPowerToHighPerformance: 0,
+ failIfMajorPerformanceCaveat: 0,
+ majorVersion: 2,
+ minorVersion: 0,
+ enableExtensionsByDefault: 1,
+ explicitSwapControl: 0,
+ renderViaOffscreenBackBuffer: 1,
+ };
+
+ let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
+ if (!ctx && contextAttributes.majorVersion > 1) {
+ console.warn('Falling back to WebGL 1.0');
+ contextAttributes.majorVersion = 1;
+ contextAttributes.minorVersion = 0;
+ ctx = GL.createContext(htmlCanvas, contextAttributes);
+ }
+
+ return ctx;
+ }
+}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts
new file mode 100644
index 0000000000..715b252988
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts
@@ -0,0 +1,67 @@
+type SizeWatcherElement = {
+ SizeWatcher: SizeWatcherInstance;
+} & HTMLElement
+
+type SizeWatcherInstance = {
+ callback: DotNet.DotNetObject;
+}
+
+export class SizeWatcher {
+ static observer: ResizeObserver;
+ static elements: Map;
+
+ public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) {
+ if (!element || !callback)
+ return;
+
+ //console.info(`Adding size watcher observation with callback ${callback._id}...`);
+
+ SizeWatcher.init();
+
+ const watcherElement = element as SizeWatcherElement;
+ watcherElement.SizeWatcher = {
+ callback: callback
+ };
+
+ SizeWatcher.elements.set(elementId, element);
+ SizeWatcher.observer.observe(element);
+
+ SizeWatcher.invoke(element);
+ }
+
+ public static unobserve(elementId: string) {
+ if (!elementId || !SizeWatcher.observer)
+ return;
+
+ //console.info('Removing size watcher observation...');
+
+ const element = SizeWatcher.elements.get(elementId)!;
+
+ SizeWatcher.elements.delete(elementId);
+ SizeWatcher.observer.unobserve(element);
+ }
+
+ static init() {
+ if (SizeWatcher.observer)
+ return;
+
+ //console.info('Starting size watcher...');
+
+ SizeWatcher.elements = new Map();
+ SizeWatcher.observer = new ResizeObserver((entries) => {
+ for (let entry of entries) {
+ SizeWatcher.invoke(entry.target);
+ }
+ });
+ }
+
+ static invoke(element: Element) {
+ const watcherElement = element as SizeWatcherElement;
+ const instance = watcherElement.SizeWatcher;
+
+ if (!instance || !instance.callback)
+ return;
+
+ return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
+ }
+}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
similarity index 52%
rename from src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts
rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
index 1ee055eff9..65b8fa908e 100644
--- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
@@ -1,132 +1,14 @@
-// As we don't have proper package managing for Avalonia.Web project, declare types manually
-declare global {
- interface FileSystemWritableFileStream {
- write(position: number, data: BufferSource | Blob | string): Promise;
- truncate(size: number): Promise;
- close(): Promise;
- }
- type PermissionsMode = "read" | "readwrite";
- interface FileSystemFileHandle {
- name: string,
- getFile(): Promise;
- createWritable(options?: { keepExistingData?: boolean }): Promise;
-
- queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
- requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
+import { IndexedDbWrapper } from "./IndexedDbWrapper";
- entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>;
- }
- type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
- type StartInDirectory = WellKnownDirectory | FileSystemFileHandle;
- interface FilePickerAcceptType {
- description: string,
- // mime -> ext[] array
- accept: { [mime: string]: string | string[] }
- }
- interface FilePickerOptions {
- types?: FilePickerAcceptType[],
- excludeAcceptAllOption: boolean,
- id?: string,
+declare global {
+ type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
+ type StartInDirectory = WellKnownDirectory | FileSystemFileHandle;
+ interface OpenFilePickerOptions {
startIn?: StartInDirectory
}
- interface OpenFilePickerOptions extends FilePickerOptions {
- multiple: boolean
- }
- interface SaveFilePickerOptions extends FilePickerOptions {
- suggestedName?: string
- }
- interface DirectoryPickerOptions {
- id?: string,
+ interface SaveFilePickerOptions {
startIn?: StartInDirectory
}
-
- interface Window {
- showOpenFilePicker: (options: OpenFilePickerOptions) => Promise;
- showSaveFilePicker: (options: SaveFilePickerOptions) => Promise;
- showDirectoryPicker: (options: DirectoryPickerOptions) => Promise;
- }
-}
-
-// TODO move to another file and use import
-class IndexedDbWrapper {
- constructor(private databaseName: string, private objectStores: [ string ]) {
-
- }
-
- public connect(): Promise {
- const conn = window.indexedDB.open(this.databaseName, 1);
-
- conn.onupgradeneeded = event => {
- const db = (>event.target).result;
- this.objectStores.forEach(store => {
- db.createObjectStore(store);
- });
- }
-
- return new Promise((resolve, reject) => {
- conn.onsuccess = event => {
- resolve(new InnerDbConnection((>event.target).result));
- }
- conn.onerror = event => {
- reject((>event.target).error);
- }
- });
- }
-}
-
-class InnerDbConnection {
- constructor(private database: IDBDatabase) { }
-
- private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
- const tx = this.database.transaction(store, mode);
- return tx.objectStore(store);
- }
-
- public put(store: string, obj: any, key?: IDBValidKey): Promise {
- const os = this.openStore(store, "readwrite");
-
- return 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 delete(store: string, key: IDBValidKey): Promise {
- const os = this.openStore(store, "readwrite");
-
- return new Promise((resolve, reject) => {
- const response = os.delete(key);
- response.onsuccess = () => {
- resolve();
- };
- response.onerror = () => {
- reject(response.error);
- };
- });
- }
-
- public close() {
- this.database.close();
- }
}
const fileBookmarksStore: string = "fileBookmarks";
@@ -135,7 +17,7 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
])
class StorageItem {
- constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { }
+ constructor(private handle: FileSystemHandle, private bookmarkId?: string) { }
public getName(): string {
return this.handle.name
@@ -146,21 +28,35 @@ class StorageItem {
}
public async openRead(): Promise {
+ if (!(this.handle instanceof FileSystemFileHandle)) {
+ throw new Error("StorageItem is not a file");
+ }
+
await this.verityPermissions('read');
- return await this.handle.getFile();
+ const file = await this.handle.getFile();
+ return file;
}
public async openWrite(): Promise {
+ if (!(this.handle instanceof FileSystemFileHandle)) {
+ throw new Error("StorageItem is not a file");
+ }
+
await this.verityPermissions('readwrite');
return await this.handle.createWritable({ keepExistingData: true });
}
- public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> {
- const file = this.handle.getFile && await this.handle.getFile();
-
- return file && {
+ public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> {
+ const file = this.handle instanceof FileSystemFileHandle
+ && await this.handle.getFile();
+
+ if (!file) {
+ return null;
+ }
+
+ return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
@@ -168,6 +64,10 @@ class StorageItem {
}
public async getItems(): Promise {
+ if (this.handle.kind !== "directory"){
+ return new StorageItems([]);
+ }
+
const items: StorageItem[] = [];
for await (const [key, value] of this.handle.entries()) {
items.push(new StorageItem(value));
@@ -175,7 +75,7 @@ class StorageItem {
return new StorageItems(items);
}
- private async verityPermissions(mode: PermissionsMode): Promise {
+ private async verityPermissions(mode: FileSystemPermissionMode): Promise {
if (await this.handle.queryPermission({ mode }) === 'granted') {
return;
}
@@ -200,7 +100,7 @@ class StorageItem {
connection.close();
}
}
-
+
public async deleteBookmark(): Promise {
if (!this.bookmarkId) {
return;
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/package.json b/src/Web/Avalonia.Web.Blazor/webapp/package.json
new file mode 100644
index 0000000000..130e3032b4
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "avalonia.web",
+ "scripts": {
+ "build": "tsc --build",
+ "clean": "tsc --build --clean"
+ },
+ "devDependencies": {
+ "@types/emscripten": "^1.39.6",
+ "@types/wicg-file-system-access": "^2020.9.5",
+ "typescript": "^4.7.4",
+ "webpack": "^5.73.0",
+ "webpack-cli": "^4.10.0"
+ }
+}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json b/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
new file mode 100644
index 0000000000..9a18b259c0
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES6",
+ "strict": true,
+ "sourceMap": true,
+ "outDir": "../wwwroot",
+ "removeComments": true,
+ "noEmitOnError": true,
+ "lib": [
+ "dom",
+ "ES6",
+ "esnext.asynciterable"
+ ]
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
similarity index 100%
rename from src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts
rename to src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js b/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js
new file mode 100644
index 0000000000..e69de29bb2