Artem Kobzar
7 months ago
committed by
GitHub
31 changed files with 3643 additions and 0 deletions
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 4.3 MiB |
@ -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" |
@ -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() |
@ -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 |
||||||
|
) |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
} |
@ -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") |
||||||
|
} |
||||||
|
} |
@ -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() |
@ -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 |
@ -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() |
@ -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 |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
package example.imageviewer.utils |
||||||
|
|
||||||
|
@JsModule("uuid") |
||||||
|
external object UUID { |
||||||
|
fun v4(): String |
||||||
|
} |
@ -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) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 { |
||||||
|
|
||||||
|
} |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
@ -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), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
) |
||||||
|
} |
@ -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), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -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() |
||||||
|
} |
@ -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) |
||||||
|
) |
||||||
|
} |
@ -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" |
@ -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 {} |
||||||
|
} |
@ -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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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> |
@ -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) |
||||||
|
} |
@ -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() |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue