Browse Source

Add Wasm and JS support

pull/4745/head
Artem Kobzar 2 weeks ago
parent
commit
cfee9f156a
  1. 4
      examples/imageviewer/README.md
  2. 1
      examples/imageviewer/gradle.properties
  3. 2841
      examples/imageviewer/kotlin-js-store/yarn.lock
  4. 1
      examples/imageviewer/settings.gradle.kts
  5. 23
      examples/imageviewer/shared/build.gradle.kts
  6. BIN
      examples/imageviewer/shared/src/commonMain/composeResources/drawable/dummy_map.png
  7. 11
      examples/imageviewer/shared/src/jsMain/kotlin/example/imageviewer/Localization.js.kt
  8. 8
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/ImageBitmap.jsWasm.kt
  9. 36
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/ImageStorage.kt
  10. 13
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/PopupNotification.kt
  11. 10
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/SharePicture.kt
  12. 24
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/filter/BitmapFilter.jsWasm.kt
  13. 9
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/model/Page.jsWasm.kt
  14. 22
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/platform.jsWasm.kt
  15. 84
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/utils/GraphicsMath.jsWasm.kt
  16. 6
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/utils/uuid.kt
  17. 64
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/CameraView.jsWasm.kt
  18. 85
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/EditMemoryDialog.jsWasm.kt
  19. 48
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/GalleryScreen.jsWasm.kt
  20. 26
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/LocationVisualizer.jsWasm.kt
  21. 27
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/ScrollableColumn.jsWasm.kt
  22. 12
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/Tooltip.jsWasm.kt
  23. 22
      examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/ZoomControllerView.jsWasm.kt
  24. 11
      examples/imageviewer/shared/src/wasmJsMain/kotlin/example/imageviewer/Localization.wasm.kt
  25. 83
      examples/imageviewer/webApp/build.gradle.kts
  26. 12
      examples/imageviewer/webApp/src/jsMain/kotlin/Main.js.kt
  27. 12
      examples/imageviewer/webApp/src/jsMain/resources/index.html
  28. 34
      examples/imageviewer/webApp/src/jsWasmMain/kotlin/Common.kt
  29. 14
      examples/imageviewer/webApp/src/wasmJsMain/kotlin/Main.wasm.kt
  30. 95
      examples/imageviewer/webApp/src/wasmJsMain/resources/index.html
  31. 5
      examples/imageviewer/webApp/src/wasmJsMain/resources/load.mjs

4
examples/imageviewer/README.md

