From 942eb09b1ae3694633e81384005cdb636399a1f3 Mon Sep 17 00:00:00 2001
From: Oleksandr Karpovich <a.n.karpovich@gmail.com>
Date: Fri, 14 Apr 2023 18:09:44 +0200
Subject: [PATCH 1/2] Add webApp module to follow the project convention

---
 compose-imageviewer/settings.gradle.kts       |   1 +
 compose-imageviewer/shared/build.gradle.kts   |  24 +--
 .../example/imageviewer/ImageViewer.common.kt |   2 +-
 .../example/imageviewer/style/Palette.kt      |   2 +-
 .../kotlin/example/imageviewer/view/Toast.kt  |   2 +-
 .../shared/src/wasmMain/kotlin/Main.wasm.kt   | 188 ------------------
 compose-imageviewer/webApp/build.gradle.kts   |  69 +++++++
 .../webApp/src/jsMain/kotlin/Actuals.kt       |  14 ++
 .../webApp/src/jsMain/kotlin/Main.kt          |  19 ++
 .../src/jsMain/resources/index.html           |   8 +-
 .../src/jsWasmMain/kotlin/Common.kt}          |  37 ++--
 .../webApp/src/wasmMain/kotlin/Actuals.kt     |  88 ++++++++
 .../webApp/src/wasmMain/kotlin/Main.wasm.kt   |  13 ++
 .../src/wasmMain/resources/index.html         |   0
 .../src/wasmMain/resources/load.mjs           |   0
 15 files changed, 227 insertions(+), 240 deletions(-)
 delete mode 100644 compose-imageviewer/shared/src/wasmMain/kotlin/Main.wasm.kt
 create mode 100644 compose-imageviewer/webApp/build.gradle.kts
 create mode 100644 compose-imageviewer/webApp/src/jsMain/kotlin/Actuals.kt
 create mode 100644 compose-imageviewer/webApp/src/jsMain/kotlin/Main.kt
 rename compose-imageviewer/{shared => webApp}/src/jsMain/resources/index.html (55%)
 rename compose-imageviewer/{shared/src/jsMain/kotlin/Main.js.kt => webApp/src/jsWasmMain/kotlin/Common.kt} (79%)
 create mode 100644 compose-imageviewer/webApp/src/wasmMain/kotlin/Actuals.kt
 create mode 100644 compose-imageviewer/webApp/src/wasmMain/kotlin/Main.wasm.kt
 rename compose-imageviewer/{shared => webApp}/src/wasmMain/resources/index.html (100%)
 rename compose-imageviewer/{shared => webApp}/src/wasmMain/resources/load.mjs (100%)

diff --git a/compose-imageviewer/settings.gradle.kts b/compose-imageviewer/settings.gradle.kts
index 06f5ed5..4190cb0 100644
--- a/compose-imageviewer/settings.gradle.kts
+++ b/compose-imageviewer/settings.gradle.kts
@@ -27,3 +27,4 @@ rootProject.name = "imageviewer"
 include(":androidApp")
 include(":shared")
 include(":desktopApp")
+include(":webApp")
diff --git a/compose-imageviewer/shared/build.gradle.kts b/compose-imageviewer/shared/build.gradle.kts
index d82d6cd..eab231b 100755
--- a/compose-imageviewer/shared/build.gradle.kts
+++ b/compose-imageviewer/shared/build.gradle.kts
@@ -21,25 +21,11 @@ kotlin {
 //    iosSimulatorArm64()
 
     js(IR) {
-        moduleName = "imageviewer"
         browser()
-        binaries.executable()
     }
 
     wasm {
-        moduleName = "imageviewer"
-        browser {
-            commonWebpackConfig {
-                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).copy(
-                    open = mapOf(
-                        "app" to mapOf(
-                            "name" to "google chrome",
-                        )
-                    ),
-                )
-            }
-        }
-        binaries.executable()
+        browser()
     }
 
 //    cocoapods {
@@ -111,10 +97,6 @@ kotlin {
     }
 }
 
-compose.experimental {
-    web.application {}
-}
-
 android {
     compileSdk = 33
     sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
@@ -131,5 +113,5 @@ android {
     }
 }
 
-// Use a proper version of webpack, TODO remove after updating to Kotlin 1.9. 
-rootProject.the<NodeJsRootExtension>().versions.webpack.version = "5.76.2"
\ No newline at end of file
+// Use a proper version of webpack, TODO remove after updating to Kotlin 1.9.
+rootProject.the<NodeJsRootExtension>().versions.webpack.version = "5.76.2"
diff --git a/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt b/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
index aca0c3e..5345f43 100644
--- a/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
+++ b/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
@@ -34,7 +34,7 @@ enum class ExternalImageViewerEvent {
 
 @OptIn(ExperimentalAnimationApi::class)
 @Composable
-internal fun ImageViewerCommon(
+fun ImageViewerCommon(
     dependencies: Dependencies,
     externalEvents: Flow<ExternalImageViewerEvent> = emptyFlow()
 ) {
diff --git a/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt b/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
index b0056ac..ac70937 100755
--- a/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
+++ b/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
@@ -48,7 +48,7 @@ object ImageviewerColors {
 }
 
 @Composable
-internal fun ImageViewerTheme(content: @Composable () -> Unit) {
+fun ImageViewerTheme(content: @Composable () -> Unit) {
     isSystemInDarkTheme() // todo check and change colors
     MaterialTheme(
         colorScheme = MaterialTheme.colorScheme.copy(
diff --git a/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt b/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
index 6a0ca3e..0b7ab4c 100755
--- a/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
+++ b/compose-imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
@@ -22,7 +22,7 @@ sealed interface ToastState {
 }
 
 @Composable
-internal fun Toast(
+fun Toast(
     state: MutableState<ToastState>
 ) {
     val value = state.value
diff --git a/compose-imageviewer/shared/src/wasmMain/kotlin/Main.wasm.kt b/compose-imageviewer/shared/src/wasmMain/kotlin/Main.wasm.kt
deleted file mode 100644
index 123708c..0000000
--- a/compose-imageviewer/shared/src/wasmMain/kotlin/Main.wasm.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Surface
-import androidx.compose.runtime.*
-import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.window.Window
-import example.imageviewer.*
-import example.imageviewer.ImageViewerCommon
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.core.FilterType
-import example.imageviewer.model.*
-import example.imageviewer.model.filtration.BlurFilter
-import example.imageviewer.model.filtration.GrayScaleFilter
-import example.imageviewer.model.filtration.PixelFilter
-import example.imageviewer.style.ImageViewerTheme
-import example.imageviewer.utils.ioDispatcher
-import example.imageviewer.view.Toast
-import example.imageviewer.view.ToastState
-import kotlinx.coroutines.CoroutineScope
-import org.jetbrains.compose.resources.ExperimentalResourceApi
-import org.jetbrains.compose.resources.Resource
-import org.khronos.webgl.ArrayBuffer
-import org.khronos.webgl.Int8Array
-import org.w3c.xhr.XMLHttpRequest
-import org.w3c.xhr.XMLHttpRequestResponseType
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
-import kotlin.wasm.unsafe.*
-import kotlin.wasm.unsafe.withScopedMemoryAllocator
-import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
-import androidx.compose.ui.window.CanvasBasedWindow
-
-@OptIn(ExperimentalComposeUiApi::class)
-fun main() {
-    CanvasBasedWindow("ImageViewer") {
-        ImageViewerWeb()
-    }
-}
-
-@Composable
-internal fun ImageViewerWeb() {
-    val toastState = remember { mutableStateOf<ToastState>(ToastState.Hidden) }
-    val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher }
-    val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) }
-
-    ImageViewerTheme {
-        Surface(
-            modifier = Modifier.fillMaxSize()
-        ) {
-            ImageViewerCommon(
-                dependencies = dependencies
-            )
-            Toast(toastState)
-        }
-    }
-}
-
-@OptIn(ExperimentalResourceApi::class)
-fun getDependencies(ioScope: CoroutineScope, toastState: MutableState<ToastState>) = object : Dependencies {
-    override val pictures: SnapshotStateList<PictureData> = mutableStateListOf(*resourcePictures)
-    override val ioScope: CoroutineScope = ioScope
-    override fun getFilter(type: FilterType): BitmapFilter = when (type) {
-        FilterType.GrayScale -> GrayScaleFilter()
-        FilterType.Pixel -> PixelFilter()
-        FilterType.Blur -> BlurFilter()
-    }
-
-    override val localization: Localization = object : Localization {
-        override val appName = "ImageViewer"
-        override val loading = "Loading images..."
-        override val repoEmpty = "Repository is empty."
-        override val noInternet = "No internet access."
-        override val repoInvalid = "List of images in current repository is invalid or empty."
-        override val refreshUnavailable = "Cannot refresh images."
-        override val loadImageUnavailable = "Cannot load full size image."
-        override val lastImage = "This is last image."
-        override val firstImage = "This is first image."
-        override val picture = "Picture:"
-        override val size = "Size:"
-        override val pixels = "pixels."
-        override val back = "Back"
-    }
-
-    override val httpClient: WrappedHttpClient = object : WrappedHttpClient {
-
-        override suspend fun getAsBytes(urlString: String): ByteArray {
-            return urlResource(urlString).readBytes()
-        }
-    }
-
-    private fun ContentRepository<ImageBitmap>.cacheByUrlAdapter(): ContentRepository<ImageBitmap> {
-        val original = this
-        return object : ContentRepository<ImageBitmap> {
-            val cache = mutableMapOf<String, ImageBitmap>()
-            override suspend fun loadContent(url: String): ImageBitmap {
-                return cache.getOrPut(url) {
-                    original.loadContent(url)
-                }
-            }
-        }
-    }
-
-    override val imageRepository: ContentRepository<ImageBitmap> =
-        createNetworkRepository(httpClient)
-            .adapter { it.toImageBitmap() }
-            .cacheByUrlAdapter()
-
-    override val notification: Notification = object : PopupNotification(localization) {
-        override fun showPopUpMessage(text: String) {
-            toastState.value = ToastState.Shown(text)
-        }
-    }
-}
-
-@ExperimentalResourceApi
-private fun urlResource(path: String): Resource = JSResourceImpl(path)
-
-@OptIn(ExperimentalResourceApi::class)
-private abstract class AbstractResourceImpl(val path: String) : Resource {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        return if (other is AbstractResourceImpl) {
-            path == other.path
-        } else {
-            false
-        }
-    }
-
-    override fun hashCode(): Int {
-        return path.hashCode()
-    }
-}
-
-@ExperimentalResourceApi
-private class JSResourceImpl(path: String) : AbstractResourceImpl(path) {
-    override suspend fun readBytes(): ByteArray {
-        return suspendCoroutine { continuation ->
-            val req = XMLHttpRequest()
-            req.open("GET", path, true)
-            req.responseType = "arraybuffer".asDynamic().unsafeCast<XMLHttpRequestResponseType>()
-
-            req.onload = { _ ->
-                val arrayBuffer = req.response
-                if (arrayBuffer is ArrayBuffer) {
-                    val size = arrayBuffer.byteLength
-                    continuation.resume(arrayBuffer.toByteArray())
-                } else {
-                    continuation.resumeWithException(MissingResourceException(path))
-                }
-                null
-            }
-            req.send(null)
-        }
-    }
-}
-
-private class MissingResourceException constructor(path: String) :
-    Exception("Missing resource with path: $path")
-
-
-private fun ArrayBuffer.toByteArray(): ByteArray {
-    val source = Int8Array(this, 0, byteLength)
-    return jsInt8ArrayToKotlinByteArray(source)
-}
-
-@JsFun(
-    """ (src, size, dstAddr) => {
-        const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
-        mem8.set(src);
-    }
-"""
-)
-internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)
-
-internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray {
-    val size = x.length
-
-    @OptIn(UnsafeWasmMemoryApi::class)
-    return withScopedMemoryAllocator { allocator ->
-        val memBuffer = allocator.allocate(size)
-        val dstAddress = memBuffer.address.toInt()
-        jsExportInt8ArrayToWasm(x, size, dstAddress)
-        ByteArray(size) { i -> (memBuffer + i).loadByte() }
-    }
-}
diff --git a/compose-imageviewer/webApp/build.gradle.kts b/compose-imageviewer/webApp/build.gradle.kts
new file mode 100644
index 0000000..5a43082
--- /dev/null
+++ b/compose-imageviewer/webApp/build.gradle.kts
@@ -0,0 +1,69 @@
+import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
+
+plugins {
+    kotlin("multiplatform")
+    id("org.jetbrains.compose")
+}
+
+val copyJsResources = tasks.create("copyJsResourcesWorkaround", Copy::class.java) {
+    from(project(":shared").file("src/commonMain/resources"))
+    into("build/processedResources/js/main")
+}
+
+val copyWasmResources = tasks.create("copyWasmResourcesWorkaround", Copy::class.java) {
+    from(project(":shared").file("src/commonMain/resources"))
+    into("build/processedResources/wasm/main")
+}
+
+afterEvaluate {
+    project.tasks.getByName("jsProcessResources").finalizedBy(copyJsResources)
+    project.tasks.getByName("wasmProcessResources").finalizedBy(copyWasmResources)
+}
+
+kotlin {
+    js(IR) {
+        moduleName = "imageviewer"
+        browser()
+        binaries.executable()
+    }
+
+    wasm {
+        moduleName = "imageviewer"
+        browser {
+            commonWebpackConfig {
+                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).copy(
+                    open = mapOf(
+                        "app" to mapOf(
+                            "name" to "google chrome",
+                        )
+                    ),
+                )
+            }
+        }
+        binaries.executable()
+    }
+
+    sourceSets {
+        val jsWasmMain by creating {
+            dependencies {
+                implementation(project(":shared"))
+                implementation(compose.runtime)
+                implementation(compose.ui)
+                implementation(compose.foundation)
+                implementation(compose.material)
+                @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
+                implementation(compose.components.resources)
+            }
+        }
+        val jsMain by getting {
+            dependsOn(jsWasmMain)
+        }
+        val wasmMain by getting {
+            dependsOn(jsWasmMain)
+        }
+    }
+}
+
+compose.experimental {
+    web.application {}
+}
diff --git a/compose-imageviewer/webApp/src/jsMain/kotlin/Actuals.kt b/compose-imageviewer/webApp/src/jsMain/kotlin/Actuals.kt
new file mode 100644
index 0000000..1907493
--- /dev/null
+++ b/compose-imageviewer/webApp/src/jsMain/kotlin/Actuals.kt
@@ -0,0 +1,14 @@
+import example.imageviewer.model.WrappedHttpClient
+import io.ktor.client.*
+import io.ktor.client.engine.js.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+
+actual fun createWrappedHttpClient(): WrappedHttpClient {
+    return object : WrappedHttpClient {
+        private val ktorClient = HttpClient(JsClient())
+        override suspend fun getAsBytes(urlString: String): ByteArray {
+            return ktorClient.get(urlString).readBytes()
+        }
+    }
+}
diff --git a/compose-imageviewer/webApp/src/jsMain/kotlin/Main.kt b/compose-imageviewer/webApp/src/jsMain/kotlin/Main.kt
new file mode 100644
index 0000000..8c15620
--- /dev/null
+++ b/compose-imageviewer/webApp/src/jsMain/kotlin/Main.kt
@@ -0,0 +1,19 @@
+import androidx.compose.runtime.*
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.window.CanvasBasedWindow
+import example.imageviewer.*
+import example.imageviewer.model.*
+import io.ktor.client.*
+import io.ktor.client.engine.js.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import org.jetbrains.skiko.wasm.onWasmReady
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+    onWasmReady {
+        CanvasBasedWindow("ImageViewer") {
+            ImageViewerWeb()
+        }
+    }
+}
diff --git a/compose-imageviewer/shared/src/jsMain/resources/index.html b/compose-imageviewer/webApp/src/jsMain/resources/index.html
similarity index 55%
rename from compose-imageviewer/shared/src/jsMain/resources/index.html
rename to compose-imageviewer/webApp/src/jsMain/resources/index.html
index 6ab6ea9..283ee9b 100644
--- a/compose-imageviewer/shared/src/jsMain/resources/index.html
+++ b/compose-imageviewer/webApp/src/jsMain/resources/index.html
@@ -6,11 +6,7 @@
     <script src="skiko.js"> </script>
 </head>
 <body>
-<center>
-    <div style="background-color: darkslategrey;">
-        <canvas id="ComposeTarget" width="800" height="1000"></canvas>
-    </div>
-</center>
+<canvas id="ComposeTarget"></canvas>
 <script src="imageviewer.js"> </script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/compose-imageviewer/shared/src/jsMain/kotlin/Main.js.kt b/compose-imageviewer/webApp/src/jsWasmMain/kotlin/Common.kt
similarity index 79%
rename from compose-imageviewer/shared/src/jsMain/kotlin/Main.js.kt
rename to compose-imageviewer/webApp/src/jsWasmMain/kotlin/Common.kt
index 6eb4eae..c8cf5d0 100644
--- a/compose-imageviewer/shared/src/jsMain/kotlin/Main.js.kt
+++ b/compose-imageviewer/webApp/src/jsWasmMain/kotlin/Common.kt
@@ -2,13 +2,9 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.Surface
 import androidx.compose.runtime.*
 import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.window.CanvasBasedWindow
-import androidx.compose.ui.window.Window
 import example.imageviewer.*
-import example.imageviewer.ImageViewerCommon
 import example.imageviewer.core.BitmapFilter
 import example.imageviewer.core.FilterType
 import example.imageviewer.model.*
@@ -19,22 +15,10 @@ import example.imageviewer.style.ImageViewerTheme
 import example.imageviewer.utils.ioDispatcher
 import example.imageviewer.view.Toast
 import example.imageviewer.view.ToastState
-import io.ktor.client.*
-import io.ktor.client.engine.js.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
 import kotlinx.coroutines.CoroutineScope
 import org.jetbrains.compose.resources.ExperimentalResourceApi
-import org.jetbrains.skiko.wasm.onWasmReady
 
-@OptIn(ExperimentalComposeUiApi::class)
-fun main() {
-    onWasmReady {
-        CanvasBasedWindow("ImageViewer") {
-            ImageViewerWeb()
-        }
-    }
-}
+expect fun createWrappedHttpClient(): WrappedHttpClient
 
 @Composable
 internal fun ImageViewerWeb() {
@@ -80,20 +64,29 @@ fun getDependencies(ioScope: CoroutineScope, toastState: MutableState<ToastState
         override val back = "Back"
     }
 
-    override val httpClient: WrappedHttpClient = object : WrappedHttpClient {
-        private val ktorClient = HttpClient(JsClient())
-        override suspend fun getAsBytes(urlString: String): ByteArray {
-            return ktorClient.get(urlString).readBytes()
+
+    override val httpClient: WrappedHttpClient = createWrappedHttpClient()
+
+    private fun ContentRepository<ImageBitmap>.cacheByUrlAdapter(): ContentRepository<ImageBitmap> {
+        val original = this
+        return object : ContentRepository<ImageBitmap> {
+            val cache = mutableMapOf<String, ImageBitmap>()
+            override suspend fun loadContent(url: String): ImageBitmap {
+                return cache.getOrPut(url) {
+                    original.loadContent(url)
+                }
+            }
         }
     }
 
     override val imageRepository: ContentRepository<ImageBitmap> =
         createNetworkRepository(httpClient)
             .adapter { it.toImageBitmap() }
+            .cacheByUrlAdapter()
 
     override val notification: Notification = object : PopupNotification(localization) {
         override fun showPopUpMessage(text: String) {
             toastState.value = ToastState.Shown(text)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose-imageviewer/webApp/src/wasmMain/kotlin/Actuals.kt b/compose-imageviewer/webApp/src/wasmMain/kotlin/Actuals.kt
new file mode 100644
index 0000000..3fb53f2
--- /dev/null
+++ b/compose-imageviewer/webApp/src/wasmMain/kotlin/Actuals.kt
@@ -0,0 +1,88 @@
+import androidx.compose.runtime.*
+import example.imageviewer.*
+import example.imageviewer.model.*
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.Resource
+import org.khronos.webgl.ArrayBuffer
+import org.khronos.webgl.Int8Array
+import org.w3c.xhr.XMLHttpRequest
+import org.w3c.xhr.XMLHttpRequestResponseType
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlin.wasm.unsafe.*
+
+actual fun createWrappedHttpClient(): WrappedHttpClient {
+    return object : WrappedHttpClient {
+        override suspend fun getAsBytes(urlString: String): ByteArray {
+            return JSResourceImpl(urlString).readBytes()
+        }
+    }
+}
+
+@OptIn(ExperimentalResourceApi::class)
+private abstract class AbstractResourceImpl(val path: String) : Resource {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        return if (other is AbstractResourceImpl) {
+            path == other.path
+        } else {
+            false
+        }
+    }
+
+    override fun hashCode(): Int {
+        return path.hashCode()
+    }
+}
+private class JSResourceImpl(path: String) : AbstractResourceImpl(path) {
+    override suspend fun readBytes(): ByteArray {
+        return suspendCoroutine { continuation ->
+            val req = XMLHttpRequest()
+            req.open("GET", path, true)
+            req.responseType = "arraybuffer".asDynamic().unsafeCast<XMLHttpRequestResponseType>()
+
+            req.onload = { _ ->
+                val arrayBuffer = req.response
+                if (arrayBuffer is ArrayBuffer) {
+                    val size = arrayBuffer.byteLength
+                    continuation.resume(arrayBuffer.toByteArray())
+                } else {
+                    continuation.resumeWithException(MissingResourceException(path))
+                }
+                null
+            }
+            req.send(null)
+        }
+    }
+}
+
+private class MissingResourceException constructor(path: String) :
+    Exception("Missing resource with path: $path")
+
+
+private fun ArrayBuffer.toByteArray(): ByteArray {
+    val source = Int8Array(this, 0, byteLength)
+    return jsInt8ArrayToKotlinByteArray(source)
+}
+
+@JsFun(
+    """ (src, size, dstAddr) => {
+        const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size);
+        mem8.set(src);
+    }
+"""
+)
+internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int)
+
+internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray {
+    val size = x.length
+
+    @OptIn(UnsafeWasmMemoryApi::class)
+    return withScopedMemoryAllocator { allocator ->
+        val memBuffer = allocator.allocate(size)
+        val dstAddress = memBuffer.address.toInt()
+        jsExportInt8ArrayToWasm(x, size, dstAddress)
+        ByteArray(size) { i -> (memBuffer + i).loadByte() }
+    }
+}
diff --git a/compose-imageviewer/webApp/src/wasmMain/kotlin/Main.wasm.kt b/compose-imageviewer/webApp/src/wasmMain/kotlin/Main.wasm.kt
new file mode 100644
index 0000000..ef398a3
--- /dev/null
+++ b/compose-imageviewer/webApp/src/wasmMain/kotlin/Main.wasm.kt
@@ -0,0 +1,13 @@
+import androidx.compose.runtime.*
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.window.CanvasBasedWindow
+import example.imageviewer.*
+import example.imageviewer.model.*
+import kotlin.wasm.unsafe.*
+
+@OptIn(ExperimentalComposeUiApi::class)
+fun main() {
+    CanvasBasedWindow("ImageViewer") {
+        ImageViewerWeb()
+    }
+}
diff --git a/compose-imageviewer/shared/src/wasmMain/resources/index.html b/compose-imageviewer/webApp/src/wasmMain/resources/index.html
similarity index 100%
rename from compose-imageviewer/shared/src/wasmMain/resources/index.html
rename to compose-imageviewer/webApp/src/wasmMain/resources/index.html
diff --git a/compose-imageviewer/shared/src/wasmMain/resources/load.mjs b/compose-imageviewer/webApp/src/wasmMain/resources/load.mjs
similarity index 100%
rename from compose-imageviewer/shared/src/wasmMain/resources/load.mjs
rename to compose-imageviewer/webApp/src/wasmMain/resources/load.mjs

From fdca92b3b04b00671c4503ee13666c728506f0b4 Mon Sep 17 00:00:00 2001
From: Oleksandr Karpovich <a.n.karpovich@gmail.com>
Date: Fri, 14 Apr 2023 18:13:53 +0200
Subject: [PATCH 2/2] Update compose-imageviewer README.md

---
 compose-imageviewer/README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/compose-imageviewer/README.md b/compose-imageviewer/README.md
index 7acb0d8..2a9f6c6 100755
--- a/compose-imageviewer/README.md
+++ b/compose-imageviewer/README.md
@@ -21,7 +21,7 @@ Check out the repo, navigate to the project folder, and use the following comman
 
 ### Run Web version via Gradle
 
-`./gradlew :shared:wasmRun`
+`./gradlew :webApp:wasmRun`
 
 ### Run Desktop version via Gradle