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