@ -27,6 +27,10 @@ Choose a run configuration for an appropriate target in IDE and run it.
# outputs are written to desktopApp/build/compose/binaries
```
## Run on Web via Gradle
`./gradlew :web:wasmJsRun`
### Running Android application
- Get a [Google Maps API key](https://developers.google.com/maps/documentation/android-sdk/get-api-key)

1
examples/imageviewer/gradle.properties

@ -6,6 +6,7 @@ org.gradle.configuration-cache=true
org.gradle.caching=true
org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.macos.enabled=true
org.jetbrains.compose.experimental.wasm.enabled=true
kotlin.version=1.9.23
agp.version=8.0.2
compose.version=1.6.2

2841
examples/imageviewer/kotlin-js-store/yarn.lock

File diff suppressed because it is too large Load Diff

1
examples/imageviewer/settings.gradle.kts

@ -29,5 +29,6 @@ rootProject.name = "imageviewer"
include(":androidApp")
include(":shared")
include(":webApp")
include(":desktopApp")
include(":mapview-desktop")

23
examples/imageviewer/shared/build.gradle.kts

@ -11,6 +11,11 @@ version = "1.0-SNAPSHOT"
kotlin {
androidTarget()
jvm("desktop")
js {
browser()
useEsModules()
}
wasmJs { browser() }
listOf(
iosX64(),
@ -29,6 +34,7 @@ kotlin {
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
@ -38,6 +44,7 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
androidMain.dependencies {
api("androidx.activity:activity-compose:1.8.2")
api("androidx.appcompat:appcompat:1.6.1")
@ -50,6 +57,22 @@ kotlin {
implementation("com.google.android.gms:play-services-location:21.1.0")
implementation("com.google.maps.android:maps-compose:2.11.2")
}
val jsWasmMain by creating {
dependsOn(commonMain.get())
dependencies {
implementation(npm("uuid", "^9.0.1"))
}
}
val jsMain by getting {
dependsOn(jsWasmMain)
}
val wasmJsMain by getting {
dependsOn(jsWasmMain)
}
val desktopMain by getting
desktopMain.dependencies {
implementation(compose.desktop.common)

BIN
examples/imageviewer/shared/src/commonMain/composeResources/drawable/dummy_map.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

11
examples/imageviewer/shared/src/jsMain/kotlin/example/imageviewer/Localization.js.kt

@ -0,0 +1,11 @@
package example.imageviewer
import kotlinx.browser.window
actual fun getCurrentLanguage(): AvailableLanguages =
when (window.navigator.languages.firstOrNull() ?: window.navigator.language) {
"de" -> AvailableLanguages.DE
else -> AvailableLanguages.EN
}
actual fun getCurrentPlatform(): String = "Web JS"

8
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/ImageBitmap.jsWasm.kt

@ -0,0 +1,8 @@
package example.imageviewer
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import org.jetbrains.skia.Image
actual fun ByteArray.toImageBitmap(): ImageBitmap =
Image.makeFromEncoded(this).toComposeImageBitmap()

36
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/ImageStorage.kt

@ -0,0 +1,36 @@
package example.imageviewer
import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.model.PictureData
// TODO: Rework it with some web service to store the images
class WebImageStorage : ImageStorage {
private val pictures = HashMap<String, SavedPicture>()
override fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) {
pictures[picture.id] = SavedPicture(picture, image.imageBitmap)
}
override fun delete(picture: PictureData.Camera) {
pictures.remove(picture.id)
}
override fun rewrite(picture: PictureData.Camera) {
pictures[picture.id]?.let {
pictures[picture.id] = it.copy(data = picture)
}
}
override suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap {
return pictures[picture.id]?.bitmap ?: error("Picture was not found")
}
override suspend fun getImage(picture: PictureData.Camera): ImageBitmap {
return pictures[picture.id]?.bitmap ?: error("Picture was not found")
}
private data class SavedPicture(
val data: PictureData,
val bitmap: ImageBitmap
)
}

13
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/PopupNotification.kt

@ -0,0 +1,13 @@
package example.imageviewer
import androidx.compose.runtime.MutableState
import example.imageviewer.view.ToastState
class WebPopupNotification(
private val toastState: MutableState<ToastState>,
localization: Localization
) : PopupNotification(localization) {
override fun showPopUpMessage(text: String) {
toastState.value = ToastState.Shown(text)
}
}

10
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/SharePicture.kt

@ -0,0 +1,10 @@
package example.imageviewer
import example.imageviewer.filter.PlatformContext
import example.imageviewer.model.PictureData
class WebSharePicture : SharePicture {
override fun share(context: PlatformContext, picture: PictureData) {
error("Should not be called")
}
}

24
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/filter/BitmapFilter.jsWasm.kt

@ -0,0 +1,24 @@
package example.imageviewer.filter
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.*
import example.imageviewer.utils.applyBlurFilter
import example.imageviewer.utils.applyGrayScaleFilter
import example.imageviewer.utils.applyPixelFilter
actual fun grayScaleFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap {
return applyGrayScaleFilter(bitmap.asSkiaBitmap()).asComposeImageBitmap()
}
actual fun pixelFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap {
return applyPixelFilter(bitmap.asSkiaBitmap()).asComposeImageBitmap()
}
actual fun blurFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap {
return applyBlurFilter(bitmap.asSkiaBitmap()).asComposeImageBitmap()
}
actual class PlatformContext
@Composable
actual fun getPlatformContext(): PlatformContext = PlatformContext()

9
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/model/Page.jsWasm.kt

@ -0,0 +1,9 @@
package example.imageviewer.model
actual class MemoryPage actual constructor(actual val pictureIndex: Int) : Page
actual class CameraPage : Page
actual class FullScreenPage actual constructor(actual val pictureIndex: Int) : Page
actual class GalleryPage : Page

22
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/platform.jsWasm.kt

@ -0,0 +1,22 @@
package example.imageviewer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import example.imageviewer.utils.UUID
import kotlinx.coroutines.Dispatchers
class WebStorableImage(
val imageBitmap: ImageBitmap
)
actual typealias PlatformStorableImage = WebStorableImage
actual val ioDispatcher = Dispatchers.Default
actual val isShareFeatureSupported: Boolean = false
actual val shareIcon: ImageVector = Icons.Filled.Share
actual fun createUUID(): String = UUID.v4()

84
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/utils/GraphicsMath.jsWasm.kt

@ -0,0 +1,84 @@
package example.imageviewer.utils
import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorInfo
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.FilterTileMode
import org.jetbrains.skia.Image
import org.jetbrains.skia.ImageFilter
import org.jetbrains.skia.ImageInfo
import org.jetbrains.skia.Paint
fun scaleBitmapAspectRatio(
bitmap: Bitmap,
width: Int,
height: Int
): Bitmap {
val boundWidth = width.toFloat()
val boundHeight = height.toFloat()
val ratioX = boundWidth / bitmap.width
val ratioY = boundHeight / bitmap.height
val ratio = if (ratioX < ratioY) ratioX else ratioY
val resultWidth = (bitmap.width * ratio).toInt()
val resultHeight = (bitmap.height * ratio).toInt()
val result = Bitmap().apply {
allocN32Pixels(resultWidth, resultHeight)
}
val canvas = Canvas(result)
canvas.drawImageRect(Image.makeFromBitmap(bitmap), result.bounds.toRect())
canvas.readPixels(result, 0, 0)
canvas.close()
return result
}
fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
val imageInfo = ImageInfo(
width = bitmap.width,
height = bitmap.height,
colorInfo = ColorInfo(ColorType.GRAY_8, ColorAlphaType.PREMUL, null)
)
val result = Bitmap().apply {
allocPixels(imageInfo)
}
val canvas = Canvas(result)
canvas.drawImageRect(Image.makeFromBitmap(bitmap), bitmap.bounds.toRect())
canvas.readPixels(result, 0, 0)
canvas.close()
return result
}
fun applyPixelFilter(bitmap: Bitmap): Bitmap {
val width = bitmap.width
val height = bitmap.height
var result = scaleBitmapAspectRatio(bitmap, width / 4, height / 4)
result = scaleBitmapAspectRatio(result, width, height)
return result
}
fun applyBlurFilter(bitmap: Bitmap): Bitmap {
val result = Bitmap().apply {
allocN32Pixels(bitmap.width, bitmap.height)
}
val blur = Paint().apply {
imageFilter = ImageFilter.makeBlur(3f, 3f, FilterTileMode.CLAMP)
}
val canvas = Canvas(result)
canvas.saveLayer(null, blur)
canvas.drawImageRect(Image.makeFromBitmap(bitmap), bitmap.bounds.toRect())
canvas.restore()
canvas.readPixels(result, 0, 0)
canvas.close()
return result
}

6
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/utils/uuid.kt

@ -0,0 +1,6 @@
package example.imageviewer.utils
@JsModule("uuid")
external object UUID {
fun v4(): String
}

64
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/CameraView.jsWasm.kt

@ -0,0 +1,64 @@
package example.imageviewer.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.unit.dp
import example.imageviewer.*
import example.imageviewer.icon.IconPhotoCamera
import example.imageviewer.model.PictureData
import example.imageviewer.model.createCameraPictureData
import imageviewer.shared.generated.resources.Res
@Composable
actual fun CameraView(
modifier: Modifier,
onCapture: (picture: PictureData.Camera, image: PlatformStorableImage) -> Unit
) {
val randomPicture = remember { resourcePictures.random() }
var imageBitmap by remember { mutableStateOf(ImageBitmap(1, 1)) }
LaunchedEffect(randomPicture) {
imageBitmap = Res.readBytes(randomPicture.resource).toImageBitmap()
}
Box(Modifier.fillMaxSize().background(Color.Black)) {
Image(
bitmap = imageBitmap,
contentDescription = "Camera stub",
Modifier.fillMaxSize()
)
Text(
text = """
Camera is not available on Web for now.
Instead, we will use a random picture.
""".trimIndent(),
color = Color.White,
modifier = Modifier.align(Alignment.Center)
.background(
color = Color.Black.copy(alpha = 0.7f),
shape = RoundedCornerShape(10.dp)
)
.padding(20.dp)
)
val nameAndDescription = createNewPhotoNameAndDescription()
CircularButton(
imageVector = IconPhotoCamera,
modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
) {
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
description = nameAndDescription.description,
gps = randomPicture.gps
),
WebStorableImage(imageBitmap)
)
}
}
}

85
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/EditMemoryDialog.jsWasm.kt

@ -0,0 +1,85 @@
package example.imageviewer.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AlertDialog
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
actual fun BoxScope.EditMemoryDialog(
previousName: String,
previousDescription: String,
save: (name: String, description: String) -> Unit
) {
var name by remember { mutableStateOf(previousName) }
var description by remember { mutableStateOf(previousDescription) }
AlertDialog(
onDismissRequest = {
save(name, description)
},
buttons = {
Column(
modifier = Modifier
.align(Alignment.Center)
.padding(30.dp)
.clip(RoundedCornerShape(20.dp))
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TextField(
value = name,
onValueChange = { name = it },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White,
),
textStyle = LocalTextStyle.current.copy(
textAlign = TextAlign.Center,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
),
)
TextField(
value = description,
onValueChange = { description = it },
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.White,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
)
)
}
},
)
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.4f))
.clickable {
}
) {
}
}

48
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/GalleryScreen.jsWasm.kt

@ -0,0 +1,48 @@
package example.imageviewer.view
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
// On the desktop, include a scrollbar
@Composable
actual fun GalleryLazyVerticalGrid(
columns: GridCells,
modifier: Modifier,
verticalArrangement: Arrangement.Vertical,
horizontalArrangement: Arrangement.Horizontal,
content: LazyGridScope.() -> Unit
) {
Box(
modifier = modifier
) {
val scrollState = rememberLazyGridState()
val adapter = rememberScrollbarAdapter(scrollState)
LazyVerticalGrid(
columns = columns,
modifier = Modifier.fillMaxSize(),
state = scrollState,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
content = content
)
Box(
modifier = Modifier.matchParentSize()
){
VerticalScrollbar(
adapter = adapter,
modifier = Modifier.align(Alignment.CenterEnd),
)
}
}
}

26
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/LocationVisualizer.jsWasm.kt

@ -0,0 +1,26 @@
package example.imageviewer.view
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import example.imageviewer.model.GpsPosition
import imageviewer.shared.generated.resources.Res
import imageviewer.shared.generated.resources.dummy_map
import org.jetbrains.compose.resources.painterResource
@Composable
actual fun LocationVisualizer(
modifier: Modifier,
gps: GpsPosition,
title: String,
parentScrollEnableState: MutableState<Boolean>
) {
Image(
painter = painterResource(Res.drawable.dummy_map),
contentDescription = "Map",
contentScale = ContentScale.Crop,
modifier = modifier
)
}

27
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/ScrollableColumn.jsWasm.kt

@ -0,0 +1,27 @@
package example.imageviewer.view
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
actual fun ScrollableColumn(modifier: Modifier, content: @Composable ColumnScope.() -> Unit) {
val scrollState = rememberScrollState()
Box(modifier) {
Column(Modifier.verticalScroll(scrollState)) {
content()
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.padding(4.dp)
.fillMaxHeight(),
adapter = rememberScrollbarAdapter(scrollState),
)
}
}

12
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/Tooltip.jsWasm.kt

@ -0,0 +1,12 @@
package example.imageviewer.view
import androidx.compose.runtime.Composable
@Composable
actual fun Tooltip(
text: String,
content: @Composable () -> Unit
) {
//No tooltip for web
content()
}

22
examples/imageviewer/shared/src/jsWasmMain/kotlin/example/imageviewer/view/ZoomControllerView.jsWasm.kt

@ -0,0 +1,22 @@
package example.imageviewer.view
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Slider
import androidx.compose.material.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import example.imageviewer.model.ScalableState
@Composable
actual fun ZoomControllerView(modifier: Modifier, scalableState: ScalableState) {
Slider(
modifier = modifier.fillMaxWidth(0.5f).padding(12.dp),
value = scalableState.zoom,
valueRange = scalableState.zoomLimits.start..scalableState.zoomLimits.endInclusive,
onValueChange = { scalableState.setZoom(it) },
colors = SliderDefaults.colors(thumbColor = Color.White, activeTrackColor = Color.White)
)
}

11
examples/imageviewer/shared/src/wasmJsMain/kotlin/example/imageviewer/Localization.wasm.kt

@ -0,0 +1,11 @@
package example.imageviewer
import kotlinx.browser.window
actual fun getCurrentLanguage(): AvailableLanguages =
when (window.navigator.languages[0]?.toString() ?: window.navigator.language) {
"de" -> AvailableLanguages.DE
else -> AvailableLanguages.EN
}
actual fun getCurrentPlatform(): String = "Web Wasm"

83
examples/imageviewer/webApp/build.gradle.kts

@ -0,0 +1,83 @@
import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask
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/composeResources"))
into("build/processedResources/js/main")
}
tasks.withType<DefaultIncrementalSyncTask> {
dependsOn(copyJsResources)
}
val copyWasmResources = tasks.create("copyWasmResourcesWorkaround", Copy::class.java) {
from(project(":shared").file("src/commonMain/composeResources"))
into("build/processedResources/wasmJs/main")
}
afterEvaluate {
project.tasks.getByName("jsProcessResources").finalizedBy(copyJsResources)
project.tasks.getByName("wasmJsProcessResources").finalizedBy(copyWasmResources)
project.tasks.getByName("wasmJsDevelopmentExecutableCompileSync").dependsOn(copyWasmResources)
}
val rootDirPath = project.rootDir.path
kotlin {
js {
moduleName = "imageviewer"
browser {
commonWebpackConfig {
outputFileName = "imageviewer.js"
}
}
binaries.executable()
useEsModules()
}
wasmJs {
moduleName = "imageviewer"
browser {
commonWebpackConfig {
devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
static = (static ?: mutableListOf()).apply {
// Serve sources to debug inside browser
add(rootDirPath)
add(rootDirPath + "/shared/")
add(rootDirPath + "/webApp/")
}
}
}
}
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 wasmJsMain by getting {
dependsOn(jsWasmMain)
}
}
}
compose.experimental {
web.application {}
}

12
examples/imageviewer/webApp/src/jsMain/kotlin/Main.js.kt

@ -0,0 +1,12 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import org.jetbrains.skiko.wasm.onWasmReady
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
onWasmReady {
CanvasBasedWindow("ImageViewer") {
ImageViewerWeb()
}
}
}

12
examples/imageviewer/webApp/src/jsMain/resources/index.html

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ImageViewer</title>
<script src="skiko.js"> </script>
</head>
<body>
<canvas id="ComposeTarget"></canvas>
<script src="imageviewer.js"> </script>
</body>
</html>

34
examples/imageviewer/webApp/src/jsWasmMain/kotlin/Common.kt

@ -0,0 +1,34 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import example.imageviewer.*
import example.imageviewer.style.ImageViewerTheme
import example.imageviewer.ioDispatcher
import example.imageviewer.view.Toast
import example.imageviewer.view.ToastState
import kotlinx.coroutines.CoroutineScope
@Composable
internal fun ImageViewerWeb() {
val toastState = remember { mutableStateOf<ToastState>(ToastState.Hidden) }
val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher }
val dependencies = remember(ioScope) { getDependencies(toastState) }
ImageViewerTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
ImageViewerCommon(
dependencies = dependencies
)
Toast(toastState)
}
}
}
fun getDependencies(toastState: MutableState<ToastState>) = object : Dependencies() {
override val imageStorage: ImageStorage = WebImageStorage()
override val sharePicture = WebSharePicture()
override val notification = WebPopupNotification(toastState, localization)
}

14
examples/imageviewer/webApp/src/wasmJsMain/kotlin/Main.wasm.kt

@ -0,0 +1,14 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.configureWebResources
@OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class)
fun main() {
configureWebResources {
resourcePathMapping { path -> "./$path" }
}
CanvasBasedWindow("ImageViewer") {
ImageViewerWeb()
}
}

95
examples/imageviewer/webApp/src/wasmJsMain/resources/index.html

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Image Viewer (Kotlin/Wasm)</title>
<script type="application/javascript" src="skiko.js"></script>
<script type="application/javascript" src="webApp.js"></script>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: white;
overflow: hidden;
}
#warning {
position: absolute;
top: 100px;
left: 100px;
max-width: 830px;
z-index: 100;
background-color: white;
font-size: initial;
display: none;
}
#warning li {
padding-bottom: 15px;
}
#warning span.code {
font-family: monospace;
}
ul {
margin-top: 0;
margin-bottom: 15px;
}
#footer {
position: fixed;
bottom: 0;
width: 100%;
z-index: 1000;
background-color: white;
font-size: initial;
}
#close {
position: absolute;
top: 0;
right: 10px;
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="ComposeTarget"></canvas>
<div id="warning">
Please make sure that your runtime environment supports the latest version of Wasm GC and Exception-Handling proposals.
For more information, see <a href="https://kotl.in/wasm-help">https://kotl.in/wasm-help</a>.
<br/>
<br/>
<ul>
<li>For <b>Chrome</b> and <b>Chromium-based</b> browsers (Edge, Brave etc.), it <b>should just work</b> since version 119.</li>
<li>For <b>Firefox</b> 120 it <b>should just work</b>.</li>
<li>For <b>Firefox</b> 119:
<ol>
<li>Open <span class="code">about:config</span> in the browser.</li>
<li>Enable <strong>javascript.options.wasm_gc</strong>.</li>
<li>Refresh this page.</li>
</ol>
</li>
</ul>
</div>
</body>
<script type="application/javascript" >
const unhandledError = (event, error) => {
if (error instanceof WebAssembly.CompileError) {
document.getElementById("warning").style.display="initial";
// Hide a Scary Webpack Overlay which is less informative in this case.
const webpackOverlay = document.getElementById("webpack-dev-server-client-overlay");
if (webpackOverlay != null) webpackOverlay.style.display="none";
}
}
addEventListener("error", (event) => unhandledError(event, event.error));
addEventListener("unhandledrejection", (event) => unhandledError(event, event.reason));
</script>
</html>

5
examples/imageviewer/webApp/src/wasmJsMain/resources/load.mjs

@ -0,0 +1,5 @@
import { instantiate } from './imageviewer.uninstantiated.mjs';
await wasmSetup;
instantiate({ skia: Module['asm'] });
Loading…
Cancel
Save