Browse Source

Add some features to sample (#2934)

pull/2941/head
dima.avdeev 2 years ago committed by GitHub
parent
commit
cbada1099f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      experimental/examples/imageviewer/README.md
  2. 6
      experimental/examples/imageviewer/androidApp/build.gradle.kts
  3. 11
      experimental/examples/imageviewer/androidApp/src/androidMain/AndroidManifest.xml
  4. 5
      experimental/examples/imageviewer/androidApp/src/androidMain/res/values/themes.xml
  5. 1
      experimental/examples/imageviewer/build.gradle.kts
  6. 2
      experimental/examples/imageviewer/default.local.properties
  7. 5
      experimental/examples/imageviewer/gradle.properties
  8. BIN
      experimental/examples/imageviewer/screenshots/desktop-run-configuration.png
  9. BIN
      experimental/examples/imageviewer/screenshots/imageviewer.png
  10. 0
      experimental/examples/imageviewer/screenshots/run-configurations.png
  11. 21
      experimental/examples/imageviewer/shared/build.gradle.kts
  12. 7
      experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml
  13. 63
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/AndroidImageStorage.kt
  14. 3
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/Localization.android.kt
  15. 98
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/filter/BitmapFilter.android.kt
  16. 14
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
  17. 13
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
  18. 12
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
  19. 13
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt
  20. 7
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/IOScope.android.kt
  21. 150
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt
  22. 53
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt
  23. 24
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/LocationVisualizer.android.kt
  24. 6
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/Tooltip.android.kt
  25. 11
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ZoomControllerView.android.kt
  26. 13
      experimental/examples/imageviewer/shared/src/androidMain/res/values-de/strings.xml
  27. 13
      experimental/examples/imageviewer/shared/src/androidMain/res/values/strings.xml
  28. 139
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt
  29. 97
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
  30. 43
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Localization.kt
  31. 33
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/NameAndDescription.common.kt
  32. 213
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ResourcePictures.kt
  33. 7
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/BitmapFilter.kt
  34. 20
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/filter/BitmapFilter.common.kt
  35. 2
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/filter/FilterType.kt
  36. 2
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Config.kt
  37. 23
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ContentRepository.kt
  38. 61
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt
  39. 67
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PhotoGallery.kt
  40. 30
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Picture.kt
  41. 62
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt
  42. 8
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ScalableState.kt
  43. 7
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt
  44. 4
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
  45. 5
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/utils/IOScope.kt
  46. 39
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt
  47. 5
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraView.common.kt
  48. 16
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CircularButton.kt
  49. 218
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  50. 145
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImageScreen.kt
  51. 142
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt
  52. 3
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LocationVisualizer.common.kt
  53. 90
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt
  54. 16
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt
  55. 38
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Thumbnail.kt
  56. 30
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ThumbnailImage.kt
  57. 3
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
  58. 1
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Tooltip.common.kt
  59. 9
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ZoomControllerView.common.kt
  60. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/1-thumbnail.jpg
  61. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/1.jpg
  62. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/10-thumbnail.jpg
  63. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/10.jpg
  64. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/11-thumbnail.jpg
  65. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/11.jpg
  66. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/12-thumbnail.jpg
  67. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/12.jpg
  68. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/13-thumbnail.jpg
  69. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/13.jpg
  70. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/14-thumbnail.jpg
  71. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/14.jpg
  72. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/15-thumbnail.jpg
  73. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/15.jpg
  74. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/16-thumbnail.jpg
  75. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/16.jpg
  76. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/17-thumbnail.jpg
  77. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/17.jpg
  78. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/2-thumbnail.jpg
  79. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/2.jpg
  80. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/3-thumbnail.jpg
  81. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/3.jpg
  82. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/4-thumbnail.jpg
  83. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/4.jpg
  84. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/5-thumbnail.jpg
  85. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/5.jpg
  86. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/6-thumbnail.jpg
  87. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/6.jpg
  88. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/7-thumbnail.jpg
  89. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/7.jpg
  90. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/8-thumbnail.jpg
  91. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/8.jpg
  92. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/9-thumbnail.jpg
  93. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/9.jpg
  94. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/back.png
  95. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/blur_off.png
  96. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/blur_on.png
  97. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/empty.png
  98. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_off.png
  99. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_on.png
  100. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/pixel_off.png
  101. Some files were not shown because too many files have changed in this diff Show More

10
experimental/examples/imageviewer/README.md

@ -7,7 +7,7 @@ based on Compose Multiplatform UI library (desktop, android and iOS).
Choose a run configuration for an appropriate target in IDE and run it. Choose a run configuration for an appropriate target in IDE and run it.
![run-configurations.png](run-configurations.png) ![run-configurations.png](screenshots/run-configurations.png)
To run on iOS device, please correct `TEAM_ID` value in `iosApp/Configuration/Config.xcconfig` with your Apple Team ID. To run on iOS device, please correct `TEAM_ID` value in `iosApp/Configuration/Config.xcconfig` with your Apple Team ID.
Alternatively, you may setup signing within XCode opening `iosApp/iosApp.xcworkspace` and then Alternatively, you may setup signing within XCode opening `iosApp/iosApp.xcworkspace` and then
@ -28,6 +28,8 @@ Then choose **iosApp** configuration in IDE and run it.
### Running Android application ### Running Android application
Open project in IntelliJ IDEA or Android Studio and run "android" configuration. - Get a [Google Maps API key](https://developers.google.com/maps/documentation/android-sdk/get-api-key)
- Add to local.properties: `sdk.dir=YOUR_SDK_PATH`
![Desktop](screenshots/imageviewer.png) - Create a file in the root directory named `local.properties` with a single line that looks like
this, replacing YOUR_KEY with the key from step 1: `MAPS_API_KEY=YOUR_KEY`
- Open project in IntelliJ IDEA or Android Studio and run "android" configuration.

6
experimental/examples/imageviewer/androidApp/build.gradle.kts

@ -2,6 +2,7 @@ plugins {
kotlin("multiplatform") kotlin("multiplatform")
id("com.android.application") id("com.android.application")
id("org.jetbrains.compose") id("org.jetbrains.compose")
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1"
} }
kotlin { kotlin {
@ -29,3 +30,8 @@ android {
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
} }
secrets {
defaultPropertiesFileName = "default.local.properties"
propertiesFileName = "local.properties"
}

11
experimental/examples/imageviewer/androidApp/src/androidMain/AndroidManifest.xml

@ -12,11 +12,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_imageviewer_round" android:roundIcon="@mipmap/ic_imageviewer_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> android:theme="@style/Theme.ImageViewer">
<activity
android:exported="true" <!-- Add value `MAPS_API_KEY=***` to local.properties file (More detailed in README.md) -->
android:name="example.imageviewer.MainActivity" <meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
>
<activity android:exported="true" android:name="example.imageviewer.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

5
experimental/examples/imageviewer/androidApp/src/androidMain/res/values/themes.xml

@ -0,0 +1,5 @@
<resources>
<style name="Theme.ImageViewer" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowFullscreen">true</item>
</style>
</resources>

1
experimental/examples/imageviewer/build.gradle.kts

@ -14,6 +14,5 @@ allprojects {
google() google()
mavenCentral() mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
mavenLocal()
} }
} }

2
experimental/examples/imageviewer/default.local.properties

@ -0,0 +1,2 @@
# set MAPS_API_KEY at local.properties
MAPS_API_KEY=STUB_FOR_GOOGLE_MAPS_API_KEY

5
experimental/examples/imageviewer/gradle.properties

@ -11,7 +11,6 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.native.useEmbeddableCompilerJar=true kotlin.native.useEmbeddableCompilerJar=true
# Enable kotlin/native experimental memory model # Enable kotlin/native experimental memory model
kotlin.native.binary.memoryModel=experimental kotlin.native.binary.memoryModel=experimental
kotlin.version=1.8.0 kotlin.version=1.8.10
agp.version=7.1.3 agp.version=7.1.3
compose.version=1.4.0-alpha01-dev975 compose.version=1.4.0-alpha01-dev985
ktor.version=2.2.1

BIN
experimental/examples/imageviewer/screenshots/desktop-run-configuration.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

BIN
experimental/examples/imageviewer/screenshots/imageviewer.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

0
experimental/examples/imageviewer/run-configurations.png → experimental/examples/imageviewer/screenshots/run-configurations.png

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

21
experimental/examples/imageviewer/shared/build.gradle.kts

@ -9,7 +9,6 @@ plugins {
} }
version = "1.0-SNAPSHOT" version = "1.0-SNAPSHOT"
val ktorVersion = extra["ktor.version"]
kotlin { kotlin {
android() android()
@ -26,20 +25,21 @@ kotlin {
baseName = "shared" baseName = "shared"
isStatic = true isStatic = true
} }
extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" extraSpecAttributes["resources"] =
"['src/commonMain/resources/**', 'src/iosMain/resources/**']"
} }
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
implementation(compose.material) implementation(compose.material)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation(compose.material3) implementation(compose.material3)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources) implementation(compose.components.resources)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
} }
} }
val androidMain by getting { val androidMain by getting {
@ -47,12 +47,19 @@ kotlin {
api("androidx.activity:activity-compose:1.6.1") api("androidx.activity:activity-compose:1.6.1")
api("androidx.appcompat:appcompat:1.6.1") api("androidx.appcompat:appcompat:1.6.1")
api("androidx.core:core-ktx:1.9.0") api("androidx.core:core-ktx:1.9.0")
implementation("io.ktor:ktor-client-okhttp:$ktorVersion") implementation("androidx.camera:camera-camera2:1.2.1")
implementation("androidx.camera:camera-lifecycle:1.2.1")
implementation("androidx.camera:camera-view:1.2.1")
implementation("com.google.accompanist:accompanist-permissions:0.29.2-rc")
implementation("com.google.android.gms:play-services-maps:18.1.0")
implementation("com.google.android.gms:play-services-location:21.0.1")
implementation("com.google.maps.android:maps-compose:2.11.2")
} }
} }
val iosMain by getting { val iosMain by getting {
dependencies { dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion") // TODO: update coroutines (or remove, if 1.8.0 will be presented in Compose)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-Beta")
} }
} }
val iosSimulatorArm64Main by getting { val iosSimulatorArm64Main by getting {
@ -63,8 +70,6 @@ kotlin {
val desktopMain by getting { val desktopMain by getting {
dependencies { dependencies {
implementation(compose.desktop.common) implementation(compose.desktop.common)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
} }
} }
} }

7
experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml

@ -1,2 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest package="example.imageviewer.shared"/> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="example.imageviewer.shared">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

63
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/AndroidImageStorage.kt

@ -0,0 +1,63 @@
package example.imageviewer
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.graphics.scale
import example.imageviewer.model.PictureData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
private const val maxStorableImageSizePx = 2000
private const val storableThumbnailSizePx = 200
class AndroidImageStorage(
private val pictures: SnapshotStateList<PictureData>,
private val ioScope: CoroutineScope
) : ImageStorage {
val largeImages = mutableMapOf<PictureData.Camera, ImageBitmap>()
val thumbnails = mutableMapOf<PictureData.Camera, ImageBitmap>()
override fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) {
if (image.imageBitmap.width == 0 || image.imageBitmap.height == 0) {
return
}
ioScope.launch {
val androidBitmap = image.imageBitmap.asAndroidBitmap()
val targetScale = maxOf(
maxStorableImageSizePx.toFloat() / androidBitmap.width,
maxStorableImageSizePx.toFloat() / androidBitmap.height
)
largeImages[picture] =
if (targetScale < 1.0) {
androidBitmap.scale(
width = (androidBitmap.width * targetScale).toInt(),
height = (androidBitmap.height * targetScale).toInt()
).asImageBitmap()
} else {
image.imageBitmap
}
val targetThumbnailScale = maxOf(
storableThumbnailSizePx.toFloat() / androidBitmap.width,
storableThumbnailSizePx.toFloat() / androidBitmap.height
)
thumbnails[picture] = androidBitmap.scale(
width = (androidBitmap.width * targetThumbnailScale).toInt(),
height = (androidBitmap.height * targetThumbnailScale).toInt()
).asImageBitmap()
pictures.add(0, picture)
}
}
override suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap {
return thumbnails[picture]!!
}
override suspend fun getImage(picture: PictureData.Camera): ImageBitmap {
return largeImages[picture]!!
}
}

3
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/Localization.android.kt

@ -0,0 +1,3 @@
package example.imageviewer
actual fun getCurrentLanguage(): AvailableLanguages = AvailableLanguages.EN

98
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.android.kt → experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/filter/BitmapFilter.android.kt

@ -1,37 +1,58 @@
package example.imageviewer.utils package example.imageviewer.filter
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.*
import android.graphics.Canvas
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.renderscript.Allocation import android.renderscript.Allocation
import android.renderscript.Element import android.renderscript.Element
import android.renderscript.RenderScript import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur import android.renderscript.ScriptIntrinsicBlur
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
actual fun grayScaleFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap {
return applyGrayScaleFilter(bitmap.asAndroidBitmap()).asImageBitmap()
}
fun scaleBitmapAspectRatio( actual fun pixelFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap {
bitmap: Bitmap, return applyPixelFilter(bitmap.asAndroidBitmap()).asImageBitmap()
width: Int, }
height: Int,
filter: Boolean = false
): Bitmap {
val boundW: Float = width.toFloat()
val boundH: Float = height.toFloat()
val ratioX: Float = boundW / bitmap.width actual fun blurFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap {
val ratioY: Float = boundH / bitmap.height return applyBlurFilter(bitmap.asAndroidBitmap(), context.androidContext).asImageBitmap()
val ratio: Float = if (ratioX < ratioY) ratioX else ratioY }
val resultH = (bitmap.height * ratio).toInt() actual class PlatformContext(val androidContext: Context)
val resultW = (bitmap.width * ratio).toInt()
return Bitmap.createScaledBitmap(bitmap, resultW, resultH, filter) @Composable
internal actual fun getPlatformContext(): PlatformContext = PlatformContext(LocalContext.current)
private fun applyBlurFilter(bitmap: Bitmap, context: Context): Bitmap {
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val renderScript: RenderScript = RenderScript.create(context)
val tmpIn: Allocation = Allocation.createFromBitmap(renderScript, bitmap)
val tmpOut: Allocation = Allocation.createFromBitmap(renderScript, result)
val theIntrinsic: ScriptIntrinsicBlur =
ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
theIntrinsic.setRadius(15f)
theIntrinsic.setInput(tmpIn)
theIntrinsic.forEach(tmpOut)
tmpOut.copyTo(result)
return result
} }
fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap { private fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true) val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
@ -48,34 +69,33 @@ fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
return result return result
} }
fun applyPixelFilter(bitmap: Bitmap): Bitmap { private fun applyPixelFilter(bitmap: Bitmap): Bitmap {
var result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true) var result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val w: Int = bitmap.width val w: Int = bitmap.width
val h: Int = bitmap.height val h: Int = bitmap.height
result = scaleBitmapAspectRatio(result, w / 4, h / 4) result = scaleBitmapAspectRatio(result, w / 12, h / 12)
result = scaleBitmapAspectRatio(result, w, h) result = scaleBitmapAspectRatio(result, w, h)
return result return result
} }
fun applyBlurFilter(bitmap: Bitmap, context: Context): Bitmap { private fun scaleBitmapAspectRatio(
bitmap: Bitmap,
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true) width: Int,
height: Int,
val renderScript: RenderScript = RenderScript.create(context) filter: Boolean = false
): Bitmap {
val tmpIn: Allocation = Allocation.createFromBitmap(renderScript, bitmap) val boundW: Float = width.toFloat()
val tmpOut: Allocation = Allocation.createFromBitmap(renderScript, result) val boundH: Float = height.toFloat()
val theIntrinsic: ScriptIntrinsicBlur =
ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
theIntrinsic.setRadius(3f) val ratioX: Float = boundW / bitmap.width
theIntrinsic.setInput(tmpIn) val ratioY: Float = boundH / bitmap.height
theIntrinsic.forEach(tmpOut) val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
tmpOut.copyTo(result) val resultH = (bitmap.height * ratio).toInt()
val resultW = (bitmap.width * ratio).toInt()
return result return Bitmap.createScaledBitmap(bitmap, resultW, resultH, filter)
} }

14
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt

@ -1,14 +0,0 @@
package example.imageviewer.model.filtration
import android.content.Context
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyBlurFilter
class BlurFilter(private val context: Context) : BitmapFilter {
override fun apply(bitmap: ImageBitmap): ImageBitmap =
applyBlurFilter(bitmap.asAndroidBitmap(), context).asImageBitmap()
}

13
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt

@ -1,13 +0,0 @@
package example.imageviewer.model.filtration
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyGrayScaleFilter
class GrayScaleFilter : BitmapFilter {
override fun apply(bitmap: ImageBitmap): ImageBitmap =
applyGrayScaleFilter(bitmap.asAndroidBitmap()).asImageBitmap()
}

12
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt

@ -1,12 +0,0 @@
package example.imageviewer.model.filtration
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyPixelFilter
class PixelFilter : BitmapFilter {
override fun apply(bitmap: ImageBitmap): ImageBitmap =
applyPixelFilter(bitmap.asAndroidBitmap()).asImageBitmap()
}

13
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt

@ -3,5 +3,18 @@ package example.imageviewer
import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import kotlinx.coroutines.Dispatchers
import java.util.*
actual fun Modifier.notchPadding(): Modifier = displayCutoutPadding().statusBarsPadding() actual fun Modifier.notchPadding(): Modifier = displayCutoutPadding().statusBarsPadding()
class AndroidStorableImage(
val imageBitmap: ImageBitmap
)
actual typealias PlatformStorableImage = AndroidStorableImage
actual fun createUUID(): String = UUID.randomUUID().toString()
actual val ioDispatcher = Dispatchers.IO

7
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/IOScope.android.kt

@ -1,7 +0,0 @@
package example.imageviewer.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.Dispatchers
actual val ioDispatcher = Dispatchers.IO

150
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/CameraView.android.kt

@ -1,21 +1,155 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.foundation.background import android.annotation.SuppressLint
import android.location.Location
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OnImageCapturedCallback
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.location.CurrentLocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.tasks.Task
import example.imageviewer.*
import example.imageviewer.model.GpsPosition
import example.imageviewer.model.PictureData
import example.imageviewer.model.createCameraPictureData
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
private val executor = Executors.newSingleThreadExecutor()
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
internal actual fun CameraView(modifier: Modifier) { internal actual fun CameraView(
Box(Modifier.fillMaxSize().background(Color.Black)) { modifier: Modifier,
Text( onCapture: (picture: PictureData.Camera, image: PlatformStorableImage) -> Unit
text = "Camera is not available on Android for now.", ) {
color = Color.White, val cameraPermissionState = rememberMultiplePermissionsState(
modifier = Modifier.align(Alignment.Center) listOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION,
)
) )
if (cameraPermissionState.allPermissionsGranted) {
CameraWithGrantedPermission(modifier, onCapture)
} else {
LaunchedEffect(Unit) {
cameraPermissionState.launchMultiplePermissionRequest()
}
}
} }
@SuppressLint("MissingPermission")
@Composable
private fun CameraWithGrantedPermission(
modifier: Modifier,
onCapture: (picture: PictureData.Camera, image: PlatformStorableImage) -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val preview = Preview.Builder().build()
val previewView = remember { PreviewView(context) }
val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() }
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
LaunchedEffect(Unit) {
val cameraProvider = suspendCoroutine<ProcessCameraProvider> { continuation ->
ProcessCameraProvider.getInstance(context).also { cameraProvider ->
cameraProvider.addListener({
continuation.resume(cameraProvider.get())
}, executor)
}
}
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture
)
preview.setSurfaceProvider(previewView.surfaceProvider)
}
val nameAndDescription = createNewPhotoNameAndDescription()
var capturePhotoStarted by remember { mutableStateOf(false) }
Box(contentAlignment = Alignment.BottomCenter, modifier = modifier) {
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
Button(
enabled = !capturePhotoStarted,
onClick = {
capturePhotoStarted = true
imageCapture.takePicture(executor, object : OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val byteArray: ByteArray = image.planes[0].buffer.toByteArray()
val imageBitmap = byteArray.toImageBitmap()
image.close()
fun sendToStorage(gpsPosition: GpsPosition) {
onCapture(
createCameraPictureData(
name = nameAndDescription.name,
description = nameAndDescription.description,
gps = gpsPosition
),
AndroidStorableImage(imageBitmap)
)
capturePhotoStarted = false
}
val lastLocation: Task<Location> =
LocationServices.getFusedLocationProviderClient(context)
.getCurrentLocation(
CurrentLocationRequest.Builder().build(),
null
)
lastLocation.addOnSuccessListener {
sendToStorage(GpsPosition(it.latitude, it.longitude))
}
lastLocation.addOnFailureListener {
sendToStorage(GpsPosition(0.0, 0.0))
}
}
})
}) {
Text(LocalLocalization.current.takePhoto, color = Color.White)
}
if (capturePhotoStarted) {
CircularProgressIndicator(
modifier = Modifier.size(80.dp).align(Alignment.Center),
color = Color.White.copy(alpha = 0.7f),
strokeWidth = 8.dp,
)
}
}
}
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
} }

53
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt

@ -5,29 +5,10 @@ import android.widget.Toast
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import example.imageviewer.Dependencies import example.imageviewer.*
import example.imageviewer.ImageViewerCommon
import example.imageviewer.Localization
import example.imageviewer.Notification
import example.imageviewer.PopupNotification
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
import example.imageviewer.model.ContentRepository
import example.imageviewer.model.adapter
import example.imageviewer.model.createNetworkRepository
import example.imageviewer.model.filtration.BlurFilter
import example.imageviewer.model.filtration.GrayScaleFilter
import example.imageviewer.model.filtration.PixelFilter
import example.imageviewer.shared.R
import example.imageviewer.style.ImageViewerTheme import example.imageviewer.style.ImageViewerTheme
import example.imageviewer.toImageBitmap
import example.imageviewer.utils.ioDispatcher
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@Composable @Composable
fun ImageViewerAndroid() { fun ImageViewerAndroid() {
@ -39,39 +20,11 @@ fun ImageViewerAndroid() {
} }
} }
private fun getDependencies(context: Context, ioScope: CoroutineScope) = object : Dependencies { private fun getDependencies(context: Context, ioScope: CoroutineScope) = object : Dependencies() {
override val httpClient: HttpClient = HttpClient(OkHttp)
override val ioScope: CoroutineScope = ioScope
override fun getFilter(type: FilterType): BitmapFilter =
when (type) {
FilterType.GrayScale -> GrayScaleFilter()
FilterType.Pixel -> PixelFilter()
FilterType.Blur -> BlurFilter(context)
}
override val localization: Localization = object : Localization {
override val back get() = context.getString(R.string.back)
override val appName get() = context.getString(R.string.app_name)
override val loading get() = context.getString(R.string.loading)
override val repoInvalid get() = context.getString(R.string.repo_invalid)
override val repoEmpty get() = context.getString(R.string.repo_empty)
override val noInternet get() = context.getString(R.string.no_internet)
override val loadImageUnavailable get() = context.getString(R.string.load_image_unavailable)
override val lastImage get() = context.getString(R.string.last_image)
override val firstImage get() = context.getString(R.string.first_image)
override val picture get() = context.getString(R.string.picture)
override val size get() = context.getString(R.string.size)
override val pixels get() = context.getString(R.string.pixels)
override val refreshUnavailable get() = context.getString(R.string.refresh_unavailable)
}
override val imageRepository: ContentRepository<ImageBitmap> =
createNetworkRepository(httpClient)
.adapter { it.toImageBitmap() }
override val notification: Notification = object : PopupNotification(localization) { override val notification: Notification = object : PopupNotification(localization) {
override fun showPopUpMessage(text: String) { override fun showPopUpMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show() Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
} }
} }
override val imageStorage: ImageStorage = AndroidImageStorage(pictures, ioScope)
} }

24
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/LocationVisualizer.android.kt

@ -1,19 +1,21 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import com.google.android.gms.maps.model.CameraPosition
import org.jetbrains.compose.resources.ExperimentalResourceApi import com.google.android.gms.maps.model.LatLng
import org.jetbrains.compose.resources.painterResource import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.rememberCameraPositionState
import example.imageviewer.model.GpsPosition
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal actual fun LocationVisualizer(modifier: Modifier) { internal actual fun LocationVisualizer(modifier: Modifier, gps: GpsPosition, title: String) {
Image( val currentLocation = LatLng(gps.latitude, gps.longitude)
painter = painterResource("dummy_map.png"), val cameraPositionState = rememberCameraPositionState {
contentDescription = "Map", position = CameraPosition.fromLatLngZoom(currentLocation, 10f)
contentScale = ContentScale.Crop, }
modifier = modifier GoogleMap(
modifier = modifier,
cameraPositionState = cameraPositionState
) )
} }

6
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/Tooltip.android.kt

@ -1,9 +1,13 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable @Composable
internal actual fun Tooltip(text: String, content: @Composable () -> Unit) { internal actual fun Tooltip(
text: String,
content: @Composable () -> Unit
) {
// No Tooltip for Android // No Tooltip for Android
content() content()
} }

11
experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ZoomControllerView.android.kt

@ -0,0 +1,11 @@
package example.imageviewer.view
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import example.imageviewer.model.ScalableState
import androidx.compose.ui.Modifier
@Composable
internal actual fun ZoomControllerView(modifier: Modifier, scalableState: ScalableState) {
// No need for additional ZoomControllerView for Android
}

13
experimental/examples/imageviewer/shared/src/androidMain/res/values-de/strings.xml

@ -1,17 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">ImageViewer</string> <string name="app_name">ImageViewer</string>
<string name="loading">Bilder werden geladen...</string>
<string name="repo_empty">Bildverzeichnis ist leer.</string>
<string name="no_internet">Kein Internetzugriff.</string>
<string name="repo_invalid">Bildverzeichnis beschädigt oder leer.</string>
<string name="refresh_unavailable">Kann Bilder nicht aktualisieren.</string>
<string name="load_image_unavailable">Kann volles Bild nicht laden.</string>
<string name="last_image">Dies ist das letzte Bild.</string>
<string name="first_image">Dies ist das erste Bild.</string>
<string name="picture">Bild:</string>
<string name="size">Abmessungen:</string>
<string name="pixels">Pixel.</string>
<string name="back">Zurück</string>
<string name="refresh">Aktualisieren</string>
</resources> </resources>

13
experimental/examples/imageviewer/shared/src/androidMain/res/values/strings.xml

@ -1,16 +1,3 @@
<resources> <resources>
<string name="app_name">My Memories</string> <string name="app_name">My Memories</string>
<string name="loading">Loading images...</string>
<string name="repo_empty">Repository is empty.</string>
<string name="no_internet">No internet access.</string>
<string name="repo_invalid">List of images in current repository is invalid or empty.</string>
<string name="refresh_unavailable">Cannot refresh images.</string>
<string name="load_image_unavailable">Cannot load full size image.</string>
<string name="last_image">This is last image.</string>
<string name="first_image">This is first image.</string>
<string name="picture">Picture:</string>
<string name="size">Size:</string>
<string name="pixels">pixels.</string>
<string name="back">back</string>
<string name="refresh">Refresh</string>
</resources> </resources>

139
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt

@ -1,80 +1,99 @@
package example.imageviewer package example.imageviewer
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.core.BitmapFilter import example.imageviewer.model.*
import example.imageviewer.core.FilterType import kotlinx.coroutines.flow.Flow
import example.imageviewer.model.ContentRepository import kotlinx.coroutines.flow.emptyFlow
import example.imageviewer.model.Picture import org.jetbrains.compose.resources.ExperimentalResourceApi
import example.imageviewer.model.name import org.jetbrains.compose.resources.resource
import io.ktor.client.*
import kotlinx.coroutines.CoroutineScope @OptIn(ExperimentalResourceApi::class)
import kotlinx.serialization.json.Json abstract class Dependencies {
abstract val notification: Notification
interface Dependencies { abstract val imageStorage: ImageStorage
val httpClient: HttpClient val pictures: SnapshotStateList<PictureData> = mutableStateListOf(*resourcePictures)
val ioScope: CoroutineScope open val externalEvents: Flow<ExternalImageViewerEvent> = emptyFlow()
fun getFilter(type: FilterType): BitmapFilter val localization: Localization = getCurrentLocalization()
val localization: Localization val imageProvider: ImageProvider = object : ImageProvider {
val imageRepository: ContentRepository<ImageBitmap> override suspend fun getImage(picture: PictureData): ImageBitmap = when (picture) {
val notification: Notification is PictureData.Resource -> {
val json: Json get() = jsonReader resource(picture.resource).readBytes().toImageBitmap()
}
is PictureData.Camera -> {
imageStorage.getImage(picture)
}
}
override suspend fun getThumbnail(picture: PictureData): ImageBitmap = when (picture) {
is PictureData.Resource -> {
resource(picture.thumbnailResource).readBytes().toImageBitmap()
}
is PictureData.Camera -> {
imageStorage.getThumbnail(picture)
}
}
}
} }
interface Notification { interface Notification {
fun notifyInvalidRepo() fun notifyImageData(picture: PictureData)
fun notifyRepoIsEmpty()
fun notifyNoInternet()
fun notifyLoadImageUnavailable()
fun notifyLastImage()
fun notifyFirstImage()
fun notifyImageData(picture: Picture)
fun notifyRefreshUnavailable()
} }
abstract class PopupNotification(private val localization: Localization) : Notification { abstract class PopupNotification(private val localization: Localization) : Notification {
abstract fun showPopUpMessage(text: String) abstract fun showPopUpMessage(text: String)
override fun notifyImageData(picture: PictureData) = showPopUpMessage(
override fun notifyInvalidRepo() = showPopUpMessage(localization.repoInvalid)
override fun notifyRepoIsEmpty() = showPopUpMessage(localization.repoEmpty)
override fun notifyNoInternet() = showPopUpMessage(localization.noInternet)
override fun notifyLoadImageUnavailable() =
showPopUpMessage(
"""
${localization.noInternet}
${localization.loadImageUnavailable}
""".trimIndent()
)
override fun notifyLastImage() = showPopUpMessage(localization.lastImage)
override fun notifyFirstImage() = showPopUpMessage(localization.firstImage)
override fun notifyImageData(picture: Picture) = showPopUpMessage(
"${localization.picture} ${picture.name}" "${localization.picture} ${picture.name}"
) )
override fun notifyRefreshUnavailable() = showPopUpMessage(
"""
${localization.noInternet}
${localization.refreshUnavailable}
""".trimIndent()
)
} }
interface Localization { interface Localization {
val back: String
val appName: String val appName: String
val loading: String val back: String
val repoInvalid: String
val repoEmpty: String
val noInternet: String
val loadImageUnavailable: String
val lastImage: String
val firstImage: String
val picture: String val picture: String
val size: String val takePhoto: String
val pixels: String val addPhoto: String
val refreshUnavailable: String val kotlinConfName: String
val kotlinConfDescription: String
val newPhotoName: String
val newPhotoDescription: String
}
interface ImageProvider {
suspend fun getImage(picture: PictureData): ImageBitmap
suspend fun getThumbnail(picture: PictureData): ImageBitmap
}
interface ImageStorage {
fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage)
suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap
suspend fun getImage(picture: PictureData.Camera): ImageBitmap
}
internal val LocalLocalization = staticCompositionLocalOf<Localization> {
noLocalProvidedFor("LocalLocalization")
}
internal val LocalNotification = staticCompositionLocalOf<Notification> {
noLocalProvidedFor("LocalNotification")
}
internal val LocalImageProvider = staticCompositionLocalOf<ImageProvider> {
noLocalProvidedFor("LocalImageProvider")
}
internal val LocalImageStorage = staticCompositionLocalOf<ImageStorage> {
noLocalProvidedFor("LocalImageStorage")
}
internal val LocalInternalEvents = staticCompositionLocalOf<Flow<ExternalImageViewerEvent>> {
noLocalProvidedFor("LocalInternalEvents")
} }
private val jsonReader: Json = Json { private fun noLocalProvidedFor(name: String): Nothing {
ignoreUnknownKeys = true error("CompositionLocal $name not present")
} }

97
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt

@ -1,47 +1,52 @@
package example.imageviewer package example.imageviewer
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.*
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.with
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import example.imageviewer.model.CameraPage import example.imageviewer.model.*
import example.imageviewer.model.FullScreenPage import example.imageviewer.view.*
import example.imageviewer.model.GalleryPage
import example.imageviewer.model.MemoryPage
import example.imageviewer.model.Page
import example.imageviewer.model.PhotoGallery
import example.imageviewer.model.bigUrl
import example.imageviewer.view.CameraScreen
import example.imageviewer.view.FullscreenImage
import example.imageviewer.view.GalleryScreen
import example.imageviewer.view.MemoryScreen
import example.imageviewer.view.NavigationStack
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
enum class ExternalImageViewerEvent { enum class ExternalImageViewerEvent {
Foward, Foward,
Back Back,
Escape,
} }
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
internal fun ImageViewerCommon( internal fun ImageViewerCommon(
dependencies: Dependencies, dependencies: Dependencies
externalEvents: Flow<ExternalImageViewerEvent> = emptyFlow()
) { ) {
val photoGallery = remember { PhotoGallery(dependencies) } CompositionLocalProvider(
val rootGalleryPage = GalleryPage(photoGallery, externalEvents) LocalLocalization provides dependencies.localization,
val navigationStack = remember { NavigationStack<Page>(rootGalleryPage) } LocalNotification provides dependencies.notification,
LocalImageProvider provides dependencies.imageProvider,
LocalImageStorage provides dependencies.imageStorage,
LocalInternalEvents provides dependencies.externalEvents
) {
ImageViewerWithProvidedDependencies(dependencies.pictures)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun ImageViewerWithProvidedDependencies(
pictures: SnapshotStateList<PictureData>
) {
val selectedPictureIndex: MutableState<Int> = mutableStateOf(0)
val navigationStack = remember { NavigationStack<Page>(GalleryPage()) }
val externalEvents = LocalInternalEvents.current
LaunchedEffect(Unit) {
externalEvents.collect {
if (it == ExternalImageViewerEvent.Escape) {
navigationStack.back()
}
}
}
Surface(modifier = Modifier.fillMaxSize()) { Surface(modifier = Modifier.fillMaxSize()) {
AnimatedContent(targetState = navigationStack.lastWithIndex(), transitionSpec = { AnimatedContent(targetState = navigationStack.lastWithIndex(), transitionSpec = {
@ -60,24 +65,19 @@ internal fun ImageViewerCommon(
when (page) { when (page) {
is GalleryPage -> { is GalleryPage -> {
GalleryScreen( GalleryScreen(
page, pictures = pictures,
photoGallery, selectedPictureIndex = selectedPictureIndex,
dependencies,
onClickPreviewPicture = { previewPictureId -> onClickPreviewPicture = { previewPictureId ->
navigationStack.push(MemoryPage(previewPictureId)) navigationStack.push(MemoryPage(previewPictureId))
}, }
onMakeNewMemory = { ) {
navigationStack.push(CameraPage()) navigationStack.push(CameraPage())
}) }
} }
is FullScreenPage -> { is FullScreenPage -> {
FullscreenImage( FullscreenImageScreen(
galleryId = page.galleryId, picture = page.picture,
gallery = photoGallery,
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
getFilter = { dependencies.getFilter(it) },
localization = dependencies.localization,
back = { back = {
navigationStack.back() navigationStack.back()
} }
@ -86,10 +86,8 @@ internal fun ImageViewerCommon(
is MemoryPage -> { is MemoryPage -> {
MemoryScreen( MemoryScreen(
pictures = pictures,
memoryPage = page, memoryPage = page,
photoGallery = photoGallery,
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
localization = dependencies.localization,
onSelectRelatedMemory = { galleryId -> onSelectRelatedMemory = { galleryId ->
navigationStack.push(MemoryPage(galleryId)) navigationStack.push(MemoryPage(galleryId))
}, },
@ -98,13 +96,18 @@ internal fun ImageViewerCommon(
}, },
onHeaderClick = { galleryId -> onHeaderClick = { galleryId ->
navigationStack.push(FullScreenPage(galleryId)) navigationStack.push(FullScreenPage(galleryId))
}) },
)
} }
is CameraPage -> { is CameraPage -> {
CameraScreen( CameraScreen(
localization = dependencies.localization, onBack = { resetSelectedPicture ->
onBack = { navigationStack.back() } if (resetSelectedPicture) {
selectedPictureIndex.value = 0
}
navigationStack.back()
},
) )
} }
} }

43
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Localization.kt

@ -0,0 +1,43 @@
package example.imageviewer
enum class AvailableLanguages {
DE,
EN;
}
expect fun getCurrentLanguage(): AvailableLanguages
private object EnglishLocalization : Localization {
override val appName = "My Memories"
override val picture = "Picture:"
override val back = "Back"
override val takePhoto = "Take a photo 📸"
override val addPhoto = "Add a photo"
override val kotlinConfName = "KotlinConf 2023 🎉"
override val kotlinConfDescription = """
This photo was taken during KotlinConf 2023! 🎊
Have a fun with Kotlin and Compose Multiplatform 🥳
""".trimIndent()
override val newPhotoName = "New Memory"
override val newPhotoDescription = "May amazing things happen to you! 🙂"
}
private object DeutschLocalization : Localization {
override val appName = "Meine Erinnerungen"
override val picture = "Bild:"
override val back = "Zurück"
override val takePhoto = "Mach ein Foto 📸"
override val addPhoto = "Füge ein Foto hinzu"
override val kotlinConfName = "KotlinConf 2023 🎉"
override val kotlinConfDescription = """
This photo was taken during KotlinConf 2023! 🎊
Have a fun with Kotlin and Compose Multiplatform 🥳
""".trimIndent()
override val newPhotoName = "New Memory"
override val newPhotoDescription = "May amazing things happen to you! 🙂"
}
fun getCurrentLocalization() = when (getCurrentLanguage()) {
AvailableLanguages.EN -> EnglishLocalization
AvailableLanguages.DE -> DeutschLocalization
}

33
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/NameAndDescription.common.kt

@ -0,0 +1,33 @@
package example.imageviewer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import kotlinx.datetime.*
class NameAndDescription(
val name: String,
val description: String,
)
@Composable
internal fun createNewPhotoNameAndDescription(): NameAndDescription {
val localization = LocalLocalization.current
return remember {
Clock.System.now().toLocalDateTime(TimeZone.UTC)
val kotlinConfEndTime =
LocalDateTime(2023, Month.APRIL, 14, hour = 23, minute = 59).toInstant(TimeZone.UTC)
if (Clock.System.now() < kotlinConfEndTime) {
NameAndDescription(
localization.kotlinConfName,
localization.kotlinConfDescription
)
} else {
NameAndDescription(
localization.newPhotoName,
localization.newPhotoDescription
)
}
}
}

213
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ResourcePictures.kt

@ -0,0 +1,213 @@
package example.imageviewer
import example.imageviewer.model.GpsPosition
import example.imageviewer.model.PictureData
val resourcePictures = arrayOf(
PictureData.Resource(
resource = "1.jpg",
thumbnailResource = "1-thumbnail.jpg",
name = "Mountain K2",
description = """
K2, at 8,611 meters above sea level, is the second-highest mountain on Earth, after Mount Everest.
The name K2 is derived from notation used by the Great Trigonometrical Survey of British India. Thomas Montgomerie made the first survey of the Karakoram from Mount Haramukh, some 210 km to the south, and sketched the two most prominent peaks, labelling them K1 and K2, where the K stands for Karakoram.
Also the new Kotlin compiler is code-named "K2".
""".trimIndent(),
dateString = "20 Mar.",
gps = GpsPosition(35.8825, 76.513333)
),
PictureData.Resource(
resource = "2.jpg",
thumbnailResource = "2-thumbnail.jpg",
name = "Kina The Calico",
description = """
This cute kitty 🐱 loves one thing above all: soups and sauces!
A true connoisseur of all liquid meals, you'll frequently find her lounging by a sunny window and surveying the neighbourhood.
But only until it's dinner time again, of course! 🍜
""".trimIndent(),
dateString = "3 Feb.",
gps = GpsPosition(48.138018, 11.5737048)
),
PictureData.Resource(
resource = "3.jpg",
thumbnailResource = "3-thumbnail.jpg",
name = "Blue City",
description = """
Is a city in northwest Morocco.
It is the chief town of the province of the same name and is noted for its buildings in shades of blue, for which it is nicknamed the "Blue City".
Chefchaouen is situated just inland from Tangier and Tétouan.
""".trimIndent(),
dateString = "12 May.",
gps = GpsPosition(35.171389, -5.269722)
),
PictureData.Resource(
resource = "4.jpg",
thumbnailResource = "4-thumbnail.jpg",
name = "Tokyo Skytree",
description = """
Tokyo Skytree is a broadcasting and observation tower in Sumida, Tokyo.
It became the tallest structure in Japan in 2010 and reached its full height of 634 meters in March 2011.
""".trimIndent(),
dateString = "22 Mar.",
gps = GpsPosition(35.7101, 139.8107)
),
PictureData.Resource(
resource = "5.jpg",
thumbnailResource = "5-thumbnail.jpg",
name = "Taranaki",
description = """
Mount Taranaki is a dormant stratovolcano in the Taranaki region on the west coast of New Zealand's North Island.
At 2,518 metres, it is the second highest mountain in the North Island, after Mount Ruapehu.
It has a secondary cone, Fanthams Peak, 1,966 metres, on its south side.
""".trimIndent(),
dateString = "3 May.",
gps = GpsPosition(-39.296389, 174.064722)
),
PictureData.Resource(
resource = "6.jpg",
thumbnailResource = "6-thumbnail.jpg",
name = "Auckland SkyCity",
description = """
SkyCity Casino History
This kiwi casino is a part of the Sky Tower, a giant resort that was completed in 1997.
There were many New Zealand casinos at that time and this was in fact the second one ever built in the whole country
""".trimIndent(),
dateString = "15 Aug.",
gps = GpsPosition(-36.846589, 174.760871)
),
PictureData.Resource(
resource = "7.jpg",
thumbnailResource = "7-thumbnail.jpg",
name = "Berliner Fernsehturm",
description = """
At 368 meters, the Berlin television tower is the tallest building in Germany and the fifth tallest television tower in Europe.
The television tower is located in the park at the television tower in Berlin's Mitte district.
When it was completed in 1969, it was the second highest television tower in the world and, with over a million visitors a year, is one of the ten most popular sights in Germany.
""".trimIndent(),
dateString = "24 Sep.",
gps = GpsPosition(52.520833, 13.409444)
),
PictureData.Resource(
resource = "8.jpg",
thumbnailResource = "8-thumbnail.jpg",
name = "Hoggar Mountains",
description = """
The Hoggar Mountains are a highland region in the central Sahara in southern Algeria, along the Tropic of Cancer.
The mountains cover an area of approximately 550,000 km.
""".trimIndent(),
dateString = "13 Jul.",
gps = GpsPosition(22.133333, 6.166667)
),
PictureData.Resource(
resource = "9.jpg",
thumbnailResource = "9-thumbnail.jpg",
name = "Nakhal Fort",
description = """
Nakhal Fort is a large fortification in Al Batinah Region of Oman.
It is named after the Wilayah of Nakhal.
The fort houses a museum, operated by the Ministry of Tourism, which has exhibits of historic guns, and the fort also hosts a weekly goat market.
""".trimIndent(),
dateString = "20 Aug.",
gps = GpsPosition(23.395, 57.829)
),
PictureData.Resource(
resource = "10.jpg",
thumbnailResource = "10-thumbnail.jpg",
name = "Mountain Ararat",
description = """
Mount Ararat is a snow-capped and dormant compound volcano in the extreme east of Turkey.
It consists of two major volcanic cones: Greater Ararat and Little Ararat.
Greater Ararat is the highest peak in Turkey and the Armenian Highland with an elevation of 5,137 m. 🏔
""".trimIndent(),
dateString = "12 Apr.",
gps = GpsPosition(40.169339, 44.488434)
),
PictureData.Resource(
resource = "11.jpg",
thumbnailResource = "11-thumbnail.jpg",
name = "Cabo da Roca",
description = """
The view on Cabo da Roca.
Cabo da Roca or Cape Roca is a cape which forms the westernmost point of the Sintra Mountain Range, of mainland Portugal, of continental Europe, and of the Eurasian landmass.
""".trimIndent(),
dateString = "3 Jun.",
gps = GpsPosition(38.789283172, 9.4909725957)
),
PictureData.Resource(
resource = "12.jpg",
thumbnailResource = "12-thumbnail.jpg",
name = "Surprised Whiskers 🐱",
description = """
Surprised Whiskers: A Furry Tale.
The photo captures Whiskers' adorably astonished expression as something unexpected catches his eye.
The scene masterfully highlights the cat's vibrant fur and mesmerizing gaze, drawing the viewer into the furry tale unfolding before them.
""".trimIndent(),
dateString = "10 Apr.",
gps = GpsPosition(52.3560485, 4.9085645)
),
PictureData.Resource(
resource = "13.jpg",
thumbnailResource = "13-thumbnail.jpg",
name = "Software Engineering Donut",
description = """
Munich
During our Introduction to Software Engineering Lectures, the professor would hand out little prizes to students who would solve coding challenges quickly.
I solved a challenge about software design patterns as the first student of over 800, and got rewarded with this donut in the style of a cookie monster!
It was really delicious! 😋
""".trimIndent(),
dateString = "21 Dec.",
gps = GpsPosition(48.1764708, 11.4580367)
),
PictureData.Resource(
resource = "14.jpg",
thumbnailResource = "14-thumbnail.jpg",
name = "Seligman Police Car.",
description = """
Seligman, USA
I really enjoy old cars, and historic police cars are no exception! 🚓
I stumbled across this one during a roadtrip across the united states in Seligman, a 500-soul town in the middle of the Arizona countryside.
The extended hood and rounded forms of this car are just delightful to me. Plus, it has the option to go wee-ooo-wee-ooo! 🚨
""".trimIndent(),
dateString = "14 Jul.",
gps = GpsPosition(35.3259364, -112.8553165)
),
PictureData.Resource(
resource = "15.jpg",
thumbnailResource = "15-thumbnail.jpg",
name = "Good Luck Charms",
description = """
Munich
I decided I'd make my office a little bit more homely with trinkets from Tokyo and Las Vegas! 🐱🎰
The cat is a variant of a Daruma doll, and is regarded more as a talisman of good luck, which you can never have enough of!
The dice come from a casino in Las Vegas that shut down, and in traditional fashion, I decided they should show the numbers four and three, since that gives you the lucky number seven.
These figures are still sitting on my desk, and it makes me really happy to look at them! 👀
""".trimIndent(),
dateString = "28 Mar.",
gps = GpsPosition(48.1458602, 11.5053059)
),
PictureData.Resource(
resource = "16.jpg",
thumbnailResource = "16-thumbnail.jpg",
name = "Pong Restaurant",
description = """
Stockholm, Sweden
This little restaurant caught my eye because of the color scheme they use! 🦩
The neon lights illuminating the dark streets of stockholm was a sight to behold, and the fact that only the first and last letter weren't lit up seems almost intentional.
Also, the dumplings served at that place was delightful! 🥟
""".trimIndent(),
dateString = "25 Jul.",
gps = GpsPosition(59.3364318, 18.0587228)
),
PictureData.Resource(
resource = "17.jpg",
thumbnailResource = "17-thumbnail.jpg",
name = "Loki",
description = """
Meet Loki, my black cat - a furry feline with big, beautiful eyes and an arrogant attitude.
Just look at that judging gaze - it's clear that Loki demands more food and is disappointed with my efforts so far!
Despite his round belly and chubby cheeks, Loki exudes a regal and confident aura that's hard to resist.
""".trimIndent(),
dateString = "4 Mar.",
gps = GpsPosition(66.476857055, 25.759675853)
),
)

7
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/BitmapFilter.kt

@ -1,7 +0,0 @@
package example.imageviewer.core
import androidx.compose.ui.graphics.ImageBitmap
interface BitmapFilter {
fun apply(bitmap: ImageBitmap): ImageBitmap
}

20
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/filter/BitmapFilter.common.kt

@ -0,0 +1,20 @@
package example.imageviewer.filter
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
fun getFilter(type: FilterType): (ImageBitmap, PlatformContext) -> ImageBitmap =
when (type) {
FilterType.GrayScale -> ::grayScaleFilter
FilterType.Pixel -> ::pixelFilter
FilterType.Blur -> ::blurFilter
}
expect fun grayScaleFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap
expect fun pixelFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap
expect fun blurFilter(bitmap: ImageBitmap, context: PlatformContext): ImageBitmap
expect class PlatformContext
@Composable
internal expect fun getPlatformContext(): PlatformContext

2
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt → experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/filter/FilterType.kt

@ -1,4 +1,4 @@
package example.imageviewer.core package example.imageviewer.filter
enum class FilterType { enum class FilterType {
GrayScale, Pixel, Blur GrayScale, Pixel, Blur

2
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Config.kt

@ -1,7 +1,5 @@
package example.imageviewer.model package example.imageviewer.model
const val BASE_URL = "https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo"
const val PICTURES_DATA_URL = "$BASE_URL/pictures.json"
const val MAX_SCALE = 5f const val MAX_SCALE = 5f
const val MIN_SCALE = 1f const val MIN_SCALE = 1f
const val TOAST_DURATION = 3000L const val TOAST_DURATION = 3000L

23
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ContentRepository.kt

@ -1,23 +0,0 @@
package example.imageviewer.model
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
interface ContentRepository<T> {
suspend fun loadContent(url: String): T
}
fun createNetworkRepository(ktorClient: HttpClient) = object : ContentRepository<ByteArray> {
override suspend fun loadContent(url: String): ByteArray =
ktorClient.get(urlString = url).readBytes()
}
fun <A, B> ContentRepository<A>.adapter(transform: (A) -> B): ContentRepository<B> {
val origin = this
return object : ContentRepository<B> {
override suspend fun loadContent(url: String): B {
return transform(origin.loadContent(url))
}
}
}

61
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt

@ -1,59 +1,8 @@
package example.imageviewer.model package example.imageviewer.model
import androidx.compose.foundation.ScrollState sealed interface Page
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import example.imageviewer.ExternalImageViewerEvent
import example.imageviewer.view.GalleryStyle
import kotlinx.coroutines.flow.Flow
sealed class Page class MemoryPage(val picture: PictureData) : Page
class CameraPage : Page
class MemoryPage(val galleryId: GalleryId) : Page() { class FullScreenPage(val picture: PictureData) : Page
val scrollState = ScrollState(0) class GalleryPage : Page
}
class CameraPage : Page()
class FullScreenPage(val galleryId: GalleryId) : Page()
class GalleryPage(
val photoGallery: PhotoGallery,
val externalEvents: Flow<ExternalImageViewerEvent>
) : Page() {
var galleryStyle by mutableStateOf(GalleryStyle.SQUARES)
fun toggleGalleryStyle() {
galleryStyle =
if (galleryStyle == GalleryStyle.SQUARES) GalleryStyle.LIST else GalleryStyle.SQUARES
}
var currentPictureIndex by mutableStateOf(0)
val picture get(): Picture? = photoGallery.galleryStateFlow.value.getOrNull(currentPictureIndex)?.picture
val galleryEntry: GalleryEntryWithMetadata?
get() = photoGallery.galleryStateFlow.value.getOrNull(
currentPictureIndex
)
val pictureId
get(): GalleryId? = photoGallery.galleryStateFlow.value.getOrNull(
currentPictureIndex
)?.id
fun nextImage() {
currentPictureIndex =
(currentPictureIndex + 1).mod(photoGallery.galleryStateFlow.value.lastIndex)
}
fun previousImage() {
currentPictureIndex =
(currentPictureIndex - 1).mod(photoGallery.galleryStateFlow.value.lastIndex)
}
fun selectPicture(galleryId: GalleryId) {
currentPictureIndex = photoGallery.galleryStateFlow.value.indexOfFirst { it.id == galleryId }
}
}

67
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PhotoGallery.kt

@ -1,67 +0,0 @@
package example.imageviewer.model
import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.Dependencies
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.ListSerializer
import kotlin.jvm.JvmInline
@JvmInline
value class GalleryId(val l: Long)
data class GalleryEntryWithMetadata(
val id: GalleryId,
val picture: Picture,
val thumbnail: ImageBitmap,
)
class PhotoGallery(val deps: Dependencies) {
private val _galleryStateFlow = MutableStateFlow<List<GalleryEntryWithMetadata>>(listOf())
val galleryStateFlow: StateFlow<List<GalleryEntryWithMetadata>> = _galleryStateFlow
init {
updatePictures()
}
fun updatePictures() {
deps.ioScope.launch {
try {
val pics = getNewPictures(deps)
_galleryStateFlow.emit(pics)
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
// https://kotlinlang.org/docs/exception-handling.html#exceptions-aggregation
throw e
} catch (e: Exception) {
e.printStackTrace()
deps.notification.notifyNoInternet()
}
}
}
private suspend fun getNewPictures(dependencies: Dependencies): List<GalleryEntryWithMetadata> {
val pictures = dependencies.json.decodeFromString(
ListSerializer(Picture.serializer()),
dependencies.httpClient.get(PICTURES_DATA_URL).bodyAsText()
)
val miniatures = pictures
.map { picture ->
dependencies.ioScope.async {
picture to dependencies.imageRepository.loadContent(picture.smallUrl)
}
}
.awaitAll()
.mapIndexed { index, pictureAndBitmap ->
val (pic, bit) = pictureAndBitmap
GalleryEntryWithMetadata(GalleryId(index.toLong()), pic, bit)
}
return miniatures
}
}

30
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Picture.kt

@ -1,30 +0,0 @@
package example.imageviewer.model
import kotlinx.serialization.Serializable
@Serializable
data class Picture(val big: String, val small: String)
fun getNameURL(url: String): String = url.substring(url.lastIndexOf('/') + 1, url.length)
val Picture.name: String get() {
val realName = getNameURL(big)
return mockNames.getOrElse(realName) { realName }
}
val Picture.bigUrl get() = "$BASE_URL/$big"
val Picture.smallUrl get() = "$BASE_URL/$small"
val mockNames = mapOf(
"1.jpg" to "Gondolas",
"2.jpg" to "Winter Pier",
"3.jpg" to "Kitties outside",
"4.jpg" to "Heap of trees",
"5.jpg" to "Resilient Cacti",
"6.jpg" to "Swirls",
"7.jpg" to "Gradient Descent",
"8.jpg" to "Sleepy in Seattle",
"9.jpg" to "Lightful infrastructure",
"10.jpg" to "Compose Pathway",
"11.jpg" to "Rotary",
"12.jpg" to "Towering",
"13.jpg" to "Vasa"
)

62
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt

@ -0,0 +1,62 @@
package example.imageviewer.model
import example.imageviewer.createUUID
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.Serializable
@Serializable
class GpsPosition(
val latitude: Double,
val longitude: Double
)
sealed interface PictureData {
val name: String
val description: String
val gps: GpsPosition
val dateString: String
class Resource(
val resource: String,
val thumbnailResource: String,
override val name: String,
override val description: String,
override val gps: GpsPosition,
override val dateString: String,
) : PictureData
@Serializable
class Camera(
val id: String,
val timeStampSeconds: Long,
override val name: String,
override val description: String,
override val gps: GpsPosition,
) : PictureData {
override fun equals(other: Any?): Boolean = (other as? Camera)?.id == id
override fun hashCode(): Int = id.hashCode()
override val dateString: String
get(): String {
val instantTime = Instant.fromEpochSeconds(timeStampSeconds, 0)
val utcTime = instantTime.toLocalDateTime(TimeZone.UTC)
val date = utcTime.date
val monthStr = date.month.name.lowercase()
.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
.take(3)
val dayStr = date.dayOfMonth
return "$dayStr $monthStr."
}
}
}
fun createCameraPictureData(name: String, description: String, gps: GpsPosition) =
PictureData.Camera(
id = createUUID(),
timeStampSeconds = Clock.System.now().epochSeconds,
name = name,
description = description,
gps = gps,
)

8
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ScalableState.kt

@ -8,7 +8,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
class ScalableState(val imageSize: IntSize) { class ScalableState() {
var imageSize by mutableStateOf(IntSize(0, 0))
var boxSize by mutableStateOf(IntSize(1, 1)) var boxSize by mutableStateOf(IntSize(1, 1))
var offset by mutableStateOf(IntOffset.Zero) var offset by mutableStateOf(IntOffset.Zero)
var scale by mutableStateOf(1f) var scale by mutableStateOf(1f)
@ -58,6 +59,11 @@ fun ScalableState.addDragAmount(diff: Offset) {
updateOffsetLimits() updateOffsetLimits()
} }
fun ScalableState.updateImageSize(width: Int, height: Int) {
imageSize = IntSize(width, height)
updateOffsetLimits()
}
private fun ScalableState.updateOffsetLimits() { private fun ScalableState.updateOffsetLimits() {
if (offset.x + visiblePart.width > imageSize.width) { if (offset.x + visiblePart.width > imageSize.width) {
changeOffset(x = imageSize.width - visiblePart.width) changeOffset(x = imageSize.width - visiblePart.width)

7
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt

@ -1,5 +1,12 @@
package example.imageviewer package example.imageviewer
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import kotlinx.coroutines.CoroutineDispatcher
expect fun Modifier.notchPadding(): Modifier expect fun Modifier.notchPadding(): Modifier
expect class PlatformStorableImage
expect fun createUUID(): String
expect val ioDispatcher: CoroutineDispatcher

4
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt

@ -26,8 +26,8 @@ object ImageviewerColors {
val onBackground = Color(0xFF19191C) val onBackground = Color(0xFF19191C)
val fullScreenImageBackground = Color(0xFF19191C) val fullScreenImageBackground = Color(0xFF19191C)
val filterButtonsBackground = fullScreenImageBackground.copy(alpha = 0.7f)
val uiLightBlack = Color(25, 25, 28, 180) val uiLightBlack = Color(25, 25, 28).copy(alpha = 0.7f)
val textOnImage = Color.White val textOnImage = Color.White
val noteBlockBackground = Color(0xFFF3F3F4) val noteBlockBackground = Color(0xFFF3F3F4)

5
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/utils/IOScope.kt

@ -1,5 +0,0 @@
package example.imageviewer.utils
import kotlinx.coroutines.CoroutineDispatcher
expect val ioDispatcher: CoroutineDispatcher

39
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt

@ -1,24 +1,35 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.foundation.layout.* import androidx.compose.foundation.background
import androidx.compose.runtime.Composable import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import example.imageviewer.Localization import androidx.compose.ui.graphics.Color
import org.jetbrains.compose.resources.ExperimentalResourceApi import example.imageviewer.LocalImageStorage
import org.jetbrains.compose.resources.painterResource import kotlinx.coroutines.delay
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal fun CameraScreen(localization: Localization, onBack: () -> Unit) { internal fun CameraScreen(onBack: (resetSelectedPicture: Boolean) -> Unit) {
Box(Modifier.fillMaxSize()) { val storage = LocalImageStorage.current
CameraView(Modifier.fillMaxSize()) var showCamera by remember { mutableStateOf(false) }
LaunchedEffect(onBack) {
if (!showCamera) {
delay(300) // for animation
showCamera = true
}
}
Box(Modifier.fillMaxSize().background(Color.Black)) {
if (showCamera) {
CameraView(Modifier.fillMaxSize(), onCapture = { picture, image ->
storage.saveImage(picture, image)
onBack(true)
})
}
TopLayout( TopLayout(
alignLeftContent = { alignLeftContent = {
Tooltip(localization.back) { BackButton {
CircularButton( onBack(false)
painterResource("arrowleft.png"),
onClick = { onBack() }
)
} }
}, },
alignRightContent = {}, alignRightContent = {},

5
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraView.common.kt

@ -2,6 +2,9 @@ package example.imageviewer.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import example.imageviewer.ImageStorage
import example.imageviewer.PlatformStorableImage
import example.imageviewer.model.PictureData
@Composable @Composable
internal expect fun CameraView(modifier: Modifier) internal expect fun CameraView(modifier: Modifier, onCapture: (picture: PictureData.Camera, image: PlatformStorableImage)->Unit)

16
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CircularButton.kt

@ -12,7 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.LocalLocalization
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@Composable @Composable
internal fun CircularButton( internal fun CircularButton(
@ -21,7 +24,7 @@ internal fun CircularButton(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Box( Box(
modifier.size(50.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack) modifier.size(54.dp).clip(CircleShape).background(ImageviewerColors.uiLightBlack)
.clickable { onClick() }, contentAlignment = Alignment.Center .clickable { onClick() }, contentAlignment = Alignment.Center
) { ) {
Image( Image(
@ -31,3 +34,14 @@ internal fun CircularButton(
) )
} }
} }
@OptIn(ExperimentalResourceApi::class)
@Composable
internal fun BackButton(onClick: () -> Unit) {
Tooltip(LocalLocalization.current.back) {
CircularButton(
painterResource("arrowleft.png"),
onClick = onClick
)
}
}

218
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt

@ -1,218 +0,0 @@
package example.imageviewer.view
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.*
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import example.imageviewer.Localization
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
import example.imageviewer.model.*
import example.imageviewer.style.*
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@Composable
internal fun FullscreenImage(
galleryId: GalleryId?,
gallery: PhotoGallery,
getImage: suspend (Picture) -> ImageBitmap,
getFilter: (FilterType) -> BitmapFilter,
localization: Localization,
back: () -> Unit,
) {
val picture = gallery.galleryStateFlow.value.first { it.id == galleryId }.picture
val availableFilters = FilterType.values().toList()
var selectedFilters by remember { mutableStateOf(emptySet<FilterType>()) }
val originalImageState = remember(galleryId) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(galleryId) {
if (galleryId != null) {
originalImageState.value = getImage(picture)
}
}
val originalImage = originalImageState.value
val imageWithFilter = remember(originalImage, selectedFilters) {
if (originalImage != null) {
var result: ImageBitmap = originalImage
for (filter in selectedFilters.map { getFilter(it) }) {
result = filter.apply(result)
}
result
} else {
null
}
}
Box(Modifier.fillMaxSize().background(color = ImageviewerColors.fullScreenImageBackground)) {
Column {
FullscreenImageBar(
localization,
picture.name,
back,
availableFilters,
selectedFilters,
onSelectFilter = {
if (it !in selectedFilters) {
selectedFilters += it
} else {
selectedFilters -= it
}
})
if (imageWithFilter != null) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
val imageSize = IntSize(imageWithFilter.width, imageWithFilter.height)
val scalableState = remember(imageSize) { ScalableState(imageSize) }
val visiblePartOfImage: IntRect = scalableState.visiblePart
Column {
Slider(
modifier = Modifier.fillMaxWidth(),
value = scalableState.scale,
valueRange = MIN_SCALE..MAX_SCALE,
onValueChange = { scalableState.setScale(it) },
)
Box(
modifier = Modifier.fillMaxSize()
.onGloballyPositioned { coordinates ->
scalableState.changeBoxSize(coordinates.size)
}
.addUserInput(scalableState)
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = BitmapPainter(
imageWithFilter,
srcOffset = visiblePartOfImage.topLeft,
srcSize = visiblePartOfImage.size
),
contentDescription = null
)
}
}
Box(
Modifier.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.background(ImageviewerColors.fullScreenImageBackground).padding(16.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(bottom = 16.dp)
) {
FilterButtons(availableFilters, selectedFilters, {
if (it !in selectedFilters) {
selectedFilters += it
} else {
selectedFilters -= it
}
})
}
}
}
} else {
LoadingScreen()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class)
@Composable
private fun FullscreenImageBar(
localization: Localization,
pictureName: String?,
onBack: () -> Unit,
filters: List<FilterType>,
selectedFilters: Set<FilterType>,
onSelectFilter: (FilterType) -> Unit
) {
TopLayout(
alignLeftContent = {
Tooltip(localization.back) {
CircularButton(
painterResource("arrowleft.png"),
onClick = { onBack() }
)
}
},
alignRightContent = {},
)
}
@Composable
private fun FilterButtons(
filters: List<FilterType>,
selectedFilters: Set<FilterType>,
onSelectFilter: (FilterType) -> Unit
) {
for (type in filters) {
FilterButton(active = type in selectedFilters,
type,
onClick = {
onSelectFilter(type)
})
}
}
@Composable
private fun FilterButton(
active: Boolean,
type: FilterType,
onClick: () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
val filterButtonHover by interactionSource.collectIsHoveredAsState()
Box(
modifier = Modifier.background(color = ImageviewerColors.Transparent).clip(CircleShape)
) {
Tooltip(type.toString()) {
Image(
getFilterImage(active, type = type),
contentDescription = null,
Modifier.size(40.dp)
.hoverable(interactionSource)
.background(color = ImageviewerColors.buttonBackground(filterButtonHover))
.clickable { onClick() }
)
}
}
}
@OptIn(ExperimentalResourceApi::class)
@Composable
private fun getFilterImage(active: Boolean, type: FilterType): Painter {
return when (type) {
FilterType.GrayScale -> if (active) {
painterResource("grayscale_on.png")
} else {
painterResource("grayscale_off.png")
}
FilterType.Pixel -> if (active) {
painterResource("pixel_on.png")
} else {
painterResource("pixel_off.png")
}
FilterType.Blur -> if (active) {
painterResource("blur_on.png")
} else {
painterResource("blur_off.png")
}
}
}

145
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImageScreen.kt

@ -0,0 +1,145 @@
package example.imageviewer.view
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
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.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
import example.imageviewer.LocalImageProvider
import example.imageviewer.Localization
import example.imageviewer.LocalLocalization
import example.imageviewer.filter.FilterType
import example.imageviewer.filter.getFilter
import example.imageviewer.filter.getPlatformContext
import example.imageviewer.model.*
import example.imageviewer.style.*
@Composable
internal fun FullscreenImageScreen(
picture: PictureData,
back: () -> Unit,
) {
val imageProvider = LocalImageProvider.current
val localization: Localization = LocalLocalization.current
val availableFilters = FilterType.values().toList()
var selectedFilters by remember { mutableStateOf(emptySet<FilterType>()) }
val originalImageState = remember(picture) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(picture) {
originalImageState.value = imageProvider.getImage(picture)
}
val platformContext = getPlatformContext()
val originalImage = originalImageState.value
val imageWithFilter = remember(originalImage, selectedFilters) {
if (originalImage != null) {
var result: ImageBitmap = originalImage
for (filter in selectedFilters.map { getFilter(it) }) {
result = filter.invoke(result, platformContext)
}
result
} else {
null
}
}
Box(Modifier.fillMaxSize().background(color = ImageviewerColors.fullScreenImageBackground)) {
if (imageWithFilter != null) {
val scalableState = remember { ScalableState() }
scalableState.updateImageSize(imageWithFilter.width, imageWithFilter.height)
val visiblePartOfImage: IntRect = scalableState.visiblePart
Box(
Modifier.fillMaxSize()
.onGloballyPositioned { coordinates ->
scalableState.changeBoxSize(coordinates.size)
}
.addUserInput(scalableState)
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = BitmapPainter(
imageWithFilter,
srcOffset = visiblePartOfImage.topLeft,
srcSize = visiblePartOfImage.size
),
contentDescription = null,
)
Column(
Modifier
.align(Alignment.BottomCenter)
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
.background(ImageviewerColors.filterButtonsBackground)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
FilterButtons(
picture = picture,
filters = availableFilters,
selectedFilters = selectedFilters,
onSelectFilter = {
if (it !in selectedFilters) {
selectedFilters += it
} else {
selectedFilters -= it
}
},
)
ZoomControllerView(Modifier, scalableState)
}
}
} else {
LoadingScreen()
}
TopLayout(
alignLeftContent = {
Tooltip(localization.back) {
BackButton(back)
}
},
alignRightContent = {},
)
}
}
@Composable
private fun FilterButtons(
picture: PictureData,
filters: List<FilterType>,
selectedFilters: Set<FilterType>,
onSelectFilter: (FilterType) -> Unit,
) {
val platformContext = getPlatformContext()
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(bottom = 16.dp)
) {
for (type in filters) {
Tooltip(type.toString()) {
ThumbnailImage(
modifier = Modifier
.size(60.dp)
.clip(CircleShape)
.border(
color = if (type in selectedFilters) Color.White else Color.Gray,
width = 3.dp,
shape = CircleShape
)
.clickable {
onSelectFilter(type)
},
picture = picture,
filter = remember { { getFilter(type).invoke(it, platformContext) } }
)
}
}
}
}

142
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/GalleryScreen.kt

@ -2,9 +2,16 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.with
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
@ -12,19 +19,16 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.Dependencies import example.imageviewer.*
import example.imageviewer.ExternalImageViewerEvent import example.imageviewer.model.*
import example.imageviewer.model.GalleryEntryWithMetadata
import example.imageviewer.model.GalleryId
import example.imageviewer.model.GalleryPage
import example.imageviewer.model.PhotoGallery
import example.imageviewer.model.bigUrl
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@ -38,71 +42,88 @@ enum class GalleryStyle {
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal fun GalleryScreen( internal fun GalleryScreen(
galleryPage: GalleryPage, pictures: SnapshotStateList<PictureData>,
photoGallery: PhotoGallery, selectedPictureIndex: MutableState<Int>,
dependencies: Dependencies, onClickPreviewPicture: (PictureData) -> Unit,
onClickPreviewPicture: (GalleryId) -> Unit,
onMakeNewMemory: () -> Unit onMakeNewMemory: () -> Unit
) { ) {
val pictures by photoGallery.galleryStateFlow.collectAsState() fun nextImage() {
selectedPictureIndex.value =
(selectedPictureIndex.value + 1).mod(pictures.size)
}
fun previousImage() {
selectedPictureIndex.value =
(selectedPictureIndex.value - 1).mod(pictures.size)
}
fun selectPicture(picture: PictureData) {
selectedPictureIndex.value = pictures.indexOfFirst { it == picture }
}
val picture = pictures.getOrNull(selectedPictureIndex.value)
var galleryStyle by remember { mutableStateOf(GalleryStyle.SQUARES) }
val externalEvents = LocalInternalEvents.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
galleryPage.externalEvents.collect { externalEvents.collect {
when (it) { when (it) {
ExternalImageViewerEvent.Foward -> galleryPage.nextImage() ExternalImageViewerEvent.Foward -> nextImage()
ExternalImageViewerEvent.Back -> galleryPage.previousImage() ExternalImageViewerEvent.Back -> previousImage()
else -> {}
} }
} }
} }
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
Box { Box {
picture?.let {
PreviewImage( PreviewImage(
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) }, picture = it, onClick = {
picture = galleryPage.galleryEntry, onClick = { onClickPreviewPicture(it)
galleryPage.pictureId?.let(onClickPreviewPicture)
} }
) )
}
TopLayout( TopLayout(
alignLeftContent = {}, alignLeftContent = {},
alignRightContent = { alignRightContent = {
CircularButton(painterResource("list_view.png")) { CircularButton(painterResource("list_view.png")) {
galleryPage.toggleGalleryStyle() galleryStyle = when (galleryStyle) {
GalleryStyle.SQUARES -> GalleryStyle.LIST
GalleryStyle.LIST -> GalleryStyle.SQUARES
}
} }
}, },
) )
} }
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
when (galleryPage.galleryStyle) { when (galleryStyle) {
GalleryStyle.SQUARES -> SquaresGalleryView( GalleryStyle.SQUARES -> SquaresGalleryView(
pictures, images = pictures,
galleryPage.pictureId, selectedImage = picture,
onSelect = { galleryPage.selectPicture(it) }, onSelect = { selectPicture(it) },
) )
GalleryStyle.LIST -> ListGalleryView( GalleryStyle.LIST -> ListGalleryView(
pictures, pictures = pictures,
dependencies, onSelect = { selectPicture(it) },
onSelect = { galleryPage.selectPicture(it) }, onFullScreen = { onClickPreviewPicture(it) },
onFullScreen = { onClickPreviewPicture(it) }
) )
} }
CircularButton( CircularButton(
image = painterResource("plus.png"), image = painterResource("plus.png"),
modifier = Modifier.align(Alignment.BottomCenter).padding(48.dp), modifier = Modifier.align(Alignment.BottomCenter).padding(36.dp),
onClick = onMakeNewMemory, onClick = onMakeNewMemory,
) )
} }
} }
if (pictures.isEmpty()) {
LoadingScreen(dependencies.localization.loading)
}
} }
@Composable @Composable
private fun SquaresGalleryView( private fun SquaresGalleryView(
images: List<GalleryEntryWithMetadata>, images: List<PictureData>,
selectedImage: GalleryId?, selectedImage: PictureData?,
onSelect: (GalleryId) -> Unit, onSelect: (PictureData) -> Unit,
) { ) {
Column { Column {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
@ -111,11 +132,10 @@ private fun SquaresGalleryView(
verticalArrangement = Arrangement.spacedBy(1.dp), verticalArrangement = Arrangement.spacedBy(1.dp),
horizontalArrangement = Arrangement.spacedBy(1.dp) horizontalArrangement = Arrangement.spacedBy(1.dp)
) { ) {
itemsIndexed(images) { idx, image -> itemsIndexed(images) { idx, picture ->
val isSelected = image.id == selectedImage val isSelected = picture == selectedImage
val (picture, bitmap) = image SquareThumbnail(
SquareMiniature( picture = picture,
image.thumbnail,
onClick = { onSelect(picture) }, onClick = { onSelect(picture) },
isHighlighted = isSelected isHighlighted = isSelected
) )
@ -126,21 +146,21 @@ private fun SquaresGalleryView(
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick: () -> Unit) { internal fun SquareThumbnail(
picture: PictureData,
isHighlighted: Boolean,
onClick: () -> Unit
) {
Box( Box(
Modifier.aspectRatio(1.0f).clickable { onClick() }, Modifier.aspectRatio(1.0f).clickable(onClick = onClick),
contentAlignment = Alignment.BottomEnd contentAlignment = Alignment.BottomEnd
) { ) {
Image( Tooltip(picture.name) {
bitmap = image, ThumbnailImage(
contentDescription = null, modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().clickable { onClick() }.then( picture = picture,
if (isHighlighted) {
Modifier//.border(BorderStroke(5.dp, Color.White))
} else Modifier
),
contentScale = ContentScale.Crop
) )
}
if (isHighlighted) { if (isHighlighted) {
Box(Modifier.fillMaxSize().background(ImageviewerColors.uiLightBlack)) Box(Modifier.fillMaxSize().background(ImageviewerColors.uiLightBlack))
Box( Box(
@ -169,28 +189,26 @@ internal fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick
@Composable @Composable
private fun ListGalleryView( private fun ListGalleryView(
pictures: List<GalleryEntryWithMetadata>, pictures: List<PictureData>,
dependencies: Dependencies, onSelect: (PictureData) -> Unit,
onSelect: (GalleryId) -> Unit, onFullScreen: (PictureData) -> Unit,
onFullScreen: (GalleryId) -> Unit
) { ) {
val notification = LocalNotification.current
ScrollableColumn( ScrollableColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
for ((idx, picWithThumb) in pictures.withIndex()) { for (p in pictures.withIndex()) {
val (galleryId, picture, miniature) = picWithThumb Thumbnail(
Miniature( picture = p.value,
picture = picture,
image = miniature,
onClickSelect = { onClickSelect = {
onSelect(galleryId) onSelect(p.value)
}, },
onClickFullScreen = { onClickFullScreen = {
onFullScreen(galleryId) onFullScreen(p.value)
}, },
onClickInfo = { onClickInfo = {
dependencies.notification.notifyImageData(picture) notification.notifyImageData(p.value)
}, },
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))

3
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LocationVisualizer.common.kt

@ -2,6 +2,7 @@ package example.imageviewer.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import example.imageviewer.model.GpsPosition
@Composable @Composable
internal expect fun LocationVisualizer(modifier: Modifier) internal expect fun LocationVisualizer(modifier: Modifier, gps: GpsPosition, title: String)

90
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt

@ -11,6 +11,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -24,12 +25,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import example.imageviewer.Localization import example.imageviewer.ImageProvider
import example.imageviewer.model.GalleryEntryWithMetadata import example.imageviewer.LocalImageProvider
import example.imageviewer.model.GalleryId import example.imageviewer.model.*
import example.imageviewer.model.MemoryPage
import example.imageviewer.model.PhotoGallery
import example.imageviewer.model.Picture
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@ -37,22 +35,19 @@ import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun MemoryScreen( internal fun MemoryScreen(
pictures: SnapshotStateList<PictureData>,
memoryPage: MemoryPage, memoryPage: MemoryPage,
photoGallery: PhotoGallery, onSelectRelatedMemory: (PictureData) -> Unit,
getImage: suspend (Picture) -> ImageBitmap,
localization: Localization,
onSelectRelatedMemory: (GalleryId) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
onHeaderClick: (GalleryId) -> Unit onHeaderClick: (PictureData) -> Unit,
) { ) {
val pictures by photoGallery.galleryStateFlow.collectAsState() val imageProvider = LocalImageProvider.current
val picture = pictures.first { it.id == memoryPage.galleryId } var headerImage: ImageBitmap? by remember(memoryPage.picture) { mutableStateOf(null) }
var headerImage by remember(picture) { mutableStateOf(picture.thumbnail) } LaunchedEffect(memoryPage.picture) {
LaunchedEffect(picture) { headerImage = imageProvider.getImage(memoryPage.picture)
headerImage = getImage(picture.picture)
} }
Box { Box {
val scrollState = memoryPage.scrollState val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -68,22 +63,20 @@ internal fun MemoryScreen(
}, },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
MemoryHeader(headerImage, onClick = { onHeaderClick(memoryPage.galleryId) }) headerImage?.let {
MemoryHeader(
it,
picture = memoryPage.picture,
onClick = { onHeaderClick(memoryPage.picture) }
)
}
} }
Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) { Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
Column { Column {
Headliner("Note") Headliner("Note")
Collapsible( Collapsible(memoryPage.picture.description)
"""
I took a picture with my iPhone 14 at 17:45. The picture ended up being 3024 x 4032 pixels.
I took multiple additional photos of the same subject, but they turned out not quite as well, so I decided to keep this specific one as a memory.
I might upload this picture to Unsplash at some point, since other people might also enjoy this picture. So it would make sense to not keep it to myself! 😄
""".trimIndent()
)
Headliner("Related memories") Headliner("Related memories")
RelatedMemoriesVisualizer(pictures, onSelectRelatedMemory) RelatedMemoriesVisualizer(pictures, imageProvider, onSelectRelatedMemory)
Headliner("Place") Headliner("Place")
val locationShape = RoundedCornerShape(10.dp) val locationShape = RoundedCornerShape(10.dp)
LocationVisualizer( LocationVisualizer(
@ -91,7 +84,9 @@ internal fun MemoryScreen(
.clip(locationShape) .clip(locationShape)
.border(1.dp, Color.Gray, locationShape) .border(1.dp, Color.Gray, locationShape)
.fillMaxWidth() .fillMaxWidth()
.height(200.dp) .height(200.dp),
gps = memoryPage.picture.gps,
title = memoryPage.picture.name,
) )
Spacer(Modifier.height(50.dp)) Spacer(Modifier.height(50.dp))
Row( Row(
@ -121,12 +116,7 @@ internal fun MemoryScreen(
} }
TopLayout( TopLayout(
alignLeftContent = { alignLeftContent = {
Tooltip(localization.back) { BackButton(onBack)
CircularButton(
painterResource("arrowleft.png"),
onClick = { onBack() }
)
}
}, },
alignRightContent = {}, alignRightContent = {},
) )
@ -134,7 +124,7 @@ internal fun MemoryScreen(
} }
@Composable @Composable
private fun MemoryHeader(bitmap: ImageBitmap, onClick: () -> Unit) { private fun MemoryHeader(bitmap: ImageBitmap, picture: PictureData, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box(modifier = Modifier.clickable(interactionSource, null, onClick = { onClick() })) { Box(modifier = Modifier.clickable(interactionSource, null, onClick = { onClick() })) {
@ -145,7 +135,7 @@ private fun MemoryHeader(bitmap: ImageBitmap, onClick: () -> Unit) {
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
MagicButtonOverlay(onClick) MagicButtonOverlay(onClick)
MemoryTextOverlay() MemoryTextOverlay(picture)
} }
} }
@ -160,7 +150,7 @@ internal fun BoxScope.MagicButtonOverlay(onClick: () -> Unit) {
} }
@Composable @Composable
internal fun BoxScope.MemoryTextOverlay() { internal fun BoxScope.MemoryTextOverlay(picture: PictureData) {
val shadowTextStyle = LocalTextStyle.current.copy( val shadowTextStyle = LocalTextStyle.current.copy(
shadow = Shadow( shadow = Shadow(
color = Color.Black.copy(0.75f), color = Color.Black.copy(0.75f),
@ -172,7 +162,7 @@ internal fun BoxScope.MemoryTextOverlay() {
modifier = Modifier.align(Alignment.BottomStart).padding(start = 12.dp, bottom = 16.dp) modifier = Modifier.align(Alignment.BottomStart).padding(start = 12.dp, bottom = 16.dp)
) { ) {
Text( Text(
"28. Feb", text = picture.dateString,
textAlign = TextAlign.Left, textAlign = TextAlign.Left,
color = Color.White, color = Color.White,
fontSize = 20.sp, fontSize = 20.sp,
@ -183,7 +173,7 @@ internal fun BoxScope.MemoryTextOverlay() {
) )
Spacer(Modifier.height(1.dp)) Spacer(Modifier.height(1.dp))
Text( Text(
"London", text = picture.name,
textAlign = TextAlign.Left, textAlign = TextAlign.Left,
color = Color.White, color = Color.White,
fontSize = 14.sp, fontSize = 14.sp,
@ -233,8 +223,9 @@ internal fun Headliner(s: String) {
@Composable @Composable
internal fun RelatedMemoriesVisualizer( internal fun RelatedMemoriesVisualizer(
ps: List<GalleryEntryWithMetadata>, ps: List<PictureData>,
onSelectRelatedMemory: (GalleryId) -> Unit imageProvider: ImageProvider,
onSelectRelatedMemory: (PictureData) -> Unit
) { ) {
Box( Box(
modifier = Modifier.padding(10.dp, 0.dp).clip(RoundedCornerShape(10.dp)).fillMaxWidth() modifier = Modifier.padding(10.dp, 0.dp).clip(RoundedCornerShape(10.dp)).fillMaxWidth()
@ -244,7 +235,7 @@ internal fun RelatedMemoriesVisualizer(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
itemsIndexed(ps) { idx, item -> itemsIndexed(ps) { idx, item ->
RelatedMemory(idx, item, onSelectRelatedMemory) RelatedMemory(idx, item, imageProvider, onSelectRelatedMemory)
} }
} }
} }
@ -253,13 +244,14 @@ internal fun RelatedMemoriesVisualizer(
@Composable @Composable
internal fun RelatedMemory( internal fun RelatedMemory(
index: Int, index: Int,
galleryEntry: GalleryEntryWithMetadata, galleryEntry: PictureData,
onSelectRelatedMemory: (GalleryId) -> Unit imageProvider: ImageProvider,
onSelectRelatedMemory: (PictureData) -> Unit
) { ) {
Box(Modifier.size(130.dp).clip(RoundedCornerShape(8.dp))) { Box(Modifier.size(130.dp).clip(RoundedCornerShape(8.dp))) {
SquareMiniature( SquareThumbnail(
galleryEntry.thumbnail, picture = galleryEntry,
false, isHighlighted = false,
onClick = { onSelectRelatedMemory(galleryEntry.id) }) onClick = { onSelectRelatedMemory(galleryEntry) })
} }
} }

16
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt

@ -27,16 +27,16 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.model.GalleryEntryWithMetadata import example.imageviewer.LocalImageProvider
import example.imageviewer.model.Picture import example.imageviewer.model.PictureData
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
@Composable @Composable
internal fun PreviewImage( internal fun PreviewImage(
picture: GalleryEntryWithMetadata?, picture: PictureData,
onClick: () -> Unit, onClick: () -> Unit,
getImage: suspend (Picture) -> ImageBitmap
) { ) {
val imageProvider = LocalImageProvider.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Box( Box(
Modifier.fillMaxWidth().height(393.dp).background(Color.Black), Modifier.fillMaxWidth().height(393.dp).background(Color.Black),
@ -59,11 +59,9 @@ internal fun PreviewImage(
) )
} }
) { currentPicture -> ) { currentPicture ->
var image by remember(currentPicture) { mutableStateOf(currentPicture?.thumbnail) } var image: ImageBitmap? by remember(currentPicture) { mutableStateOf(null) }
LaunchedEffect(currentPicture) { LaunchedEffect(currentPicture) {
if (currentPicture != null) { image = imageProvider.getImage(currentPicture)
image = getImage(currentPicture.picture)
}
} }
if (image != null) { if (image != null) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
@ -74,7 +72,7 @@ internal fun PreviewImage(
.fillMaxSize(), .fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
MemoryTextOverlay() MemoryTextOverlay(currentPicture)
} }
} else { } else {
Spacer( Spacer(

38
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Miniature.kt → experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Thumbnail.kt

@ -4,40 +4,28 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.*
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.model.Picture import example.imageviewer.model.PictureData
import example.imageviewer.model.name
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
internal fun Miniature( internal fun Thumbnail(
picture: Picture, picture: PictureData,
image: ImageBitmap?,
onClickSelect: () -> Unit, onClickSelect: () -> Unit,
onClickFullScreen: () -> Unit, onClickFullScreen: () -> Unit,
onClickInfo: () -> Unit, onClickInfo: () -> Unit
) { ) {
Card( Card(
modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp) modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
@ -52,20 +40,14 @@ internal fun Miniature(
) { ) {
Row(modifier = Modifier.padding(end = 30.dp)) { Row(modifier = Modifier.padding(end = 30.dp)) {
val modifier = Modifier.height(70.dp) Tooltip(picture.name) {
.width(70.dp) ThumbnailImage(
if (image != null) { modifier = Modifier.size(70.dp)
Image(
image,
contentDescription = null,
modifier = modifier
.clip(CircleShape) .clip(CircleShape)
.border(BorderStroke(1.dp, Color.White), CircleShape) .border(BorderStroke(1.dp, Color.White), CircleShape)
.clickable { onClickFullScreen() }, .clickable { onClickFullScreen() },
contentScale = ContentScale.Crop picture = picture,
) )
} else {
CircularProgressIndicator(modifier)
} }
Text( Text(
text = picture.name, text = picture.name,

30
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ThumbnailImage.kt

@ -0,0 +1,30 @@
package example.imageviewer.view
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import example.imageviewer.LocalImageProvider
import example.imageviewer.model.PictureData
@Composable
internal fun ThumbnailImage(
modifier: Modifier,
picture: PictureData,
filter: (ImageBitmap) -> ImageBitmap = remember { { it } },
) {
val imageProvider = LocalImageProvider.current
var imageBitmap by remember(picture) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(picture) {
imageBitmap = imageProvider.getThumbnail(picture)
}
imageBitmap?.let {
Image(
bitmap = filter(it),
contentDescription = picture.name,
modifier = modifier,
contentScale = ContentScale.Crop,
)
}
}

3
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt

@ -11,6 +11,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.model.TOAST_DURATION import example.imageviewer.model.TOAST_DURATION
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
@ -37,7 +38,7 @@ internal fun Toast(
shape = RoundedCornerShape(4.dp) shape = RoundedCornerShape(4.dp)
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
Text(value.message) Text(value.message, color = Color.White)
} }
LaunchedEffect(value.message) { LaunchedEffect(value.message) {
delay(TOAST_DURATION) delay(TOAST_DURATION)

1
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Tooltip.common.kt

@ -1,6 +1,7 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable @Composable
internal expect fun Tooltip( internal expect fun Tooltip(

9
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ZoomControllerView.common.kt

@ -0,0 +1,9 @@
package example.imageviewer.view
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import example.imageviewer.model.ScalableState
@Composable
internal expect fun ZoomControllerView(modifier: Modifier, scalableState: ScalableState)

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/1-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/10-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/10.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/11-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/11.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/12-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/12.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/13-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/13.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/14-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/14.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/15-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/15.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/16-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/16.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/17-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/17.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/2-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/3-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/4-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/4.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/5-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/5.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/6-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/6.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/7-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/7.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/8-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/8.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/9-thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/9.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/back.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/blur_off.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/blur_on.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/empty.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_off.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_on.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
experimental/examples/imageviewer/shared/src/commonMain/resources/pixel_off.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save