diff --git a/artwork/imageviewerrepo/pictures.json b/artwork/imageviewerrepo/pictures.json
new file mode 100644
index 00000000..adecca67
--- /dev/null
+++ b/artwork/imageviewerrepo/pictures.json
@@ -0,0 +1,15 @@
+[
+ {"big": "1.jpg", "small": "small/1.jpg"},
+ {"big": "2.jpg", "small": "small/2.jpg"},
+ {"big": "3.jpg", "small": "small/3.jpg"},
+ {"big": "4.jpg", "small": "small/4.jpg"},
+ {"big": "5.jpg", "small": "small/5.jpg"},
+ {"big": "6.jpg", "small": "small/6.jpg"},
+ {"big": "7.jpg", "small": "small/7.jpg"},
+ {"big": "8.jpg", "small": "small/8.jpg"},
+ {"big": "9.jpg", "small": "small/9.jpg"},
+ {"big": "10.jpg", "small": "small/10.jpg"},
+ {"big": "11.jpg", "small": "small/11.jpg"},
+ {"big": "12.jpg", "small": "small/12.jpg"},
+ {"big": "13.jpg", "small": "small/13.jpg"}
+]
diff --git a/artwork/imageviewerrepo/small/1.jpg b/artwork/imageviewerrepo/small/1.jpg
new file mode 100644
index 00000000..1543e7e3
Binary files /dev/null and b/artwork/imageviewerrepo/small/1.jpg differ
diff --git a/artwork/imageviewerrepo/small/10.jpg b/artwork/imageviewerrepo/small/10.jpg
new file mode 100644
index 00000000..cd09c38c
Binary files /dev/null and b/artwork/imageviewerrepo/small/10.jpg differ
diff --git a/artwork/imageviewerrepo/small/11.jpg b/artwork/imageviewerrepo/small/11.jpg
new file mode 100644
index 00000000..ea908b31
Binary files /dev/null and b/artwork/imageviewerrepo/small/11.jpg differ
diff --git a/artwork/imageviewerrepo/small/12.jpg b/artwork/imageviewerrepo/small/12.jpg
new file mode 100644
index 00000000..e9134db0
Binary files /dev/null and b/artwork/imageviewerrepo/small/12.jpg differ
diff --git a/artwork/imageviewerrepo/small/13.jpg b/artwork/imageviewerrepo/small/13.jpg
new file mode 100644
index 00000000..c74d0197
Binary files /dev/null and b/artwork/imageviewerrepo/small/13.jpg differ
diff --git a/artwork/imageviewerrepo/small/2.jpg b/artwork/imageviewerrepo/small/2.jpg
new file mode 100644
index 00000000..9c466759
Binary files /dev/null and b/artwork/imageviewerrepo/small/2.jpg differ
diff --git a/artwork/imageviewerrepo/small/3.jpg b/artwork/imageviewerrepo/small/3.jpg
new file mode 100644
index 00000000..17a4e4fc
Binary files /dev/null and b/artwork/imageviewerrepo/small/3.jpg differ
diff --git a/artwork/imageviewerrepo/small/4.jpg b/artwork/imageviewerrepo/small/4.jpg
new file mode 100644
index 00000000..7269b9df
Binary files /dev/null and b/artwork/imageviewerrepo/small/4.jpg differ
diff --git a/artwork/imageviewerrepo/small/5.jpg b/artwork/imageviewerrepo/small/5.jpg
new file mode 100644
index 00000000..40630f71
Binary files /dev/null and b/artwork/imageviewerrepo/small/5.jpg differ
diff --git a/artwork/imageviewerrepo/small/6.jpg b/artwork/imageviewerrepo/small/6.jpg
new file mode 100644
index 00000000..4b8cc6c3
Binary files /dev/null and b/artwork/imageviewerrepo/small/6.jpg differ
diff --git a/artwork/imageviewerrepo/small/7.jpg b/artwork/imageviewerrepo/small/7.jpg
new file mode 100644
index 00000000..08335a4f
Binary files /dev/null and b/artwork/imageviewerrepo/small/7.jpg differ
diff --git a/artwork/imageviewerrepo/small/8.jpg b/artwork/imageviewerrepo/small/8.jpg
new file mode 100644
index 00000000..7da656b6
Binary files /dev/null and b/artwork/imageviewerrepo/small/8.jpg differ
diff --git a/artwork/imageviewerrepo/small/9.jpg b/artwork/imageviewerrepo/small/9.jpg
new file mode 100644
index 00000000..f7dc5754
Binary files /dev/null and b/artwork/imageviewerrepo/small/9.jpg differ
diff --git a/experimental/examples/imageviewer/.gitignore b/experimental/examples/imageviewer/.gitignore
index a32b1659..994fd0a8 100644
--- a/experimental/examples/imageviewer/.gitignore
+++ b/experimental/examples/imageviewer/.gitignore
@@ -13,3 +13,9 @@ build/
/captures
.externalNativeBuild
.cxx
+iosApp/Podfile.lock
+iosApp/Pods/*
+iosApp/Imageviewer.xcworkspace/*
+iosApp/Imageviewer.xcodeproj/*
+!iosApp/Imageviewer.xcodeproj/project.pbxproj
+shared/shared.podspec
diff --git a/experimental/examples/imageviewer/.run/desktop.run.xml b/experimental/examples/imageviewer/.run/desktopApp.run.xml
old mode 100755
new mode 100644
similarity index 65%
rename from experimental/examples/imageviewer/.run/desktop.run.xml
rename to experimental/examples/imageviewer/.run/desktopApp.run.xml
index d9335c1b..95395e11
--- a/experimental/examples/imageviewer/.run/desktop.run.xml
+++ b/experimental/examples/imageviewer/.run/desktopApp.run.xml
@@ -1,8 +1,8 @@
-
+
-
+
- true
+ true
+ true
+ false
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/.run/iosApp (AndroidStudio).run.xml b/experimental/examples/imageviewer/.run/iosApp (AndroidStudio).run.xml
new file mode 100644
index 00000000..97848422
--- /dev/null
+++ b/experimental/examples/imageviewer/.run/iosApp (AndroidStudio).run.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/.run/iosApp.run.xml b/experimental/examples/imageviewer/.run/iosApp.run.xml
new file mode 100644
index 00000000..3a59900d
--- /dev/null
+++ b/experimental/examples/imageviewer/.run/iosApp.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/README.md b/experimental/examples/imageviewer/README.md
index 3d79c526..c9fca538 100755
--- a/experimental/examples/imageviewer/README.md
+++ b/experimental/examples/imageviewer/README.md
@@ -1,11 +1,27 @@
-An example of image gallery for remote server image viewing, based on Jetpack Compose UI library (desktop and android).
+# Imageviewer
-### Running desktop application
- * To run, launch command: `./gradlew :desktop:run`
- * Or choose **desktop** configuration in IDE and run it.
- 
+An example of image gallery for remote server image viewing,
+based on Compose Multiplatform UI library (desktop, android and iOS).
+
+## How to run
+
+Choose a run configuration for an appropriate target in IDE and run it.
+
+
+
+To run on iOS device, please correct `iosApp/Configuration/TeamId.xcconfig` with your Apple Team ID.
+Alternatively, you may setup signing within XCode opening `iosApp/Imageviewer.xcworkspace` and then
+using "Signing & Capabilities" tab of `ImageViewer` target.
+
+Then choose **iosApp** configuration in IDE and run it
+(may also be referred as `ImageViewer` in the Run Configurations or `iosApp (AndroidStudio)` for Android studio).
+
+## Run on desktop via Gradle
+
+`./gradlew desktopApp:run`
### Building native desktop distribution
+
```
./gradlew :desktop:packageDistributionForCurrentOS
# outputs are written to desktop/build/compose/binaries
diff --git a/experimental/examples/imageviewer/android/build.gradle.kts b/experimental/examples/imageviewer/android/build.gradle.kts
deleted file mode 100755
index d5807ca5..00000000
--- a/experimental/examples/imageviewer/android/build.gradle.kts
+++ /dev/null
@@ -1,26 +0,0 @@
-plugins {
- id("com.android.application")
- kotlin("android")
- id("org.jetbrains.compose")
-}
-
-android {
- compileSdk = 32
-
- defaultConfig {
- minSdk = 26
- targetSdk = 32
- versionCode = 1
- versionName = "1.0"
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
-}
-
-dependencies {
- implementation(project(":common"))
- implementation("androidx.activity:activity-compose:1.5.0")
-}
diff --git a/experimental/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt b/experimental/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt
deleted file mode 100755
index 53bb8c61..00000000
--- a/experimental/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package example.imageviewer
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.activity.compose.setContent
-import example.imageviewer.view.AppUI
-import example.imageviewer.model.ContentState
-import example.imageviewer.model.ImageRepository
-
-class MainActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val content = ContentState.applyContent(
- this@MainActivity,
- "https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
- )
-
- setContent {
- AppUI(content)
- }
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/androidApp/build.gradle.kts b/experimental/examples/imageviewer/androidApp/build.gradle.kts
new file mode 100755
index 00000000..cdb92d49
--- /dev/null
+++ b/experimental/examples/imageviewer/androidApp/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ kotlin("multiplatform")
+ id("com.android.application")
+ id("org.jetbrains.compose")
+}
+
+kotlin {
+ android()
+ sourceSets {
+ val androidMain by getting {
+ dependencies {
+ implementation(project(":shared"))
+ implementation("androidx.appcompat:appcompat:1.5.1")
+ implementation("androidx.activity:activity-compose:1.6.1")
+ }
+ }
+ }
+}
+
+android {
+ compileSdk = 33
+ defaultConfig {
+ applicationId = "org.jetbrains.imageviewer"
+ minSdk = 24
+ targetSdk = 33
+ versionCode = 1
+ versionName = "1.0"
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
diff --git a/experimental/examples/imageviewer/android/src/main/AndroidManifest.xml b/experimental/examples/imageviewer/androidApp/src/main/AndroidManifest.xml
similarity index 92%
rename from experimental/examples/imageviewer/android/src/main/AndroidManifest.xml
rename to experimental/examples/imageviewer/androidApp/src/main/AndroidManifest.xml
index 5b1501c0..025f2c4a 100755
--- a/experimental/examples/imageviewer/android/src/main/AndroidManifest.xml
+++ b/experimental/examples/imageviewer/androidApp/src/main/AndroidManifest.xml
@@ -5,7 +5,6 @@
-
-
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt
deleted file mode 100755
index bf5d0b8c..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package example.imageviewer.core
-
-import android.graphics.Bitmap
-
-interface BitmapFilter {
- fun apply(bitmap: Bitmap) : Bitmap
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt
deleted file mode 100644
index 00d4b026..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt
+++ /dev/null
@@ -1,383 +0,0 @@
-package example.imageviewer.model
-
-import android.content.Context
-import android.graphics.*
-import android.os.Handler
-import android.os.Looper
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import example.imageviewer.common.R
-import example.imageviewer.core.FilterType
-import example.imageviewer.model.filtration.FiltersManager
-import example.imageviewer.utils.clearCache
-import example.imageviewer.utils.isInternetAvailable
-import example.imageviewer.view.showPopUpMessage
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-
-
-object ContentState {
-
- private lateinit var context: Context
- private lateinit var repository: ImageRepository
- private lateinit var uriRepository: String
-
- fun applyContent(context: Context, uriRepository: String): ContentState {
- if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
- return this
- }
-
- this.context = context
- this.uriRepository = uriRepository
- repository = ImageRepository(uriRepository)
- appliedFilters = FiltersManager(context)
- isContentReady.value = false
-
- initData()
-
- return this
- }
-
- private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) }
-
- private val handler: Handler by lazy { Handler(Looper.getMainLooper()) }
-
- fun getContext(): Context {
- return context
- }
-
- fun getOrientation(): Int {
- return context.resources.configuration.orientation
- }
-
- private val isAppReady = mutableStateOf(false)
- fun isAppReady(): Boolean {
- return isAppReady.value
- }
-
- private val isContentReady = mutableStateOf(false)
- fun isContentReady(): Boolean {
- return isContentReady.value
- }
-
- fun getString(id: Int): String {
- return context.getString(id)
- }
-
- // drawable content
- private val mainImage = mutableStateOf(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- private val currentImageIndex = mutableStateOf(0)
- private val miniatures = Miniatures()
-
- fun getMiniatures(): List {
- return miniatures.getMiniatures()
- }
-
- fun getSelectedImage(): Bitmap {
- return mainImage.value
- }
-
- fun getSelectedImageName(): String {
- return MainImageWrapper.getName()
- }
-
- // filters managing
- private lateinit var appliedFilters: FiltersManager
- private val filterUIState: MutableMap> = LinkedHashMap()
-
- private fun toggleFilterState(filter: FilterType) {
-
- if (!filterUIState.containsKey(filter)) {
- filterUIState[filter] = mutableStateOf(true)
- } else {
- val value = filterUIState[filter]!!.value
- filterUIState[filter]!!.value = !value
- }
- }
-
- fun toggleFilter(filter: FilterType) {
-
- if (containsFilter(filter)) {
- removeFilter(filter)
- } else {
- addFilter(filter)
- }
-
- toggleFilterState(filter)
-
- var bitmap = MainImageWrapper.origin
-
- if (bitmap != null) {
- bitmap = appliedFilters.applyFilters(bitmap)
- MainImageWrapper.setImage(bitmap)
- mainImage.value = bitmap
- }
- }
-
- private fun addFilter(filter: FilterType) {
- appliedFilters.add(filter)
- MainImageWrapper.addFilter(filter)
- }
-
- private fun removeFilter(filter: FilterType) {
- appliedFilters.remove(filter)
- MainImageWrapper.removeFilter(filter)
- }
-
- private fun containsFilter(type: FilterType): Boolean {
- return appliedFilters.contains(type)
- }
-
- fun isFilterEnabled(type: FilterType): Boolean {
- if (!filterUIState.containsKey(type)) {
- filterUIState[type] = mutableStateOf(false)
- }
- return filterUIState[type]!!.value
- }
-
- private fun restoreFilters(): Bitmap {
- filterUIState.clear()
- appliedFilters.clear()
- return MainImageWrapper.restore()
- }
-
- fun restoreMainImage() {
- mainImage.value = restoreFilters()
- }
-
- // application content initialization
- private fun initData() {
- if (isContentReady.value)
- return
-
- val directory = context.cacheDir.absolutePath
-
- executor.execute {
- try {
- if (isInternetAvailable()) {
- val imageList = repository.get()
-
- if (imageList.isEmpty()) {
- handler.post {
- showPopUpMessage(
- getString(R.string.repo_invalid),
- context
- )
- onContentReady()
- }
- return@execute
- }
-
- val pictureList = loadImages(directory, imageList)
-
- if (pictureList.isEmpty()) {
- handler.post {
- showPopUpMessage(
- getString(R.string.repo_empty),
- context
- )
- onContentReady()
- }
- } else {
- val picture = loadFullImage(imageList[0])
-
- handler.post {
- miniatures.setMiniatures(pictureList)
-
- if (isMainImageEmpty()) {
- wrapPictureIntoMainImage(picture)
- } else {
- appliedFilters.add(MainImageWrapper.getFilters())
- mainImage.value = MainImageWrapper.getImage()
- currentImageIndex.value = MainImageWrapper.getId()
- }
- onContentReady()
- }
- }
- } else {
- handler.post {
- showPopUpMessage(
- getString(R.string.no_internet),
- context
- )
- onContentReady()
- }
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- }
-
- // preview/fullscreen image managing
- fun isMainImageEmpty(): Boolean {
- return MainImageWrapper.isEmpty()
- }
-
- fun fullscreen(picture: Picture) {
- isContentReady.value = false
- AppState.screenState(ScreenType.FullscreenImage)
- setMainImage(picture)
- }
-
- fun setMainImage(picture: Picture) {
- if (MainImageWrapper.getId() == picture.id) {
- if (!isContentReady())
- onContentReady()
- return
- }
- isContentReady.value = false
-
- executor.execute {
- if (isInternetAvailable()) {
-
- val fullSizePicture = loadFullImage(picture.source)
- fullSizePicture.id = picture.id
-
- handler.post {
- wrapPictureIntoMainImage(fullSizePicture)
- onContentReady()
- }
- } else {
- handler.post {
- showPopUpMessage(
- "${getString(R.string.no_internet)}\n${getString(R.string.load_image_unavailable)}",
- context
- )
- wrapPictureIntoMainImage(picture)
- }
- }
- }
- }
-
- private fun onContentReady() {
- isContentReady.value = true
- isAppReady.value = true
- }
-
- private fun wrapPictureIntoMainImage(picture: Picture) {
- MainImageWrapper.wrapPicture(picture)
- MainImageWrapper.saveOrigin()
- mainImage.value = picture.image
- currentImageIndex.value = picture.id
- }
-
- fun swipeNext() {
- if (currentImageIndex.value == miniatures.size() - 1) {
- showPopUpMessage(
- getString(R.string.last_image),
- context
- )
- return
- }
-
- restoreFilters()
- setMainImage(miniatures.get(++currentImageIndex.value))
- }
-
- fun swipePrevious() {
- if (currentImageIndex.value == 0) {
- showPopUpMessage(
- getString(R.string.first_image),
- context
- )
- return
- }
-
- restoreFilters()
- setMainImage(miniatures.get(--currentImageIndex.value))
- }
-
- fun refresh() {
- executor.execute {
- if (isInternetAvailable()) {
- handler.post {
- clearCache(context)
- MainImageWrapper.clear()
- miniatures.clear()
- isContentReady.value = false
- initData()
- }
- } else {
- handler.post {
- showPopUpMessage(
- "${getString(R.string.no_internet)}\n${getString(R.string.refresh_unavailable)}",
- context
- )
- }
- }
- }
- }
-}
-
-private object MainImageWrapper {
- // origin image
- var origin: Bitmap? = null
- private set
-
- fun saveOrigin() {
- origin = copy(picture.value.image)
- }
-
- fun restore(): Bitmap {
-
- if (origin != null) {
- filtersSet.clear()
- picture.value.image = copy(origin!!)
- }
-
- return copy(picture.value.image)
- }
-
- // picture adapter
- private var picture = mutableStateOf(
- Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- )
-
- fun wrapPicture(picture: Picture) {
- this.picture.value = picture
- }
-
- fun setImage(bitmap: Bitmap) {
- picture.value.image = bitmap
- }
-
- fun isEmpty(): Boolean {
- return (picture.value.name == "")
- }
-
- fun clear() {
- picture.value = Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- }
-
- fun getName(): String {
- return picture.value.name
- }
-
- fun getImage(): Bitmap {
- return picture.value.image
- }
-
- fun getId(): Int {
- return picture.value.id
- }
-
- // applied filters
- private var filtersSet: MutableSet = LinkedHashSet()
-
- fun addFilter(filter: FilterType) {
- filtersSet.add(filter)
- }
-
- fun removeFilter(filter: FilterType) {
- filtersSet.remove(filter)
- }
-
- fun getFilters(): Set {
- return filtersSet
- }
-
- private fun copy(bitmap: Bitmap): Bitmap {
- return bitmap.copy(bitmap.config, false)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt
deleted file mode 100755
index 627b36ac..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package example.imageviewer.model
-
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import example.imageviewer.utils.cacheImage
-import example.imageviewer.utils.cacheImagePostfix
-import example.imageviewer.utils.scaleBitmapAspectRatio
-import example.imageviewer.utils.toPx
-import java.io.File
-import java.io.FileInputStream
-import java.io.InputStream
-import java.io.InputStreamReader
-import java.io.BufferedReader
-import java.lang.Exception
-import java.net.HttpURLConnection
-import java.net.URL
-import java.nio.charset.StandardCharsets
-
-fun loadFullImage(source: String): Picture {
- try {
- val url = URL(source)
- val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
- connection.connectTimeout = 5000
- connection.connect()
-
- val input: InputStream = connection.inputStream
- val bitmap: Bitmap? = BitmapFactory.decodeStream(input)
- if (bitmap != null) {
- return Picture(
- source = source,
- image = bitmap,
- name = getNameURL(source),
- width = bitmap.width,
- height = bitmap.height
- )
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-
- return Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-}
-
-fun loadImages(cachePath: String, list: List): MutableList {
- val result: MutableList = ArrayList()
-
- for (source in list) {
- val name = getNameURL(source)
- val path = cachePath + File.separator + name
-
- if (File(path + "info").exists()) {
- addCachedMiniature(filePath = path, outList = result)
- } else {
- addFreshMiniature(source = source, outList = result, path = cachePath)
- }
-
- result.last().id = result.size - 1
- }
-
- return result
-}
-
-private fun addFreshMiniature(
- source: String,
- outList: MutableList,
- path: String
-) {
- try {
- val url = URL(source)
- val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
- connection.connectTimeout = 5000
- connection.connect()
-
- val input: InputStream = connection.inputStream
- val result: Bitmap? = BitmapFactory.decodeStream(input)
-
- if (result != null) {
- val picture = Picture(
- source,
- getNameURL(source),
- scaleBitmapAspectRatio(result, 200, 164),
- result.width,
- result.height
- )
-
- outList.add(picture)
- cacheImage(path + getNameURL(source), picture)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-}
-
-private fun addCachedMiniature(
- filePath: String,
- outList: MutableList
-) {
- try {
- val read = BufferedReader(
- InputStreamReader(
- FileInputStream(filePath + cacheImagePostfix),
- StandardCharsets.UTF_8
- )
- )
-
- val source = read.readLine()
- val width = read.readLine().toInt()
- val height = read.readLine().toInt()
-
- read.close()
-
- val result: Bitmap? = BitmapFactory.decodeFile(filePath)
-
- if (result != null) {
- val picture = Picture(
- source,
- getNameURL(source),
- result,
- width,
- height
- )
- outList.add(picture)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-}
-
-private fun getNameURL(url: String): String {
- return url.substring(url.lastIndexOf('/') + 1, url.length)
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt
deleted file mode 100755
index 50a9f33b..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model
-
-import android.graphics.Bitmap
-
-actual data class Picture(
- var source: String = "",
- var name: String = "",
- var image: Bitmap,
- var width: Int = 0,
- var height: Int = 0,
- var id: Int = 0
-)
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
deleted file mode 100755
index aea27748..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package example.imageviewer.model.filtration
-
-import android.content.Context
-import android.graphics.Bitmap
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.utils.applyBlurFilter
-
-class BlurFilter(private val context: Context) : BitmapFilter {
-
- override fun apply(bitmap: Bitmap): Bitmap {
- return applyBlurFilter(bitmap, context)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt
deleted file mode 100755
index 2f31b010..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-
-import android.graphics.Bitmap
-import example.imageviewer.core.BitmapFilter
-
-class EmptyFilter : BitmapFilter {
-
- override fun apply(bitmap: Bitmap): Bitmap {
- return bitmap
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt
deleted file mode 100755
index 35f16ab3..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package example.imageviewer.model.filtration
-
-import android.content.Context
-import android.graphics.Bitmap
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.core.FilterType
-
-class FiltersManager(private val context: Context) {
-
- private var filtersMap: MutableMap = LinkedHashMap()
-
- fun clear() {
- filtersMap = LinkedHashMap()
- }
-
- fun add(filters: Collection) {
-
- for (filter in filters)
- add(filter)
- }
-
- fun add(filter: FilterType) {
-
- if (!filtersMap.containsKey(filter))
- filtersMap[filter] = getFilter(filter, context)
- }
-
- fun remove(filter: FilterType) {
- filtersMap.remove(filter)
- }
-
- fun contains(filter: FilterType): Boolean {
- return filtersMap.contains(filter)
- }
-
- fun applyFilters(bitmap: Bitmap): Bitmap {
-
- var result: Bitmap = bitmap
- for (filter in filtersMap) {
- result = filter.value.apply(result)
- }
-
- return result
- }
-}
-
-private fun getFilter(type: FilterType, context: Context): BitmapFilter {
-
- return when (type) {
- FilterType.GrayScale -> GrayScaleFilter()
- FilterType.Pixel -> PixelFilter()
- FilterType.Blur -> BlurFilter(context)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
deleted file mode 100755
index 5567048d..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-import android.graphics.Bitmap
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.utils.applyGrayScaleFilter
-
-class GrayScaleFilter : BitmapFilter {
-
- override fun apply(bitmap: Bitmap) : Bitmap {
- return applyGrayScaleFilter(bitmap)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
deleted file mode 100755
index a269b37f..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-import android.graphics.Bitmap
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.utils.applyPixelFilter
-
-class PixelFilter : BitmapFilter {
-
- override fun apply(bitmap: Bitmap): Bitmap {
- return applyPixelFilter(bitmap)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt
deleted file mode 100755
index 77cea5c2..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package example.imageviewer.style
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.painterResource
-import example.imageviewer.common.R
-
-@Composable
-fun icEmpty() = painterResource(R.drawable.empty)
-
-@Composable
-fun icBack() = painterResource(R.drawable.back)
-
-@Composable
-fun icRefresh() = painterResource(R.drawable.refresh)
-
-@Composable
-fun icDots() = painterResource(R.drawable.dots)
-
-@Composable
-fun icFilterGrayscaleOn() = painterResource(R.drawable.grayscale_on)
-
-@Composable
-fun icFilterGrayscaleOff() = painterResource(R.drawable.grayscale_off)
-
-@Composable
-fun icFilterPixelOn() = painterResource(R.drawable.pixel_on)
-
-@Composable
-fun icFilterPixelOff() = painterResource(R.drawable.pixel_off)
-
-@Composable
-fun icFilterBlurOn() = painterResource(R.drawable.blur_on)
-
-@Composable
-fun icFilterBlurOff() = painterResource(R.drawable.blur_off)
-
-@Composable
-fun icFilterUnknown() = painterResource(R.drawable.filter_unknown)
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt
deleted file mode 100755
index 7059938c..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package example.imageviewer.utils
-
-import android.content.Context
-import android.graphics.*
-import example.imageviewer.model.Picture
-import java.io.File
-import java.io.BufferedWriter
-import java.io.OutputStreamWriter
-import java.io.FileOutputStream
-import java.io.IOException
-import java.nio.charset.StandardCharsets
-
-val cacheImagePostfix = "info"
-
-fun cacheImage(path: String, picture: Picture) {
- try {
- FileOutputStream(path).use { out ->
- picture.image.compress(Bitmap.CompressFormat.PNG, 100, out)
- }
-
- val bw =
- BufferedWriter(
- OutputStreamWriter(
- FileOutputStream(path + cacheImagePostfix), StandardCharsets.UTF_8
- )
- )
-
- bw.write(picture.source)
- bw.write("\r\n${picture.width}")
- bw.write("\r\n${picture.height}")
- bw.close()
-
- } catch (e: IOException) {
- e.printStackTrace()
- }
-}
-
-fun clearCache(context: Context) {
-
- val directory = File(context.cacheDir.absolutePath)
-
- val files: Array? = directory.listFiles()
-
- if (files != null) {
- for (file in files) {
- if (file.isDirectory)
- continue
-
- file.delete()
- }
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Coroutines.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Coroutines.kt
deleted file mode 100644
index ab006ef1..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Coroutines.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package example.imageviewer.utils
-
-import kotlinx.coroutines.CoroutineScope
-import kotlin.coroutines.CoroutineContext
-
-actual fun runBlocking(
- context: CoroutineContext,
- block: suspend CoroutineScope.() -> T
-): T = kotlinx.coroutines.runBlocking(context, block)
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
deleted file mode 100755
index 32e234da..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-package example.imageviewer.utils
-
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.*
-import android.renderscript.Allocation
-import android.renderscript.Element
-import android.renderscript.RenderScript
-import android.renderscript.ScriptIntrinsicBlur
-import androidx.compose.ui.layout.ContentScale
-import kotlin.math.pow
-import kotlin.math.roundToInt
-import example.imageviewer.view.DragHandler
-
-fun scaleBitmapAspectRatio(
- bitmap: Bitmap,
- width: Int,
- height: Int,
- filter: Boolean = false
-): Bitmap {
- val boundW: Float = width.toFloat()
- val boundH: Float = height.toFloat()
-
- val ratioX: Float = boundW / bitmap.width
- val ratioY: Float = boundH / bitmap.height
- val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
-
- val resultH = (bitmap.height * ratio).toInt()
- val resultW = (bitmap.width * ratio).toInt()
-
- return Bitmap.createScaledBitmap(bitmap, resultW, resultH, filter)
-}
-
-fun getDisplayBounds(bitmap: Bitmap): Rect {
-
- val boundW: Float = displayWidth().toFloat()
- val boundH: Float = displayHeight().toFloat()
-
- val ratioX: Float = bitmap.width / boundW
- val ratioY: Float = bitmap.height / boundH
- val ratio: Float = if (ratioX > ratioY) ratioX else ratioY
- val resultW = (boundW * ratio)
- val resultH = (boundH * ratio)
-
- return Rect(0, 0, resultW.toInt(), resultH.toInt())
-}
-
-fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
-
- val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
-
- val canvas = Canvas(result)
-
- val colorMatrix = ColorMatrix()
- colorMatrix.setSaturation(0f)
-
- val paint = Paint()
- paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
-
- canvas.drawBitmap(result, 0f, 0f, paint)
-
- return result
-}
-
-fun applyPixelFilter(bitmap: Bitmap): Bitmap {
-
- var result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
- val w: Int = bitmap.width
- val h: Int = bitmap.height
- result = scaleBitmapAspectRatio(result, w / 20, h / 20)
- result = scaleBitmapAspectRatio(result, w, h)
-
- return result
-}
-
-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(25f)
- theIntrinsic.setInput(tmpIn)
- theIntrinsic.forEach(tmpOut)
-
- tmpOut.copyTo(result)
-
- return result
-}
-
-fun adjustImageScale(bitmap: Bitmap): ContentScale {
- val bitmapRatio = (10 * bitmap.width.toFloat() / bitmap.height).toInt()
- val displayRatio = (10 * displayWidth().toFloat() / displayHeight()).toInt()
-
- if (displayRatio > bitmapRatio) {
- return ContentScale.FillHeight
- }
- return ContentScale.FillWidth
-}
-
-fun toPx(dp: Int): Int {
- return (dp * Resources.getSystem().displayMetrics.density).toInt()
-}
-
-fun toDp(px: Int): Int {
- return (px / Resources.getSystem().displayMetrics.density).toInt()
-}
-
-fun displayWidth(): Int {
- return Resources.getSystem().displayMetrics.widthPixels
-}
-
-fun displayHeight(): Int {
- return Resources.getSystem().displayMetrics.heightPixels
-}
-
-fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap {
- val crop = cropBitmapByBounds(
- bitmap,
- getDisplayBounds(bitmap),
- scale,
- drag
- )
- return Bitmap.createBitmap(
- bitmap,
- crop.left,
- crop.top,
- crop.right - crop.left,
- crop.bottom - crop.top
- )
-}
-
-fun cropBitmapByBounds(
- bitmap: Bitmap,
- bounds: Rect,
- scaleFactor: Float,
- drag: DragHandler
-): Rect {
- if (scaleFactor <= 1f)
- return Rect(0, 0, bitmap.width, bitmap.height)
-
- var scale = scaleFactor.toDouble().pow(1.4)
-
- var boundW = (bounds.width() / scale).roundToInt()
- var boundH = (bounds.height() / scale).roundToInt()
-
- scale *= displayWidth() / bounds.width().toDouble()
-
- val offsetX = drag.getAmount().x / scale
- val offsetY = drag.getAmount().y / scale
-
- if (boundW > bitmap.width) {
- boundW = bitmap.width
- }
- if (boundH > bitmap.height) {
- boundH = bitmap.height
- }
-
- val invisibleW = bitmap.width - boundW
- var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat()
-
- if (leftOffset > invisibleW) {
- leftOffset = invisibleW.toFloat()
- drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
- }
- if (leftOffset < 0) {
- drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
- leftOffset = 0f
- }
-
- val invisibleH = bitmap.height - boundH
- var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat()
-
- if (topOffset > invisibleH) {
- topOffset = invisibleH.toFloat()
- drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
- }
- if (topOffset < 0) {
- drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
- topOffset = 0f
- }
-
- return Rect(
- leftOffset.toInt(),
- topOffset.toInt(),
- (leftOffset + boundW).toInt(),
- (topOffset + boundH).toInt()
- )
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt
deleted file mode 100755
index dacce3b7..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package example.imageviewer.view
-
-import android.content.Context
-import android.widget.Toast
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Surface
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import example.imageviewer.model.AppState
-import example.imageviewer.model.ScreenType
-import example.imageviewer.model.ContentState
-import example.imageviewer.style.Gray
-
-@Composable
-fun AppUI(content: ContentState) {
-
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = Gray
- ) {
- when (AppState.screenState()) {
- ScreenType.MainScreen -> {
- MainScreen(content)
- }
- ScreenType.FullscreenImage -> {
- FullscreenImage(content)
- }
- }
- }
-}
-
-fun showPopUpMessage(text: String, context: Context) {
- Toast.makeText(
- context,
- text,
- Toast.LENGTH_SHORT
- ).show()
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt
deleted file mode 100644
index 1c0e7d73..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-package example.imageviewer.view
-
-import android.graphics.Bitmap
-import android.graphics.Rect
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-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.asImageBitmap
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.unit.dp
-import example.imageviewer.core.FilterType
-import example.imageviewer.model.AppState
-import example.imageviewer.model.ContentState
-import example.imageviewer.model.ScreenType
-import example.imageviewer.style.DarkGray
-import example.imageviewer.style.DarkGreen
-import example.imageviewer.style.Foreground
-import example.imageviewer.style.MiniatureColor
-import example.imageviewer.style.Transparent
-import example.imageviewer.style.icBack
-import example.imageviewer.style.icFilterBlurOff
-import example.imageviewer.style.icFilterBlurOn
-import example.imageviewer.style.icFilterGrayscaleOff
-import example.imageviewer.style.icFilterGrayscaleOn
-import example.imageviewer.style.icFilterPixelOff
-import example.imageviewer.style.icFilterPixelOn
-import example.imageviewer.utils.adjustImageScale
-import example.imageviewer.utils.cropBitmapByScale
-import example.imageviewer.utils.displayWidth
-import example.imageviewer.utils.getDisplayBounds
-import kotlin.math.abs
-import kotlin.math.pow
-import kotlin.math.roundToInt
-
-@Composable
-fun FullscreenImage(
- content: ContentState
-) {
- Column {
- ToolBar(content.getSelectedImageName(), content)
- Image(content)
- }
- if (!content.isContentReady()) {
- LoadingScreen()
- }
-}
-
-@Composable
-fun ToolBar(
- text: String,
- content: ContentState
-) {
- val scrollState = rememberScrollState()
- Surface(color = MiniatureColor, modifier = Modifier.height(44.dp)) {
- Row(modifier = Modifier.padding(end = 30.dp)) {
- Surface(
- color = Transparent,
- modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
- shape = CircleShape
- ) {
- Clickable(
- onClick = {
- if (content.isContentReady()) {
- content.restoreMainImage()
- AppState.screenState(ScreenType.MainScreen)
- }
- }) {
- Image(
- icBack(),
- contentDescription = null,
- modifier = Modifier.size(38.dp)
- )
- }
- }
- Text(
- text,
- color = Foreground,
- maxLines = 1,
- modifier = Modifier.padding(start = 30.dp).weight(1f)
- .align(Alignment.CenterVertically),
- style = MaterialTheme.typography.body1
- )
-
- Surface(
- color = Color(255, 255, 255, 40),
- modifier = Modifier.size(154.dp, 38.dp)
- .align(Alignment.CenterVertically),
- shape = CircleShape
- ) {
- Row(Modifier.horizontalScroll(scrollState)) {
- for (type in FilterType.values()) {
- FilterButton(content, type)
- }
- }
- }
- }
- }
-}
-
-@Composable
-fun FilterButton(
- content: ContentState,
- type: FilterType,
- modifier: Modifier = Modifier.size(38.dp)
-) {
- Box(
- modifier = Modifier.background(color = Transparent).clip(CircleShape)
- ) {
- Clickable(
- onClick = { content.toggleFilter(type) }
- ) {
- Image(
- getFilterImage(type = type, content = content),
- contentDescription = null,
- modifier
- )
- }
- }
-
- Spacer(Modifier.width(20.dp))
-}
-
-@Composable
-fun getFilterImage(type: FilterType, content: ContentState): Painter {
- return when (type) {
- FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
- FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
- FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff()
- }
-}
-
-@Composable
-fun Image(content: ContentState) {
- val drag = remember { DragHandler() }
- val scale = remember { ScaleHandler() }
-
- Surface(
- color = DarkGray,
- modifier = Modifier.fillMaxSize()
- ) {
- Draggable(dragHandler = drag, modifier = Modifier.fillMaxSize()) {
- Scalable(onScale = scale, modifier = Modifier.fillMaxSize()) {
- val bitmap = imageByGesture(content, scale, drag)
- Image(
- bitmap = bitmap.asImageBitmap(),
- contentDescription = null,
- contentScale = adjustImageScale(bitmap)
- )
- }
- }
- }
-}
-
-@Composable
-fun imageByGesture(
- content: ContentState,
- scale: ScaleHandler,
- drag: DragHandler
-): Bitmap {
- val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag)
-
- if (scale.factor.value > 1f)
- return bitmap
-
- if (abs(drag.getDistance().x) > displayWidth() / 10) {
- if (drag.getDistance().x < 0) {
- content.swipeNext()
- } else {
- content.swipePrevious()
- }
- drag.cancel()
- }
-
- return bitmap
-}
diff --git a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt b/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt
deleted file mode 100755
index 5509e280..00000000
--- a/experimental/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt
+++ /dev/null
@@ -1,218 +0,0 @@
-package example.imageviewer.view
-
-import android.content.res.Configuration
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Card
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.material.Divider
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.graphics.painter.BitmapPainter
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.unit.dp
-import example.imageviewer.common.R
-import example.imageviewer.model.AppState
-import example.imageviewer.model.ContentState
-import example.imageviewer.model.Picture
-import example.imageviewer.model.ScreenType
-import example.imageviewer.style.DarkGray
-import example.imageviewer.style.DarkGreen
-import example.imageviewer.style.Foreground
-import example.imageviewer.style.LightGray
-import example.imageviewer.style.MiniatureColor
-import example.imageviewer.style.Transparent
-import example.imageviewer.style.icDots
-import example.imageviewer.style.icEmpty
-import example.imageviewer.style.icRefresh
-
-@Composable
-fun MainScreen(content: ContentState) {
- Column {
- TopContent(content)
- ScrollableArea(content)
- }
- if (!content.isContentReady()) {
- LoadingScreen(content.getString(R.string.loading))
- }
-}
-
-@Composable
-fun TopContent(content: ContentState) {
- TitleBar(text = content.getString(R.string.app_name), content = content)
- if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
- PreviewImage(content)
- Spacer(modifier = Modifier.height(10.dp))
- Divider()
- }
- Spacer(modifier = Modifier.height(5.dp))
-}
-
-@Composable
-fun TitleBar(text: String, content: ContentState) {
- TopAppBar(
- backgroundColor = DarkGreen,
- title = {
- Row(Modifier.height(50.dp)) {
- Text(
- text,
- color = Foreground,
- modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
- )
- Surface(
- color = Transparent,
- modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
- shape = CircleShape
- ) {
- Clickable(
- onClick = {
- if (content.isContentReady()) {
- content.refresh()
- }
- }
- ) {
- Image(
- icRefresh(),
- contentDescription = null,
- modifier = Modifier.size(35.dp)
- )
- }
- }
- }
- })
-}
-
-@Composable
-fun PreviewImage(content: ContentState) {
- Clickable(onClick = {
- AppState.screenState(ScreenType.FullscreenImage)
- }) {
- Card(
- backgroundColor = DarkGray,
- modifier = Modifier.height(250.dp),
- shape = RectangleShape,
- elevation = 1.dp
- ) {
- Image(
- if (content.isMainImageEmpty()) {
- icEmpty()
- } else {
- BitmapPainter(content.getSelectedImage().asImageBitmap())
- },
- contentDescription = null,
- modifier = Modifier
- .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
- contentScale = ContentScale.Fit
- )
- }
- }
-}
-
-@Composable
-fun Miniature(
- picture: Picture,
- content: ContentState
-) {
- Card(
- backgroundColor = MiniatureColor,
- modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
- .fillMaxWidth()
- .clickable {
- content.setMainImage(picture)
- },
- shape = RectangleShape,
- elevation = 2.dp
- ) {
- Row(modifier = Modifier.padding(end = 30.dp)) {
- Clickable(
- onClick = {
- content.fullscreen(picture)
- }
- ) {
- Image(
- picture.image.asImageBitmap(),
- contentDescription = null,
- modifier = Modifier.height(70.dp)
- .width(90.dp)
- .padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp),
- contentScale = ContentScale.Crop
- )
- }
- Text(
- text = picture.name,
- color = Foreground,
- modifier = Modifier.weight(1f).align(Alignment.CenterVertically).padding(start = 16.dp),
- style = MaterialTheme.typography.body1
- )
-
- Clickable(
- modifier = Modifier.height(70.dp)
- .width(30.dp),
- onClick = {
- showPopUpMessage(
- "${content.getString(R.string.picture)} " +
- "${picture.name} \n" +
- "${content.getString(R.string.size)} " +
- "${picture.width}x${picture.height} " +
- "${content.getString(R.string.pixels)}",
- content.getContext()
- )
- }
- ) {
- Image(
- icDots(),
- contentDescription = null,
- modifier = Modifier.height(70.dp)
- .width(30.dp)
- .padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp),
- contentScale = ContentScale.FillHeight
- )
- }
- }
- }
-}
-
-@Composable
-fun ScrollableArea(content: ContentState) {
- var index = 1
- val scrollState = rememberScrollState()
- Column(Modifier.verticalScroll(scrollState)) {
- for (picture in content.getMiniatures()) {
- Miniature(
- picture = picture,
- content = content
- )
- Spacer(modifier = Modifier.height(5.dp))
- index++
- }
- }
-}
-
-@Composable
-fun Divider() {
- Divider(
- color = LightGray,
- modifier = Modifier.padding(start = 10.dp, end = 10.dp)
- )
-}
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/filter_unknown.png b/experimental/examples/imageviewer/common/src/androidMain/res/drawable/filter_unknown.png
deleted file mode 100755
index 9193c3f3..00000000
Binary files a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/filter_unknown.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt
deleted file mode 100755
index f31ab9ca..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package example.imageviewer.core
-
-class EventLocker {
-
- private var value: Boolean = false
-
- fun lock() {
- value = false
- }
-
- fun unlock() {
- value = true
- }
-
- fun isLocked(): Boolean {
- return value
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt
deleted file mode 100755
index df6cd11f..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package example.imageviewer.core
-
-interface Repository {
- fun get() : T
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt
deleted file mode 100755
index 14178fa2..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-// READ ME FIRST!
-//
-// Code in this file is shared between the Android and Desktop JVM targets.
-// Kotlin's hierarchical multiplatform projects currently
-// don't support sharing code depending on JVM declarations.
-//
-// You can follow the progress for HMPP JVM & Android intermediate source sets here:
-// https://youtrack.jetbrains.com/issue/KT-42466
-//
-// The workaround used here to access JVM libraries causes IntelliJ IDEA to not
-// resolve symbols in this file properly.
-//
-// Resolution errors in your IDE do not indicate a problem with your setup.
-
-
-package example.imageviewer.model
-
-import example.imageviewer.core.Repository
-import example.imageviewer.utils.ktorHttpClient
-import example.imageviewer.utils.runBlocking
-import io.ktor.client.request.*
-
-class ImageRepository(
- private val httpsURL: String
-) : Repository> {
-
- override fun get(): MutableList {
- return runBlocking {
- val content = ktorHttpClient.get(httpsURL)
- content.lines().toMutableList()
- }
- }
-}
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt
deleted file mode 100755
index 4daaca39..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-// READ ME FIRST!
-//
-// Code in this file is shared between the Android and Desktop JVM targets.
-// Kotlin's hierarchical multiplatform projects currently
-// don't support sharing code depending on JVM declarations.
-//
-// You can follow the progress for HMPP JVM & Android intermediate source sets here:
-// https://youtrack.jetbrains.com/issue/KT-42466
-//
-// The workaround used here to access JVM libraries causes IntelliJ IDEA to not
-// resolve symbols in this file properly.
-//
-// Resolution errors in your IDE do not indicate a problem with your setup.
-
-package example.imageviewer.model
-
-expect class Picture
-
-class Miniatures(
- private var list: List = emptyList()
-) {
- fun get(index: Int): Picture {
- return list[index]
- }
-
- fun getMiniatures(): List {
- return list.toList()
- }
-
- fun setMiniatures(list: List) {
- this.list = list.toList()
- }
-
- fun size(): Int {
- return list.size
- }
-
- fun clear() {
- list = emptyList()
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt
deleted file mode 100755
index 8e38a79e..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package example.imageviewer.model
-
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-
-enum class ScreenType {
- MainScreen, FullscreenImage
-}
-
-object AppState {
- private var screen: MutableState
- init {
- screen = mutableStateOf(ScreenType.MainScreen)
- }
-
- fun screenState() : ScreenType {
- return screen.value
- }
-
- fun screenState(state: ScreenType) {
- screen.value = state
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
deleted file mode 100755
index ca4b822f..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package example.imageviewer.style
-
-import androidx.compose.ui.graphics.Color
-
-val DarkGreen = Color(16, 139, 102)
-val Gray = Color.DarkGray
-val LightGray = Color(100, 100, 100)
-val DarkGray = Color(32, 32, 32)
-val PreviewImageAreaHoverColor = Color(45, 45, 45)
-val ToastBackground = Color(23, 23, 23)
-val MiniatureColor = Color(50, 50, 50)
-val MiniatureHoverColor = Color(55, 55, 55)
-val Foreground = Color(210, 210, 210)
-val TranslucentBlack = Color(0, 0, 0, 60)
-val TranslucentWhite = Color(255, 255, 255, 20)
-val Transparent = Color.Transparent
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Coroutines.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Coroutines.kt
deleted file mode 100644
index d35b0954..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Coroutines.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package example.imageviewer.utils
-
-import kotlinx.coroutines.CoroutineScope
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-
-expect fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt
deleted file mode 100755
index c2d5a23b..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-// READ ME FIRST!
-//
-// Code in this file is shared between the Android and Desktop JVM targets.
-// Kotlin's hierarchical multiplatform projects currently
-// don't support sharing code depending on JVM declarations.
-//
-// You can follow the progress for HMPP JVM & Android intermediate source sets here:
-// https://youtrack.jetbrains.com/issue/KT-42466
-//
-// The workaround used here to access JVM libraries causes IntelliJ IDEA to not
-// resolve symbols in this file properly.
-//
-// Resolution errors in your IDE do not indicate a problem with your setup.
-
-package example.imageviewer.utils
-
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.async
-
-//import java.net.InetAddress
-
-fun isInternetAvailable(): Boolean {
- return runBlocking {
- try {
- ktorHttpClient.head("http://google.com")
- true
- } catch (e: Exception) {
- println(e.message)
- false
- }
- }
-}
-
-val ktorHttpClient = HttpClient {}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt
deleted file mode 100755
index 8eae0782..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.runtime.Composable
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.ui.Modifier
-
-@Composable
-fun Clickable(
- modifier: Modifier = Modifier,
- onClick: (() -> Unit)? = null,
- children: @Composable () -> Unit = { }
-) {
- Box(
- modifier = modifier.clickable {
- onClick?.invoke()
- }
- ) {
- children()
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt
deleted file mode 100755
index eabbadc4..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.foundation.gestures.detectDragGestures
-import androidx.compose.material.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import example.imageviewer.core.EventLocker
-import example.imageviewer.style.Transparent
-
-@Composable
-fun Draggable(
- dragHandler: DragHandler,
- modifier: Modifier = Modifier,
- onUpdate: (() -> Unit)? = null,
- children: @Composable() () -> Unit
-) {
- Surface(
- color = Transparent,
- modifier = modifier.pointerInput(Unit) {
- detectDragGestures(
- onDragStart = { dragHandler.reset() },
- onDragEnd = { dragHandler.reset() },
- onDragCancel = { dragHandler.cancel() },
- ) { change, dragAmount ->
- dragHandler.drag(dragAmount)
- onUpdate?.invoke()
- change.consume()
- }
- }
- ) {
- children()
- }
-}
-
-class DragHandler {
-
- private val amount = mutableStateOf(Point(0f, 0f))
- private val distance = mutableStateOf(Point(0f, 0f))
- private val locker: EventLocker = EventLocker()
-
- fun getAmount(): Point {
- return amount.value
- }
-
- fun getDistance(): Point {
- return distance.value
- }
-
- fun reset() {
- distance.value = Point(Offset.Zero)
- locker.unlock()
- }
-
- fun cancel() {
- distance.value = Point(Offset.Zero)
- locker.lock()
- }
-
- fun drag(dragDistance: Offset) {
- if (locker.isLocked()) {
- val dx = dragDistance.x
- val dy = dragDistance.y
-
- distance.value = Point(distance.value.x + dx, distance.value.y + dy)
- amount.value = Point(amount.value.x + dx, amount.value.y + dy)
- }
- }
-}
-
-class Point {
- var x: Float = 0f
- var y: Float = 0f
- constructor(x: Float, y: Float) {
- this.x = x
- this.y = y
- }
- constructor(point: Offset) {
- this.x = point.x
- this.y = point.y
- }
- fun setAttr(x: Float, y: Float) {
- this.x = x
- this.y = y
- }
-}
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt
deleted file mode 100755
index ef9887c4..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.gestures.detectTransformGestures
-import androidx.compose.material.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.pointerInput
-import example.imageviewer.style.Transparent
-
-@Composable
-fun Scalable(
- onScale: ScaleHandler,
- modifier: Modifier = Modifier,
- children: @Composable() () -> Unit
-) {
- Surface(
- color = Transparent,
- modifier = modifier.pointerInput(Unit) {
- detectTapGestures(onDoubleTap = { onScale.reset() })
- detectTransformGestures { _, _, zoom, _ ->
- onScale.onScale(zoom)
- }
- },
- ) {
- children()
- }
-}
-
-class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) {
- val factor = mutableStateOf(1f)
-
- fun reset() {
- if (factor.value > minFactor)
- factor.value = minFactor
- }
-
- fun onScale(scaleFactor: Float): Float {
- factor.value += scaleFactor - 1f
-
- if (maxFactor < factor.value) factor.value = maxFactor
- if (minFactor > factor.value) factor.value = minFactor
-
- return scaleFactor
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/SplashUI.kt b/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/SplashUI.kt
deleted file mode 100644
index 544121d2..00000000
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/SplashUI.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-import example.imageviewer.style.DarkGray
-
-@Composable
-fun SplashUI() {
- Box(Modifier.fillMaxSize().background(DarkGray)) {
- Text(
- // TODO implement common resources
- "Image Viewer",
- Modifier.align(Alignment.Center),
- color = Color.White,
- fontWeight = FontWeight.Bold,
- fontSize = 100.sp
- )
- }
-}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt
deleted file mode 100755
index b8dcdbae..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package example.imageviewer.core
-
-import java.awt.image.BufferedImage
-
-interface BitmapFilter {
- fun apply(bitmap: BufferedImage) : BufferedImage
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt
deleted file mode 100644
index 36e0983d..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt
+++ /dev/null
@@ -1,362 +0,0 @@
-package example.imageviewer.model
-
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.window.WindowState
-import androidx.compose.ui.graphics.ImageBitmap
-import androidx.compose.ui.graphics.toComposeImageBitmap
-import example.imageviewer.ResString
-import example.imageviewer.core.FilterType
-import example.imageviewer.model.filtration.FiltersManager
-import example.imageviewer.utils.cacheImagePath
-import example.imageviewer.utils.clearCache
-import example.imageviewer.utils.isInternetAvailable
-import example.imageviewer.view.showPopUpMessage
-import example.imageviewer.view.DragHandler
-import example.imageviewer.view.ScaleHandler
-import example.imageviewer.utils.cropBitmapByScale
-import example.imageviewer.utils.toByteArray
-import java.awt.image.BufferedImage
-import java.io.File
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.CoroutineScope
-import org.jetbrains.skia.Image
-
-object ContentState {
- val drag = DragHandler()
- val scale = ScaleHandler()
- lateinit var windowState: WindowState
- private lateinit var repository: ImageRepository
- private lateinit var uriRepository: String
- val scope = CoroutineScope(Dispatchers.IO)
-
- fun applyContent(state: WindowState, uriRepository: String): ContentState {
- windowState = state
- if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
- return this
- }
- this.uriRepository = uriRepository
- repository = ImageRepository(uriRepository)
- isContentReady.value = false
-
- initData()
-
- return this
- }
-
- private val isAppReady = mutableStateOf(false)
- fun isAppReady(): Boolean {
- return isAppReady.value
- }
-
- private val isContentReady = mutableStateOf(false)
- fun isContentReady(): Boolean {
- return isContentReady.value
- }
-
- // drawable content
- private val mainImage = mutableStateOf(BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
- private val currentImageIndex = mutableStateOf(0)
- private val miniatures = Miniatures()
-
- fun getMiniatures(): List {
- return miniatures.getMiniatures()
- }
-
- fun getSelectedImage(): ImageBitmap {
- return MainImageWrapper.mainImageAsImageBitmap.value
- }
-
- fun getSelectedImageName(): String {
- return MainImageWrapper.getName()
- }
-
- // filters managing
- private val appliedFilters = FiltersManager()
- private val filterUIState: MutableMap> = LinkedHashMap()
-
- private fun toggleFilterState(filter: FilterType) {
- if (!filterUIState.containsKey(filter)) {
- filterUIState[filter] = mutableStateOf(true)
- } else {
- val value = filterUIState[filter]!!.value
- filterUIState[filter]!!.value = !value
- }
- }
-
- fun toggleFilter(filter: FilterType) {
- if (containsFilter(filter)) {
- removeFilter(filter)
- } else {
- addFilter(filter)
- }
-
- toggleFilterState(filter)
-
- var bitmap = MainImageWrapper.origin
-
- if (bitmap != null) {
- bitmap = appliedFilters.applyFilters(bitmap)
- MainImageWrapper.setImage(bitmap)
- mainImage.value = bitmap
- updateMainImage()
- }
- }
-
- private fun addFilter(filter: FilterType) {
- appliedFilters.add(filter)
- MainImageWrapper.addFilter(filter)
- }
-
- private fun removeFilter(filter: FilterType) {
- appliedFilters.remove(filter)
- MainImageWrapper.removeFilter(filter)
- }
-
- private fun containsFilter(type: FilterType): Boolean {
- return appliedFilters.contains(type)
- }
-
- fun isFilterEnabled(type: FilterType): Boolean {
- if (!filterUIState.containsKey(type)) {
- filterUIState[type] = mutableStateOf(false)
- }
- return filterUIState[type]!!.value
- }
-
- private fun restoreFilters(): BufferedImage {
- filterUIState.clear()
- appliedFilters.clear()
- return MainImageWrapper.restore()
- }
-
- fun restoreMainImage() {
- mainImage.value = restoreFilters()
- }
-
- // application content initialization
- private fun initData() {
- if (isContentReady.value)
- return
-
- val directory = File(cacheImagePath)
- if (!directory.exists()) {
- directory.mkdir()
- }
-
- scope.launch(Dispatchers.IO) {
- try {
- if (isInternetAvailable()) {
- val imageList = repository.get()
-
- if (imageList.isEmpty()) {
- showPopUpMessage(
- ResString.repoInvalid
- )
- onContentReady()
- } else {
- val pictureList = loadImages(cacheImagePath, imageList)
-
- if (pictureList.isEmpty()) {
- showPopUpMessage(
- ResString.repoEmpty
- )
- onContentReady()
- } else {
- val picture = loadFullImage(imageList[0])
- miniatures.setMiniatures(pictureList)
- if (isMainImageEmpty()) {
- wrapPictureIntoMainImage(picture)
- } else {
- appliedFilters.add(MainImageWrapper.getFilters())
- currentImageIndex.value = MainImageWrapper.getId()
- }
- onContentReady()
- }
- }
- } else {
- showPopUpMessage(
- ResString.noInternet
- )
- onContentReady()
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- }
-
- // preview/fullscreen image managing
- fun isMainImageEmpty(): Boolean {
- return MainImageWrapper.isEmpty()
- }
-
- fun fullscreen(picture: Picture) {
- isContentReady.value = false
- AppState.screenState(ScreenType.FullscreenImage)
- setMainImage(picture)
- }
-
- fun setMainImage(picture: Picture) {
- if (MainImageWrapper.getId() == picture.id) {
- if (!isContentReady()) {
- onContentReady()
- }
- return
- }
- isContentReady.value = false
-
- scope.launch(Dispatchers.IO) {
- scale.reset()
- if (isInternetAvailable()) {
- val fullSizePicture = loadFullImage(picture.source)
- fullSizePicture.id = picture.id
- wrapPictureIntoMainImage(fullSizePicture)
- } else {
- showPopUpMessage(
- "${ResString.noInternet}\n${ResString.loadImageUnavailable}"
- )
- wrapPictureIntoMainImage(picture)
- }
- onContentReady()
- }
- }
-
- private fun onContentReady() {
- isContentReady.value = true
- isAppReady.value = true
- }
-
- private fun wrapPictureIntoMainImage(picture: Picture) {
- MainImageWrapper.wrapPicture(picture)
- MainImageWrapper.saveOrigin()
- mainImage.value = picture.image
- currentImageIndex.value = picture.id
- updateMainImage()
- }
-
- fun updateMainImage() {
- MainImageWrapper.mainImageAsImageBitmap.value = Image.makeFromEncoded(
- toByteArray(
- cropBitmapByScale(
- mainImage.value,
- windowState.size,
- scale.factor.value,
- drag
- )
- )
- ).toComposeImageBitmap()
- }
-
- fun swipeNext() {
- if (currentImageIndex.value == miniatures.size() - 1) {
- showPopUpMessage(ResString.lastImage)
- return
- }
-
- restoreFilters()
- setMainImage(miniatures.get(++currentImageIndex.value))
- }
-
- fun swipePrevious() {
- if (currentImageIndex.value == 0) {
- showPopUpMessage(ResString.firstImage)
- return
- }
-
- restoreFilters()
- setMainImage(miniatures.get(--currentImageIndex.value))
- }
-
- fun refresh() {
- scope.launch(Dispatchers.IO) {
- if (isInternetAvailable()) {
- clearCache()
- MainImageWrapper.clear()
- miniatures.clear()
- isContentReady.value = false
- initData()
- } else {
- showPopUpMessage(
- "${ResString.noInternet}\n${ResString.refreshUnavailable}"
- )
- }
- }
- }
-}
-
-private object MainImageWrapper {
- // origin image
- var origin: BufferedImage? = null
- private set
-
- fun saveOrigin() {
- origin = copy(picture.value.image)
- }
-
- fun restore(): BufferedImage {
- if (origin != null) {
- picture.value.image = copy(origin!!)
- filtersSet.clear()
- }
- return copy(picture.value.image)
- }
-
- var mainImageAsImageBitmap = mutableStateOf(ImageBitmap(1, 1))
-
- // picture adapter
- private var picture = mutableStateOf(
- Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
- )
-
- fun wrapPicture(picture: Picture) {
- this.picture.value = picture
- }
-
- fun setImage(bitmap: BufferedImage) {
- picture.value.image = bitmap
- }
-
- fun isEmpty(): Boolean {
- return (picture.value.name == "")
- }
-
- fun clear() {
- picture.value = Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
- }
-
- fun getName(): String {
- return picture.value.name
- }
-
- fun getImage(): BufferedImage {
- return picture.value.image
- }
-
- fun getId(): Int {
- return picture.value.id
- }
-
- // applied filters
- private var filtersSet: MutableSet = LinkedHashSet()
-
- fun addFilter(filter: FilterType) {
- filtersSet.add(filter)
- }
-
- fun removeFilter(filter: FilterType) {
- filtersSet.remove(filter)
- }
-
- fun getFilters(): Set {
- return filtersSet
- }
-
- private fun copy(bitmap: BufferedImage) : BufferedImage {
- val result = BufferedImage(bitmap.width, bitmap.height, bitmap.type)
- val graphics = result.createGraphics()
- graphics.drawImage(bitmap, 0, 0, result.width, result.height, null)
- return result
- }
-}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt
deleted file mode 100755
index 5b02b755..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-package example.imageviewer.model
-
-import java.awt.image.BufferedImage
-import example.imageviewer.utils.cacheImage
-import example.imageviewer.utils.cacheImagePostfix
-import example.imageviewer.utils.scaleBitmapAspectRatio
-import java.io.File
-import java.io.FileInputStream
-import java.io.InputStream
-import java.io.InputStreamReader
-import java.io.BufferedReader
-import javax.imageio.ImageIO
-import java.lang.Exception
-import java.net.HttpURLConnection
-import java.net.URL
-import java.nio.charset.StandardCharsets
-
-fun loadFullImage(source: String): Picture {
- try {
- val url = URL(source)
- val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
- connection.connectTimeout = 5000
- connection.connect()
-
- val input: InputStream = connection.inputStream
- val bitmap: BufferedImage? = ImageIO.read(input)
- if (bitmap != null) {
- return Picture(
- source = source,
- image = bitmap,
- name = getNameURL(source),
- width = bitmap.width,
- height = bitmap.height
- )
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-
- return Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
-}
-
-fun loadImages(cachePath: String, list: List): MutableList {
- val result: MutableList = ArrayList()
-
- for (source in list) {
- val name = getNameURL(source)
- val path = cachePath + File.separator + name
-
- if (File(path + "info").exists()) {
- addCachedMiniature(filePath = path, outList = result)
- } else {
- addFreshMiniature(source = source, outList = result, path = cachePath)
- }
-
- result.last().id = result.size - 1
- }
-
- return result
-}
-
-private fun addFreshMiniature(
- source: String,
- outList: MutableList,
- path: String
-) {
- try {
- val url = URL(source)
- val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
- connection.connectTimeout = 5000
- connection.connect()
-
- val input: InputStream = connection.inputStream
- val result: BufferedImage? = ImageIO.read(input)
-
- if (result != null) {
- val picture = Picture(
- source,
- getNameURL(source),
- scaleBitmapAspectRatio(result, 200, 164),
- result.width,
- result.height
- )
-
- outList.add(picture)
- cacheImage(path + getNameURL(source), picture)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-}
-
-private fun addCachedMiniature(
- filePath: String,
- outList: MutableList
-) {
- try {
- val read = BufferedReader(
- InputStreamReader(
- FileInputStream(filePath + cacheImagePostfix),
- StandardCharsets.UTF_8
- )
- )
-
- val source = read.readLine()
- val width = read.readLine().toInt()
- val height = read.readLine().toInt()
-
- read.close()
-
- val result: BufferedImage? = ImageIO.read(File(filePath))
-
- if (result != null) {
- val picture = Picture(
- source,
- getNameURL(source),
- result,
- width,
- height
- )
- outList.add(picture)
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-}
-
-private fun getNameURL(url: String): String {
- return url.substring(url.lastIndexOf('/') + 1, url.length)
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt
deleted file mode 100755
index 1113afb4..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model
-
-import java.awt.image.BufferedImage
-
-actual data class Picture(
- var source: String = "",
- var name: String = "",
- var image: BufferedImage,
- var width: Int = 0,
- var height: Int = 0,
- var id: Int = 0
-)
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
deleted file mode 100755
index 7ae798b3..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-import java.awt.image.BufferedImage
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.utils.applyBlurFilter
-
-class BlurFilter : BitmapFilter {
-
- override fun apply(bitmap: BufferedImage): BufferedImage {
- return applyBlurFilter(bitmap)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt
deleted file mode 100755
index 1174489a..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-
-import java.awt.image.BufferedImage
-import example.imageviewer.core.BitmapFilter
-
-class EmptyFilter : BitmapFilter {
-
- override fun apply(bitmap: BufferedImage): BufferedImage {
- return bitmap
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt
deleted file mode 100755
index 37fc4418..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package example.imageviewer.model.filtration
-
-import java.awt.image.BufferedImage
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.core.FilterType
-
-class FiltersManager {
-
- private var filtersMap: MutableMap = LinkedHashMap()
-
- fun clear() {
- filtersMap = LinkedHashMap()
- }
-
- fun add(filters: Collection) {
-
- for (filter in filters)
- add(filter)
- }
-
- fun add(filter: FilterType) {
-
- if (!filtersMap.containsKey(filter))
- filtersMap[filter] = getFilter(filter)
- }
-
- fun remove(filter: FilterType) {
- filtersMap.remove(filter)
- }
-
- fun contains(filter: FilterType): Boolean {
- return filtersMap.contains(filter)
- }
-
- fun applyFilters(bitmap: BufferedImage): BufferedImage {
-
- var result: BufferedImage = bitmap
- for (filter in filtersMap) {
- result = filter.value.apply(result)
- }
-
- return result
- }
-}
-
-private fun getFilter(type: FilterType): BitmapFilter {
-
- return when (type) {
- FilterType.GrayScale -> GrayScaleFilter()
- FilterType.Pixel -> PixelFilter()
- FilterType.Blur -> BlurFilter()
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
deleted file mode 100755
index 6b10bbf3..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-import java.awt.image.BufferedImage
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.utils.applyGrayScaleFilter
-
-class GrayScaleFilter : BitmapFilter {
-
- override fun apply(bitmap: BufferedImage) : BufferedImage {
- return applyGrayScaleFilter(bitmap)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
deleted file mode 100755
index 5b3a2cf9..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package example.imageviewer.model.filtration
-
-import java.awt.image.BufferedImage
-import example.imageviewer.core.BitmapFilter
-import example.imageviewer.utils.applyPixelFilter
-
-class PixelFilter : BitmapFilter {
-
- override fun apply(bitmap: BufferedImage): BufferedImage {
- return applyPixelFilter(bitmap)
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt
deleted file mode 100755
index 7c06d901..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package example.imageviewer.style
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.painterResource
-import java.awt.image.BufferedImage
-import javax.imageio.ImageIO
-
-@Composable
-fun icEmpty() = painterResource("images/empty.png")
-
-@Composable
-fun icBack() = painterResource("images/back.png")
-
-@Composable
-fun icRefresh() = painterResource("images/refresh.png")
-
-@Composable
-fun icDots() = painterResource("images/dots.png")
-
-@Composable
-fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png")
-
-@Composable
-fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png")
-
-@Composable
-fun icFilterPixelOn() = painterResource("images/pixel_on.png")
-
-@Composable
-fun icFilterPixelOff() = painterResource("images/pixel_off.png")
-
-@Composable
-fun icFilterBlurOn() = painterResource("images/blur_on.png")
-
-@Composable
-fun icFilterBlurOff() = painterResource("images/blur_off.png")
-
-@Composable
-fun icFilterUnknown() = painterResource("images/filter_unknown.png")
-
-@Composable
-fun icAppRounded() = painterResource("images/ic_imageviewer_round.png")
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt
deleted file mode 100755
index 192289f2..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package example.imageviewer.utils
-
-import java.awt.image.BufferedImage
-import example.imageviewer.model.Picture
-import javax.imageio.ImageIO
-import java.io.File
-import java.io.BufferedWriter
-import java.io.OutputStreamWriter
-import java.io.FileOutputStream
-import java.io.IOException
-import java.nio.charset.StandardCharsets
-
-val cacheImagePostfix = "info"
-val cacheImagePath = System.getProperty("user.home")!! +
- File.separator + "Pictures/imageviewer" + File.separator
-
-fun cacheImage(path: String, picture: Picture) {
- try {
- ImageIO.write(picture.image, "png", File(path))
-
- val bw =
- BufferedWriter(
- OutputStreamWriter(
- FileOutputStream(path + cacheImagePostfix),
- StandardCharsets.UTF_8
- )
- )
-
- bw.write(picture.source)
- bw.write("\r\n${picture.width}")
- bw.write("\r\n${picture.height}")
- bw.close()
-
- } catch (e: IOException) {
- e.printStackTrace()
- }
-}
-
-fun clearCache() {
-
- val directory = File(cacheImagePath)
-
- val files: Array? = directory.listFiles()
-
- if (files != null) {
- for (file in files) {
- if (file.isDirectory)
- continue
-
- file.delete()
- }
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Coroutines.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Coroutines.kt
deleted file mode 100644
index ab006ef1..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Coroutines.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package example.imageviewer.utils
-
-import kotlinx.coroutines.CoroutineScope
-import kotlin.coroutines.CoroutineContext
-
-actual fun runBlocking(
- context: CoroutineContext,
- block: suspend CoroutineScope.() -> T
-): T = kotlinx.coroutines.runBlocking(context, block)
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
deleted file mode 100755
index 711a6294..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-package example.imageviewer.utils
-
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import java.awt.Dimension
-import java.awt.Graphics2D
-import java.awt.Rectangle
-import java.awt.Toolkit
-import java.awt.image.BufferedImage
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.IOException
-import javax.imageio.ImageIO
-import java.awt.image.BufferedImageOp
-import java.awt.image.ConvolveOp
-import java.awt.image.Kernel
-import kotlin.math.pow
-import kotlin.math.roundToInt
-import example.imageviewer.view.DragHandler
-
-fun scaleBitmapAspectRatio(
- bitmap: BufferedImage,
- width: Int,
- height: Int
-): BufferedImage {
- val boundW: Float = width.toFloat()
- val boundH: Float = height.toFloat()
-
- val ratioX: Float = boundW / bitmap.width
- val ratioY: Float = boundH / bitmap.height
- val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
-
- val resultH = (bitmap.height * ratio).toInt()
- val resultW = (bitmap.width * ratio).toInt()
-
- val result = BufferedImage(resultW, resultH, BufferedImage.TYPE_INT_ARGB)
- val graphics = result.createGraphics()
- graphics.drawImage(bitmap, 0, 0, resultW, resultH, null)
- graphics.dispose()
-
- return result
-}
-
-fun getDisplayBounds(bitmap: BufferedImage, windowSize: DpSize): Rectangle {
-
- val boundW: Float = windowSize.width.value.toFloat()
- val boundH: Float = windowSize.height.value.toFloat()
-
- val ratioX: Float = bitmap.width / boundW
- val ratioY: Float = bitmap.height / boundH
-
- val ratio: Float = if (ratioX > ratioY) ratioX else ratioY
-
- val resultW = (boundW * ratio)
- val resultH = (boundH * ratio)
-
- return Rectangle(0, 0, resultW.toInt(), resultH.toInt())
-}
-
-fun applyGrayScaleFilter(bitmap: BufferedImage): BufferedImage {
-
- val result = BufferedImage(
- bitmap.getWidth(),
- bitmap.getHeight(),
- BufferedImage.TYPE_BYTE_GRAY)
-
- val graphics = result.getGraphics()
- graphics.drawImage(bitmap, 0, 0, null)
- graphics.dispose()
-
- return result
-}
-
-fun applyPixelFilter(bitmap: BufferedImage): BufferedImage {
-
- val w: Int = bitmap.width
- val h: Int = bitmap.height
-
- var result = scaleBitmapAspectRatio(bitmap, w / 20, h / 20)
- result = scaleBitmapAspectRatio(result, w, h)
-
- return result
-}
-
-fun applyBlurFilter(bitmap: BufferedImage): BufferedImage {
-
- var result = BufferedImage(bitmap.getWidth(), bitmap.getHeight(), bitmap.type)
-
- val graphics = result.getGraphics()
- graphics.drawImage(bitmap, 0, 0, null)
- graphics.dispose()
-
- val radius = 11
- val size = 11
- val weight: Float = 1.0f / (size * size)
- val matrix = FloatArray(size * size)
-
- for (i in 0..matrix.size - 1) {
- matrix[i] = weight
- }
-
- val kernel = Kernel(radius, size, matrix)
- val op = ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null)
- result = op.filter(result, null)
-
- return result.getSubimage(
- radius,
- radius,
- result.width - radius * 2,
- result.height - radius * 2
- )
-}
-
-fun toByteArray(bitmap: BufferedImage) : ByteArray {
- val baos = ByteArrayOutputStream()
- ImageIO.write(bitmap, "png", baos)
- return baos.toByteArray()
-}
-
-fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage {
- return bitmap.getSubimage(crop.x, crop.y, crop.width, crop.height)
-}
-
-fun cropBitmapByScale(
- bitmap: BufferedImage,
- size: DpSize,
- scale: Float,
- drag: DragHandler
-): BufferedImage {
- val crop = cropBitmapByBounds(
- bitmap,
- getDisplayBounds(bitmap, size),
- size,
- scale,
- drag
- )
- return cropImage(
- bitmap,
- Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y)
- )
-}
-
-fun cropBitmapByBounds(
- bitmap: BufferedImage,
- bounds: Rectangle,
- size: DpSize,
- scaleFactor: Float,
- drag: DragHandler
-): Rectangle {
-
- if (scaleFactor <= 1f) {
- return Rectangle(0, 0, bitmap.width, bitmap.height)
- }
-
- var scale = scaleFactor.toDouble().pow(1.4)
-
- var boundW = (bounds.width / scale).roundToInt()
- var boundH = (bounds.height / scale).roundToInt()
-
- scale *= size.width.value / bounds.width.toDouble()
-
- val offsetX = drag.getAmount().x / scale
- val offsetY = drag.getAmount().y / scale
-
- if (boundW > bitmap.width) {
- boundW = bitmap.width
- }
- if (boundH > bitmap.height) {
- boundH = bitmap.height
- }
-
- val invisibleW = bitmap.width - boundW
- var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt()
-
- if (leftOffset > invisibleW) {
- leftOffset = invisibleW
- drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
- }
- if (leftOffset < 0) {
- drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
- leftOffset = 0
- }
-
- val invisibleH = bitmap.height - boundH
- var topOffset = (invisibleH / 2 - offsetY).roundToInt()
-
- if (topOffset > invisibleH) {
- topOffset = invisibleH
- drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
- }
- if (topOffset < 0) {
- drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
- topOffset = 0
- }
-
- return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH)
-}
-
-fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): DpSize {
- val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
- val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
- val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
- val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
- val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
- return DpSize(width.dp, height.dp)
-}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt
deleted file mode 100755
index ef002711..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Surface
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import example.imageviewer.model.AppState
-import example.imageviewer.model.ScreenType
-import example.imageviewer.model.ContentState
-import example.imageviewer.style.Gray
-
-private val message: MutableState = mutableStateOf("")
-private val state: MutableState = mutableStateOf(false)
-
-@Composable
-fun AppUI(content: ContentState) {
-
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = Gray
- ) {
- when (AppState.screenState()) {
- ScreenType.MainScreen -> {
- MainScreen(content)
- }
- ScreenType.FullscreenImage -> {
- FullscreenImage(content)
- }
- }
- }
-
- Toast(message.value, state)
-}
-
-fun showPopUpMessage(text: String) {
- message.value = text
- state.value = true
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt
deleted file mode 100644
index e8e91e49..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt
+++ /dev/null
@@ -1,207 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.foundation.*
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsHoveredAsState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.type
-import androidx.compose.ui.input.key.KeyEventType
-import androidx.compose.ui.input.key.onPreviewKeyEvent
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.unit.dp
-import example.imageviewer.core.FilterType
-import example.imageviewer.model.AppState
-import example.imageviewer.model.ContentState
-import example.imageviewer.model.ScreenType
-import example.imageviewer.ResString
-import example.imageviewer.style.DarkGray
-import example.imageviewer.style.Foreground
-import example.imageviewer.style.MiniatureColor
-import example.imageviewer.style.TranslucentBlack
-import example.imageviewer.style.Transparent
-import example.imageviewer.style.icBack
-import example.imageviewer.style.icFilterBlurOff
-import example.imageviewer.style.icFilterBlurOn
-import example.imageviewer.style.icFilterGrayscaleOff
-import example.imageviewer.style.icFilterGrayscaleOn
-import example.imageviewer.style.icFilterPixelOff
-import example.imageviewer.style.icFilterPixelOn
-
-@Composable
-fun FullscreenImage(
- content: ContentState
-) {
- Column {
- ToolBar(content.getSelectedImageName(), content)
- Image(content)
- }
- if (!content.isContentReady()) {
- LoadingScreen()
- }
-}
-
-@Composable
-fun ToolBar(
- text: String,
- content: ContentState
-) {
- val backButtonInteractionSource = remember { MutableInteractionSource() }
- val backButtonHover by backButtonInteractionSource.collectIsHoveredAsState()
- Surface(
- color = MiniatureColor,
- modifier = Modifier.height(44.dp)
- ) {
- Row(modifier = Modifier.padding(end = 30.dp)) {
- Surface(
- color = Transparent,
- modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
- shape = CircleShape
- ) {
- Tooltip(ResString.back) {
- Clickable(
- modifier = Modifier
- .hoverable(backButtonInteractionSource)
- .background(color = if (backButtonHover) TranslucentBlack else Transparent),
- onClick = {
- if (content.isContentReady()) {
- content.restoreMainImage()
- AppState.screenState(ScreenType.MainScreen)
- }
- }) {
- Image(
- icBack(),
- contentDescription = null,
- modifier = Modifier.size(38.dp)
- )
- }
- }
- }
- Text(
- text,
- color = Foreground,
- maxLines = 1,
- modifier = Modifier.padding(start = 30.dp).weight(1f)
- .align(Alignment.CenterVertically),
- style = MaterialTheme.typography.body1
- )
-
- Surface(
- color = Color(255, 255, 255, 40),
- modifier = Modifier.size(154.dp, 38.dp)
- .align(Alignment.CenterVertically),
- shape = CircleShape
- ) {
- val state = rememberScrollState(0)
- Row(modifier = Modifier.horizontalScroll(state)) {
- Row {
- for (type in FilterType.values()) {
- FilterButton(content, type)
- }
- }
- }
- }
- }
- }
-}
-
-@Composable
-fun FilterButton(
- content: ContentState,
- type: FilterType,
- modifier: Modifier = Modifier.size(38.dp)
-) {
- val interactionSource = remember { MutableInteractionSource() }
- val filterButtonHover by interactionSource.collectIsHoveredAsState()
- Box(
- modifier = Modifier.background(color = Transparent).clip(CircleShape)
- ) {
- Tooltip("$type") {
- Clickable(
- modifier = Modifier
- .hoverable(interactionSource)
- .background(color = if (filterButtonHover) TranslucentBlack else Transparent),
- onClick = { content.toggleFilter(type)}
- ) {
- Image(
- getFilterImage(type = type, content = content),
- contentDescription = null,
- modifier
- )
- }
- }
- }
- Spacer(Modifier.width(20.dp))
-}
-
-@Composable
-fun getFilterImage(type: FilterType, content: ContentState): Painter {
- return when (type) {
- FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
- FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
- FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff()
- }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun Image(content: ContentState) {
- val onUpdate = remember { { content.updateMainImage() } }
- Surface(
- color = DarkGray,
- modifier = Modifier.fillMaxSize()
- ) {
- Draggable(
- onUpdate = onUpdate,
- dragHandler = content.drag,
- modifier = Modifier.fillMaxSize()
- ) {
- Zoomable(
- onUpdate = onUpdate,
- scaleHandler = content.scale,
- modifier = Modifier.fillMaxSize()
- .onPreviewKeyEvent {
- if (it.type == KeyEventType.KeyUp) {
- when (it.key) {
- Key.DirectionLeft -> {
- content.swipePrevious()
- }
- Key.DirectionRight -> {
- content.swipeNext()
- }
- }
- }
- false
- }
- ) {
- Image(
- bitmap = content.getSelectedImage(),
- contentDescription = null,
- contentScale = ContentScale.Fit
- )
- }
- }
- }
-}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
deleted file mode 100755
index edf1f4b2..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
+++ /dev/null
@@ -1,250 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.foundation.*
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsHoveredAsState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Card
-import androidx.compose.material.Divider
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.painter.BitmapPainter
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.toComposeImageBitmap
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.unit.dp
-import example.imageviewer.ResString
-import example.imageviewer.model.AppState
-import example.imageviewer.model.ContentState
-import example.imageviewer.model.Picture
-import example.imageviewer.model.ScreenType
-import example.imageviewer.style.DarkGray
-import example.imageviewer.style.DarkGreen
-import example.imageviewer.style.Foreground
-import example.imageviewer.style.LightGray
-import example.imageviewer.style.MiniatureColor
-import example.imageviewer.style.MiniatureHoverColor
-import example.imageviewer.style.TranslucentBlack
-import example.imageviewer.style.TranslucentWhite
-import example.imageviewer.style.Transparent
-import example.imageviewer.style.icDots
-import example.imageviewer.style.icEmpty
-import example.imageviewer.style.icRefresh
-import example.imageviewer.utils.toByteArray
-
-@Composable
-fun MainScreen(content: ContentState) {
- Column {
- TopContent(content)
- ScrollableArea(content)
- }
- if (!content.isContentReady()) {
- LoadingScreen(ResString.loading)
- }
-}
-
-@Composable
-fun TopContent(content: ContentState) {
- TitleBar(text = ResString.appName, content = content)
- PreviewImage(content)
- Spacer(modifier = Modifier.height(10.dp))
- Divider()
- Spacer(modifier = Modifier.height(5.dp))
-}
-
-@Composable
-fun TitleBar(text: String, content: ContentState) {
- val interactionSource = remember { MutableInteractionSource() }
- val refreshButtonHover by interactionSource.collectIsHoveredAsState()
- TopAppBar(
- backgroundColor = DarkGreen,
- title = {
- Row(Modifier.height(50.dp)) {
- Text(
- text,
- color = Foreground,
- modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
- )
- Surface(
- color = Transparent,
- modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
- shape = CircleShape
- ) {
- Tooltip(ResString.refresh) {
- Clickable(
- modifier = Modifier
- .hoverable(interactionSource)
- .background(color = if (refreshButtonHover) TranslucentBlack else Transparent),
- onClick = {
- if (content.isContentReady()) {
- content.refresh()
- }
- }
- ) {
- Image(
- icRefresh(),
- contentDescription = null,
- modifier = Modifier.size(35.dp)
- )
- }
- }
- }
- }
- })
-}
-
-@Composable
-fun PreviewImage(content: ContentState) {
- Clickable(
- modifier = Modifier.background(color = DarkGray),
- onClick = {
- AppState.screenState(ScreenType.FullscreenImage)
- }
- ) {
- Card(
- backgroundColor = Transparent,
- modifier = Modifier.height(250.dp),
- shape = RectangleShape,
- elevation = 1.dp
- ) {
- Image(
- if (content.isMainImageEmpty())
- icEmpty()
- else
- BitmapPainter(content.getSelectedImage()),
- contentDescription = null,
- modifier = Modifier
- .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
- contentScale = ContentScale.Fit
- )
- }
- }
-}
-
-@Composable
-fun Miniature(
- picture: Picture,
- content: ContentState
-) {
- val cardHoverInteractionSource = remember { MutableInteractionSource() }
- val cardHover by cardHoverInteractionSource.collectIsHoveredAsState()
- val infoButtonInteractionSource = remember { MutableInteractionSource() }
- val infoButtonHover by infoButtonInteractionSource.collectIsHoveredAsState()
- Card(
- backgroundColor = if (cardHover) MiniatureHoverColor else MiniatureColor,
- modifier = Modifier.padding(start = 10.dp, end = 18.dp).height(70.dp)
- .fillMaxWidth()
- .hoverable(cardHoverInteractionSource)
- .clickable {
- content.setMainImage(picture)
- },
- shape = RectangleShape
- ) {
- Row(modifier = Modifier.padding(end = 30.dp)) {
- Clickable(
- onClick = {
- content.fullscreen(picture)
- }
- ) {
- Image(
- org.jetbrains.skia.Image.makeFromEncoded(
- toByteArray(picture.image)
- ).toComposeImageBitmap(),
- contentDescription = null,
- modifier = Modifier.height(70.dp)
- .width(90.dp)
- .padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp),
- contentScale = ContentScale.Crop
- )
- }
- Text(
- text = picture.name,
- color = Foreground,
- modifier = Modifier
- .weight(1f)
- .align(Alignment.CenterVertically)
- .padding(start = 16.dp),
- style = MaterialTheme.typography.body1
- )
-
- Clickable(
- modifier = Modifier.height(70.dp)
- .width(30.dp)
- .hoverable(infoButtonInteractionSource)
- .background(color = if (infoButtonHover) TranslucentWhite else Transparent),
- onClick = {
- showPopUpMessage(
- "${ResString.picture} " +
- "${picture.name} \n" +
- "${ResString.size} " +
- "${picture.width}x${picture.height} " +
- "${ResString.pixels}"
- )
- }
- ) {
- Image(
- icDots(),
- contentDescription = null,
- modifier = Modifier.height(70.dp)
- .width(30.dp)
- .padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp),
- contentScale = ContentScale.FillHeight
- )
- }
- }
- }
-}
-
-@Composable
-fun ScrollableArea(content: ContentState) {
- Box(
- modifier = Modifier.fillMaxSize()
- .padding(end = 8.dp)
- ) {
- val stateVertical = rememberScrollState(0)
- Column(modifier = Modifier.verticalScroll(stateVertical)) {
- var index = 1
- Column {
- for (picture in content.getMiniatures()) {
- Miniature(
- picture = picture,
- content = content
- )
- Spacer(modifier = Modifier.height(5.dp))
- index++
- }
- }
- }
- VerticalScrollbar(
- adapter = rememberScrollbarAdapter(stateVertical),
- modifier = Modifier.align(Alignment.CenterEnd)
- .fillMaxHeight()
- )
- }
-}
-
-@Composable
-fun Divider() {
- Divider(
- color = LightGray,
- modifier = Modifier.padding(start = 10.dp, end = 10.dp)
- )
-}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt b/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
deleted file mode 100644
index e9f6321b..00000000
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package example.imageviewer.view
-
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.material.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.type
-import androidx.compose.ui.input.key.KeyEventType
-import androidx.compose.ui.input.key.onPreviewKeyEvent
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.ExperimentalComposeUiApi
-import example.imageviewer.style.Transparent
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Composable
-fun Zoomable(
- scaleHandler: ScaleHandler,
- modifier: Modifier = Modifier,
- onUpdate: (() -> Unit)? = null,
- children: @Composable() () -> Unit
-) {
- val focusRequester = FocusRequester()
-
- Surface(
- color = Transparent,
- modifier = modifier.onPreviewKeyEvent {
- if (it.type == KeyEventType.KeyUp) {
- when (it.key) {
- Key.I -> {
- scaleHandler.onScale(1.2f)
- onUpdate?.invoke()
- }
- Key.O -> {
- scaleHandler.onScale(0.8f)
- onUpdate?.invoke()
- }
- Key.R -> {
- scaleHandler.reset()
- onUpdate?.invoke()
- }
- }
- }
- false
- }
- .focusRequester(focusRequester)
- .focusable()
- .pointerInput(Unit) {
- detectTapGestures(onDoubleTap = { scaleHandler.reset() }) {
- focusRequester.requestFocus()
- }
- }
- ) {
- children()
- }
-
- DisposableEffect(Unit) {
- focusRequester.requestFocus()
- onDispose { }
- }
-}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/back.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/back.png
deleted file mode 100755
index 206b8d46..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/back.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png
deleted file mode 100755
index e6326161..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png
deleted file mode 100755
index 7f5ad81b..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/dots.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/dots.png
deleted file mode 100755
index 4eb0c9f1..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/dots.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/empty.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/empty.png
deleted file mode 100755
index 54e90076..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/empty.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png
deleted file mode 100755
index 9193c3f3..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png
deleted file mode 100755
index 57fbe789..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png
deleted file mode 100755
index ffe1f610..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png
deleted file mode 100755
index a41ebfe0..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png
deleted file mode 100755
index 1482ff85..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/refresh.png b/experimental/examples/imageviewer/common/src/desktopMain/resources/images/refresh.png
deleted file mode 100755
index 3be99c19..00000000
Binary files a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/refresh.png and /dev/null differ
diff --git a/experimental/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt b/experimental/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt
deleted file mode 100644
index c17682c4..00000000
--- a/experimental/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package example.imageviewer
-
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Window
-import androidx.compose.ui.window.WindowState
-import androidx.compose.ui.window.WindowPosition
-import androidx.compose.ui.window.application
-import androidx.compose.ui.window.rememberWindowState
-import example.imageviewer.model.ContentState
-import example.imageviewer.style.icAppRounded
-import example.imageviewer.utils.getPreferredWindowSize
-import example.imageviewer.view.AppUI
-import example.imageviewer.view.SplashUI
-
-fun main() = application {
- val state = rememberWindowState()
- val content = remember {
- ContentState.applyContent(
- state,
- "https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
- )
- }
-
- val icon = icAppRounded()
-
- if (content.isAppReady()) {
- Window(
- onCloseRequest = ::exitApplication,
- title = "Image Viewer",
- state = WindowState(
- position = WindowPosition.Aligned(Alignment.Center),
- size = getPreferredWindowSize(800, 1000)
- ),
- icon = icon
- ) {
- MaterialTheme {
- AppUI(content)
- }
- }
- } else {
- Window(
- onCloseRequest = ::exitApplication,
- title = "Image Viewer",
- state = WindowState(
- position = WindowPosition.Aligned(Alignment.Center),
- size = getPreferredWindowSize(800, 300)
- ),
- undecorated = true,
- icon = icon,
- ) {
- MaterialTheme {
- SplashUI()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/desktop/build.gradle.kts b/experimental/examples/imageviewer/desktopApp/build.gradle.kts
similarity index 84%
rename from experimental/examples/imageviewer/desktop/build.gradle.kts
rename to experimental/examples/imageviewer/desktopApp/build.gradle.kts
index 2116a048..95effc02 100755
--- a/experimental/examples/imageviewer/desktop/build.gradle.kts
+++ b/experimental/examples/imageviewer/desktopApp/build.gradle.kts
@@ -1,7 +1,7 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
- kotlin("multiplatform") // kotlin("jvm") doesn't work well in IDEA/AndroidStudio (https://github.com/JetBrains/compose-jb/issues/22)
+ kotlin("multiplatform")
id("org.jetbrains.compose")
}
@@ -10,10 +10,10 @@ kotlin {
withJava()
}
sourceSets {
- named("jvmMain") {
+ val jvmMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
- implementation(project(":common"))
+ implementation(project(":shared"))
}
}
}
diff --git a/experimental/examples/imageviewer/desktop/rules.pro b/experimental/examples/imageviewer/desktopApp/rules.pro
similarity index 100%
rename from experimental/examples/imageviewer/desktop/rules.pro
rename to experimental/examples/imageviewer/desktopApp/rules.pro
diff --git a/experimental/examples/imageviewer/desktopApp/src/jvmMain/kotlin/example/imageviewer/Main.kt b/experimental/examples/imageviewer/desktopApp/src/jvmMain/kotlin/example/imageviewer/Main.kt
new file mode 100644
index 00000000..96fe7556
--- /dev/null
+++ b/experimental/examples/imageviewer/desktopApp/src/jvmMain/kotlin/example/imageviewer/Main.kt
@@ -0,0 +1,11 @@
+package example.imageviewer
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.ui.window.application
+import example.imageviewer.view.ImageViewerDesktop
+
+fun main() = application {
+ MaterialTheme {
+ ImageViewerDesktop()
+ }
+}
diff --git a/experimental/examples/imageviewer/gradle.properties b/experimental/examples/imageviewer/gradle.properties
old mode 100755
new mode 100644
index c18fa7d4..48afff08
--- a/experimental/examples/imageviewer/gradle.properties
+++ b/experimental/examples/imageviewer/gradle.properties
@@ -1,24 +1,18 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx2048m
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
-# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app"s APK
-# https://developer.android.com/topic/libraries/support-library/androidx-rn
-android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
-# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+xcodeproj=iosApp
+kotlin.native.cocoapods.generate.wrapper=true
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx3g
+org.jetbrains.compose.experimental.jscanvas.enabled=true
+org.jetbrains.compose.experimental.macos.enabled=true
+org.jetbrains.compose.experimental.uikit.enabled=true
+kotlin.native.cacheKind=none
+kotlin.native.useEmbeddableCompilerJar=true
+kotlin.native.enableDependencyPropagation=false
+kotlin.mpp.enableGranularSourceSetsMetadata=true
+# Enable kotlin/native experimental memory model
+kotlin.native.binary.memoryModel=experimental
kotlin.version=1.7.20
agp.version=7.1.3
-compose.version=1.2.1
+compose.version=1.2.2
+ktor.version=2.2.1
diff --git a/experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.jar b/experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.jar
index f3d88b1c..249e5832 100644
Binary files a/experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.jar and b/experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/experimental/examples/imageviewer/gradlew b/experimental/examples/imageviewer/gradlew
index fbd7c515..a69d9cb6 100755
--- a/experimental/examples/imageviewer/gradlew
+++ b/experimental/examples/imageviewer/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
+ JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,101 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=`expr $i + 1`
- done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/experimental/examples/imageviewer/gradlew.bat b/experimental/examples/imageviewer/gradlew.bat
index 5093609d..f127cfd4 100755
--- a/experimental/examples/imageviewer/gradlew.bat
+++ b/experimental/examples/imageviewer/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,21 +64,6 @@ echo location of your Java installation.
goto fail
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
:execute
@rem Setup the command line
@@ -86,17 +71,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/experimental/examples/imageviewer/iosApp/Configuration/TeamId.xcconfig b/experimental/examples/imageviewer/iosApp/Configuration/TeamId.xcconfig
new file mode 100644
index 00000000..bf06eb27
--- /dev/null
+++ b/experimental/examples/imageviewer/iosApp/Configuration/TeamId.xcconfig
@@ -0,0 +1 @@
+TEAM_ID=
diff --git a/experimental/examples/imageviewer/iosApp/Imageviewer.xcodeproj/project.pbxproj b/experimental/examples/imageviewer/iosApp/Imageviewer.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..2e83e0d5
--- /dev/null
+++ b/experimental/examples/imageviewer/iosApp/Imageviewer.xcodeproj/project.pbxproj
@@ -0,0 +1,398 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 2152FB042600AC8F00CF470E /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iosApp.swift */; };
+ C1FC908188C4E8695729CB06 /* Pods_Imageviewer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_Imageviewer.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 1EB65E27D2C0F884D0A1A133 /* Pods-Imageviewer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Imageviewer.debug.xcconfig"; path = "Target Support Files/Pods-Imageviewer/Pods-Imageviewer.debug.xcconfig"; sourceTree = ""; };
+ 2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; };
+ 3D7A606AB0AD7636269BD9D0 /* Pods-Imageviewer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Imageviewer.release.xcconfig"; path = "Target Support Files/Pods-Imageviewer/Pods-Imageviewer.release.xcconfig"; sourceTree = ""; };
+ 7555FF7B242A565900829871 /* Imageviewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Imageviewer.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 8DE96E47030356CE6AD9794A /* Pods_Imageviewer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Imageviewer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ AB3632DC29227652001CCB65 /* TeamId.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = TeamId.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 9964867F0862B4D9FB6ABFC7 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C1FC908188C4E8695729CB06 /* Pods_Imageviewer.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 7555FF72242A565900829871 = {
+ isa = PBXGroup;
+ children = (
+ AB1DB47929225F7C00F7AF9C /* Configuration */,
+ 7555FF7D242A565900829871 /* iosApp */,
+ 7555FF7C242A565900829871 /* Products */,
+ E1DAFBE8E1CFC0878361EF0E /* Pods */,
+ B62309C7396AD7BF607A63B2 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 7555FF7C242A565900829871 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7555FF7B242A565900829871 /* Imageviewer.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 7555FF7D242A565900829871 /* iosApp */ = {
+ isa = PBXGroup;
+ children = (
+ 7555FF8C242A565B00829871 /* Info.plist */,
+ 2152FB032600AC8F00CF470E /* iosApp.swift */,
+ );
+ path = iosApp;
+ sourceTree = "";
+ };
+ AB1DB47929225F7C00F7AF9C /* Configuration */ = {
+ isa = PBXGroup;
+ children = (
+ AB3632DC29227652001CCB65 /* TeamId.xcconfig */,
+ );
+ path = Configuration;
+ sourceTree = "";
+ };
+ B62309C7396AD7BF607A63B2 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 8DE96E47030356CE6AD9794A /* Pods_Imageviewer.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ E1DAFBE8E1CFC0878361EF0E /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 1EB65E27D2C0F884D0A1A133 /* Pods-Imageviewer.debug.xcconfig */,
+ 3D7A606AB0AD7636269BD9D0 /* Pods-Imageviewer.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 7555FF7A242A565900829871 /* Imageviewer */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "Imageviewer" */;
+ buildPhases = (
+ E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */,
+ 7555FF77242A565900829871 /* Sources */,
+ 7555FF79242A565900829871 /* Resources */,
+ 9964867F0862B4D9FB6ABFC7 /* Frameworks */,
+ F34398AEB6C0D136D245A061 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Imageviewer;
+ productName = iosApp;
+ productReference = 7555FF7B242A565900829871 /* Imageviewer.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 7555FF73242A565900829871 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 1130;
+ LastUpgradeCheck = 1130;
+ ORGANIZATIONNAME = org.jetbrains;
+ TargetAttributes = {
+ 7555FF7A242A565900829871 = {
+ CreatedOnToolsVersion = 11.3.1;
+ };
+ };
+ };
+ buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "Imageviewer" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 7555FF72242A565900829871;
+ productRefGroup = 7555FF7C242A565900829871 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 7555FF7A242A565900829871 /* Imageviewer */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 7555FF79242A565900829871 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Imageviewer-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ F34398AEB6C0D136D245A061 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Imageviewer/Pods-Imageviewer-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Imageviewer/Pods-Imageviewer-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Imageviewer/Pods-Imageviewer-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 7555FF77242A565900829871 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 2152FB042600AC8F00CF470E /* iosApp.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 7555FFA3242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* TeamId.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 7555FFA4242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = AB3632DC29227652001CCB65 /* TeamId.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 7555FFA6242A565B00829871 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 1EB65E27D2C0F884D0A1A133 /* Pods-Imageviewer.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "";
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Imageviewer${TEAM_ID}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 7555FFA7242A565B00829871 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 3D7A606AB0AD7636269BD9D0 /* Pods-Imageviewer.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "";
+ DEVELOPMENT_TEAM = "${TEAM_ID}";
+ ENABLE_PREVIEWS = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 14.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Imageviewer${TEAM_ID}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 7555FF76242A565900829871 /* Build configuration list for PBXProject "Imageviewer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA3242A565B00829871 /* Debug */,
+ 7555FFA4242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "Imageviewer" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 7555FFA6242A565B00829871 /* Debug */,
+ 7555FFA7242A565B00829871 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 7555FF73242A565900829871 /* Project object */;
+}
diff --git a/experimental/examples/imageviewer/iosApp/Podfile b/experimental/examples/imageviewer/iosApp/Podfile
new file mode 100644
index 00000000..5f87c3d5
--- /dev/null
+++ b/experimental/examples/imageviewer/iosApp/Podfile
@@ -0,0 +1,5 @@
+target 'Imageviewer' do
+ use_frameworks!
+ platform :ios, '14.1'
+ pod 'shared', :path => '../shared'
+end
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/iosApp/iosApp/Info.plist b/experimental/examples/imageviewer/iosApp/iosApp/Info.plist
new file mode 100644
index 00000000..9a269f5e
--- /dev/null
+++ b/experimental/examples/imageviewer/iosApp/iosApp/Info.plist
@@ -0,0 +1,48 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UILaunchScreen
+
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/experimental/examples/imageviewer/iosApp/iosApp/iosApp.swift b/experimental/examples/imageviewer/iosApp/iosApp/iosApp.swift
new file mode 100644
index 00000000..b42016a6
--- /dev/null
+++ b/experimental/examples/imageviewer/iosApp/iosApp/iosApp.swift
@@ -0,0 +1,15 @@
+import UIKit
+import shared
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ window = UIWindow(frame: UIScreen.main.bounds)
+ let mainViewController = Main_iosKt.MainViewController()
+ window?.rootViewController = mainViewController
+ window?.makeKeyAndVisible()
+ return true
+ }
+}
diff --git a/experimental/examples/imageviewer/run-configurations.png b/experimental/examples/imageviewer/run-configurations.png
new file mode 100644
index 00000000..0840602f
Binary files /dev/null and b/experimental/examples/imageviewer/run-configurations.png differ
diff --git a/experimental/examples/imageviewer/settings.gradle.kts b/experimental/examples/imageviewer/settings.gradle.kts
old mode 100755
new mode 100644
index f4ac8d1f..a2c5064a
--- a/experimental/examples/imageviewer/settings.gradle.kts
+++ b/experimental/examples/imageviewer/settings.gradle.kts
@@ -12,11 +12,17 @@ pluginManagement {
kotlin("jvm").version(kotlinVersion)
kotlin("multiplatform").version(kotlinVersion)
+ kotlin("plugin.serialization").version(kotlinVersion)
kotlin("android").version(kotlinVersion)
+ id("com.android.base").version(agpVersion)
id("com.android.application").version(agpVersion)
id("com.android.library").version(agpVersion)
id("org.jetbrains.compose").version(composeVersion)
}
}
-include(":common", ":android", ":desktop")
+rootProject.name = "imageviewer"
+
+include(":androidApp")
+include(":shared")
+include(":desktopApp")
diff --git a/experimental/examples/imageviewer/shared/build.gradle.kts b/experimental/examples/imageviewer/shared/build.gradle.kts
new file mode 100755
index 00000000..9d7a3777
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/build.gradle.kts
@@ -0,0 +1,84 @@
+plugins {
+ kotlin("multiplatform")
+ kotlin("native.cocoapods")
+ id("com.android.library")
+ id("org.jetbrains.compose")
+ kotlin("plugin.serialization")
+}
+
+version = "1.0-SNAPSHOT"
+val ktorVersion = extra["ktor.version"]
+
+kotlin {
+ android()
+ jvm("desktop")
+ ios()
+ iosSimulatorArm64()
+
+ cocoapods {
+ summary = "Shared code for the sample"
+ homepage = "https://github.com/JetBrains/compose-jb"
+ ios.deploymentTarget = "14.1"
+ podfile = project.file("../iosApp/Podfile")
+ framework {
+ baseName = "shared"
+ isStatic = true
+ }
+ extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
+ }
+
+ sourceSets {
+ val commonMain by getting {
+ dependencies {
+ implementation("io.ktor:ktor-client-core:$ktorVersion")
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material)
+ implementation("org.jetbrains.compose.components:components-resources:1.3.0-beta04-dev879")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
+ }
+ }
+ val androidMain by getting {
+ dependencies {
+ implementation("androidx.appcompat:appcompat:1.5.1")
+ implementation("androidx.core:core-ktx:1.9.0")
+ implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
+ }
+ }
+ val iosMain by getting {
+ dependencies {
+ implementation("io.ktor:ktor-client-darwin:$ktorVersion")
+ }
+ }
+ val iosTest by getting
+ val iosSimulatorArm64Main by getting {
+ dependsOn(iosMain)
+ }
+ val iosSimulatorArm64Test by getting {
+ dependsOn(iosTest)
+ }
+
+ val desktopMain by getting {
+ dependencies {
+ implementation(compose.desktop.common)
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
+ implementation("io.ktor:ktor-client-cio:$ktorVersion")
+ }
+ }
+ }
+}
+
+android {
+ compileSdk = 33
+ sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ sourceSets["main"].res.srcDirs("src/androidMain/res")
+ sourceSets["main"].resources.srcDir("src/commonMain/resources")
+ defaultConfig {
+ minSdk = 24
+ targetSdk = 33
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml b/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml
new file mode 100755
index 00000000..51320073
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageBitmap.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageBitmap.android.kt
new file mode 100644
index 00000000..a1c41213
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageBitmap.android.kt
@@ -0,0 +1,12 @@
+package example.imageviewer
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+
+actual fun ByteArray.toImageBitmap(): ImageBitmap = toAndroidBitmap().asImageBitmap()
+
+fun ByteArray.toAndroidBitmap(): Bitmap {
+ return BitmapFactory.decodeByteArray(this, 0, size)
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
new file mode 100755
index 00000000..5f9c242d
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
@@ -0,0 +1,14 @@
+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()
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
new file mode 100755
index 00000000..e4ad8677
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
@@ -0,0 +1,13 @@
+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()
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
new file mode 100755
index 00000000..ae0a7197
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
@@ -0,0 +1,12 @@
+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()
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.android.kt
new file mode 100644
index 00000000..901cd77d
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.android.kt
@@ -0,0 +1,77 @@
+package example.imageviewer.utils
+
+import android.content.Context
+import android.graphics.*
+import android.renderscript.Allocation
+import android.renderscript.Element
+import android.renderscript.RenderScript
+import android.renderscript.ScriptIntrinsicBlur
+
+fun scaleBitmapAspectRatio(
+ bitmap: Bitmap,
+ width: Int,
+ height: Int,
+ filter: Boolean = false
+): Bitmap {
+ val boundW: Float = width.toFloat()
+ val boundH: Float = height.toFloat()
+
+ val ratioX: Float = boundW / bitmap.width
+ val ratioY: Float = boundH / bitmap.height
+ val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
+
+ val resultH = (bitmap.height * ratio).toInt()
+ val resultW = (bitmap.width * ratio).toInt()
+
+ return Bitmap.createScaledBitmap(bitmap, resultW, resultH, filter)
+}
+
+
+fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
+
+ val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
+
+ val canvas = Canvas(result)
+
+ val colorMatrix = ColorMatrix()
+ colorMatrix.setSaturation(0f)
+
+ val paint = Paint()
+ paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
+
+ canvas.drawBitmap(result, 0f, 0f, paint)
+
+ return result
+}
+
+fun applyPixelFilter(bitmap: Bitmap): Bitmap {
+
+ var result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
+ val w: Int = bitmap.width
+ val h: Int = bitmap.height
+ result = scaleBitmapAspectRatio(result, w / 20, h / 20)
+ result = scaleBitmapAspectRatio(result, w, h)
+
+ return result
+}
+
+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(25f)
+ theIntrinsic.setInput(tmpIn)
+ theIntrinsic.forEach(tmpOut)
+
+ tmpOut.copyTo(result)
+
+ return result
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt
new file mode 100755
index 00000000..366e5427
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt
@@ -0,0 +1,78 @@
+package example.imageviewer.view
+
+import android.content.Context
+import android.widget.Toast
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import example.imageviewer.*
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.core.FilterType
+import example.imageviewer.model.ContentRepository
+import example.imageviewer.model.State
+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 io.ktor.client.*
+import io.ktor.client.engine.okhttp.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun ImageViewerAndroid() {
+ val context: Context = LocalContext.current
+ val ioScope = rememberCoroutineScope { Dispatchers.IO }
+ val dependencies = remember(context, ioScope) { getDependencies(context, ioScope) }
+ val state = remember { mutableStateOf(State()) }
+ ImageViewerTheme {
+ ImageViewerCommon(state, 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 =
+ createNetworkRepository(httpClient)
+ .adapter { it.toImageBitmap() }
+
+ override val notification: Notification = object : PopupNotification(localization) {
+ override fun showPopUpMessage(text: String) {
+ GlobalScope.launch(Dispatchers.Main) {
+ Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/PreviewImage.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/PreviewImage.android.kt
new file mode 100644
index 00000000..455de535
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/PreviewImage.android.kt
@@ -0,0 +1,9 @@
+package example.imageviewer.view
+
+import android.content.res.Configuration
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalConfiguration
+
+@Composable
+internal actual fun needShowPreview(): Boolean =
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScalableImage.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScalableImage.android.kt
new file mode 100644
index 00000000..fa88d180
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScalableImage.android.kt
@@ -0,0 +1,8 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import example.imageviewer.model.ScalableState
+
+actual fun Modifier.addUserInput(state: MutableState) =
+ addTouchUserInput(state)
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScrollableColumn.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScrollableColumn.android.kt
new file mode 100644
index 00000000..7ef544a3
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScrollableColumn.android.kt
@@ -0,0 +1,8 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+actual fun ScrollableColumn(modifier: Modifier, content: @Composable () -> Unit) =
+ TouchScrollableColumn(modifier, content)
diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/Tooltip.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/Tooltip.android.kt
new file mode 100644
index 00000000..8e638fd7
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/Tooltip.android.kt
@@ -0,0 +1,9 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun Tooltip(text: String, content: @Composable () -> Unit) {
+ // No Tooltip for Android
+ content()
+}
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png b/experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png
rename to experimental/examples/imageviewer/shared/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml b/experimental/examples/imageviewer/shared/src/androidMain/res/values-ru/strings.xml
similarity index 96%
rename from experimental/examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml
rename to experimental/examples/imageviewer/shared/src/androidMain/res/values-ru/strings.xml
index 840f22cd..adb38dc8 100755
--- a/experimental/examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml
+++ b/experimental/examples/imageviewer/shared/src/androidMain/res/values-ru/strings.xml
@@ -12,4 +12,5 @@
Изображение:
Размеры:
пикселей.
+ назад
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/values/strings.xml b/experimental/examples/imageviewer/shared/src/androidMain/res/values/strings.xml
similarity index 95%
rename from experimental/examples/imageviewer/common/src/androidMain/res/values/strings.xml
rename to experimental/examples/imageviewer/shared/src/androidMain/res/values/strings.xml
index e515aed2..cbde2d5a 100755
--- a/experimental/examples/imageviewer/common/src/androidMain/res/values/strings.xml
+++ b/experimental/examples/imageviewer/shared/src/androidMain/res/values/strings.xml
@@ -11,4 +11,5 @@
Picture:
Size:
pixels.
+ back
\ No newline at end of file
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt
new file mode 100644
index 00000000..3f4aad2a
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt
@@ -0,0 +1,80 @@
+package example.imageviewer
+
+import androidx.compose.ui.graphics.ImageBitmap
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.core.FilterType
+import example.imageviewer.model.ContentRepository
+import example.imageviewer.model.Picture
+import example.imageviewer.model.name
+import io.ktor.client.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.serialization.json.Json
+
+interface Dependencies {
+ val httpClient: HttpClient
+ val ioScope: CoroutineScope
+ fun getFilter(type: FilterType): BitmapFilter
+ val localization: Localization
+ val imageRepository: ContentRepository
+ val notification: Notification
+ val json: Json get() = jsonReader
+}
+
+interface Notification {
+ fun notifyInvalidRepo()
+ 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 fun showPopUpMessage(text: String)
+
+ 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}"
+ )
+
+ override fun notifyRefreshUnavailable() = showPopUpMessage(
+ """
+ ${localization.noInternet}
+ ${localization.refreshUnavailable}
+ """.trimIndent()
+ )
+}
+
+interface Localization {
+ val back: String
+ val appName: String
+ val loading: String
+ val repoInvalid: String
+ val repoEmpty: String
+ val noInternet: String
+ val loadImageUnavailable: String
+ val lastImage: String
+ val firstImage: String
+ val picture: String
+ val size: String
+ val pixels: String
+ val refreshUnavailable: String
+}
+
+private val jsonReader: Json = Json {
+ ignoreUnknownKeys = true
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageBitmap.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageBitmap.common.kt
new file mode 100644
index 00000000..953bbc5d
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageBitmap.common.kt
@@ -0,0 +1,5 @@
+package example.imageviewer
+
+import androidx.compose.ui.graphics.ImageBitmap
+
+expect fun ByteArray.toImageBitmap(): ImageBitmap
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
new file mode 100644
index 00000000..292836ea
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
@@ -0,0 +1,35 @@
+package example.imageviewer
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import example.imageviewer.model.*
+import example.imageviewer.view.FullscreenImage
+import example.imageviewer.view.MainScreen
+
+@Composable
+internal fun ImageViewerCommon(state: MutableState, dependencies: Dependencies) {
+ state.refresh(dependencies)
+
+ Surface(modifier = Modifier.fillMaxSize()) {
+ when (state.value.screen) {
+ ScreenState.Miniatures -> {
+ MainScreen(state, dependencies)
+ }
+
+ ScreenState.FullScreen -> {
+ FullscreenImage(
+ picture = state.value.picture,
+ getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
+ getFilter = { dependencies.getFilter(it) },
+ localization = dependencies.localization,
+ back = { state.value = state.value.copy(screen = ScreenState.Miniatures) },
+ nextImage = { state.nextImage() },
+ previousImage = { state.previousImage() },
+ )
+ }
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/BitmapFilter.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/BitmapFilter.kt
new file mode 100755
index 00000000..ad8da123
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/BitmapFilter.kt
@@ -0,0 +1,7 @@
+package example.imageviewer.core
+
+import androidx.compose.ui.graphics.ImageBitmap
+
+interface BitmapFilter {
+ fun apply(bitmap: ImageBitmap): ImageBitmap
+}
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt
similarity index 100%
rename from experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt
rename to experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Config.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Config.kt
new file mode 100644
index 00000000..28bfc312
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Config.kt
@@ -0,0 +1,7 @@
+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 MIN_SCALE = 1f
+const val TOAST_DURATION = 3000L
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ContentRepository.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ContentRepository.kt
new file mode 100644
index 00000000..73b19297
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ContentRepository.kt
@@ -0,0 +1,23 @@
+package example.imageviewer.model
+
+import io.ktor.client.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+
+interface ContentRepository {
+ suspend fun loadContent(url: String): T
+}
+
+fun createNetworkRepository(ktorClient: HttpClient) = object : ContentRepository {
+ override suspend fun loadContent(url: String): ByteArray =
+ ktorClient.get(urlString = url).readBytes()
+}
+
+fun ContentRepository.adapter(transform: (A) -> B): ContentRepository {
+ val origin = this
+ return object : ContentRepository {
+ override suspend fun loadContent(url: String): B {
+ return transform(origin.loadContent(url))
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Picture.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Picture.kt
new file mode 100644
index 00000000..7463f873
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Picture.kt
@@ -0,0 +1,11 @@
+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 get() = getNameURL(big)
+val Picture.bigUrl get() = "$BASE_URL/$big"
+val Picture.smallUrl get() = "$BASE_URL/$small"
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ScalableState.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ScalableState.kt
new file mode 100644
index 00000000..6c9bf687
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ScalableState.kt
@@ -0,0 +1,77 @@
+package example.imageviewer.model
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+
+data class ScalableState(
+ val imageSize: IntSize,
+ val boxSize: IntSize = IntSize(1, 1),
+ val offset: IntOffset = IntOffset.Zero,
+ val scale: Float = 1f
+)
+
+val ScalableState.visiblePart
+ get() : IntRect {
+ val boxRatio = boxSize.width.toFloat() / boxSize.height
+ val imageRatio = imageSize.width.toFloat() / imageSize.height.toFloat()
+
+ val size: IntSize =
+ if (boxRatio > imageRatio) {
+ val height = imageSize.height / scale
+ val targetWidth = height * boxRatio
+ IntSize(minOf(imageSize.width, targetWidth.toInt()), height.toInt())
+ } else {
+ val width = imageSize.width / scale
+ val targetHeight = width / boxRatio
+ IntSize(width.toInt(), minOf(imageSize.height, targetHeight.toInt()))
+ }
+
+ return IntRect(offset = offset, size = size)
+ }
+
+fun MutableState.changeBoxSize(size: IntSize) = modifyState {
+ copy(boxSize = size)
+ .updateOffsetLimits()
+}
+
+fun MutableState.setScale(scale: Float) = modifyState {
+ copy(scale = scale)
+ .updateOffsetLimits()
+}
+
+fun MutableState.addScale(diff: Float) = modifyState {
+ if (scale + diff > MAX_SCALE) {
+ copy(scale = MAX_SCALE)
+ } else if (scale + diff < MIN_SCALE) {
+ copy(scale = MIN_SCALE)
+ } else {
+ copy(scale = scale + diff)
+ }.updateOffsetLimits()
+}
+
+fun MutableState.addDragAmount(diff: Offset) = modifyState {
+ copy(offset = offset - IntOffset((diff.x + 1).toInt(), (diff.y + 1).toInt()))
+ .updateOffsetLimits()
+}
+
+private fun ScalableState.updateOffsetLimits(): ScalableState {
+ var result = this
+ if (offset.x + visiblePart.width > imageSize.width) {
+ result = result.changeOffset(x = imageSize.width - visiblePart.width)
+ }
+ if (offset.y + visiblePart.height > imageSize.height) {
+ result = result.changeOffset(y = imageSize.height - visiblePart.height)
+ }
+ if (offset.x < 0) {
+ result = result.changeOffset(x = 0)
+ }
+ if (offset.y < 0) {
+ result = result.changeOffset(y = 0)
+ }
+ return result
+}
+
+private fun ScalableState.changeOffset(x: Int = offset.x, y: Int = offset.y) = copy(offset = IntOffset(x, y))
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/State.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/State.kt
new file mode 100644
index 00000000..593b8309
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/State.kt
@@ -0,0 +1,85 @@
+package example.imageviewer.model
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.graphics.ImageBitmap
+import example.imageviewer.Dependencies
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.serialization.builtins.ListSerializer
+
+data class State(
+ val currentImageIndex: Int = 0,
+ val miniatures: Map = emptyMap(),
+ val pictures: List = emptyList(),
+ val screen: ScreenState = ScreenState.Miniatures
+)
+
+sealed interface ScreenState {
+ object Miniatures : ScreenState
+ object FullScreen : ScreenState
+}
+
+val State.isContentReady get() = pictures.isNotEmpty()
+val State.picture get():Picture? = pictures.getOrNull(currentImageIndex)
+
+fun MutableState.modifyState(modification: T.() -> T) {
+ value = value.modification()
+}
+
+fun MutableState.nextImage() = modifyState {
+ var newIndex = currentImageIndex + 1
+ if (newIndex > pictures.lastIndex) {
+ newIndex = 0
+ }
+ copy(currentImageIndex = newIndex)
+}
+
+fun MutableState.previousImage() = modifyState {
+ var newIndex = currentImageIndex - 1
+ if (newIndex < 0) {
+ newIndex = pictures.lastIndex
+ }
+ copy(currentImageIndex = newIndex)
+}
+
+fun MutableState.refresh(dependencies: Dependencies) {
+ dependencies.ioScope.launch {
+ try {
+ val pictures = dependencies.json.decodeFromString(
+ ListSerializer(Picture.serializer()),
+ dependencies.httpClient.get(PICTURES_DATA_URL).bodyAsText()
+ )
+ val miniatures = pictures.map { picture ->
+ async {
+ picture to dependencies.imageRepository.loadContent(picture.smallUrl)
+ }
+ }.awaitAll().toMap()
+
+ modifyState {
+ copy(pictures = pictures, miniatures = miniatures)
+ }
+ } 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()
+ dependencies.notification.notifyNoInternet()
+ }
+ }
+}
+
+fun MutableState.setSelectedIndex(index: Int) = modifyState {
+ copy(currentImageIndex = index)
+}
+
+fun MutableState.toFullscreen(index: Int = value.currentImageIndex) = modifyState {
+ copy(
+ currentImageIndex = index,
+ screen = ScreenState.FullScreen
+ )
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
new file mode 100755
index 00000000..b63fafac
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
@@ -0,0 +1,41 @@
+package example.imageviewer.style
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+object ImageviewerColors {
+ val Gray = Color.DarkGray
+ val LightGray = Color(100, 100, 100)
+ val DarkGray = Color(32, 32, 32)
+ val PreviewImageAreaHoverColor = Color(45, 45, 45)
+ val ToastBackground = Color(23, 23, 23)
+ val MiniatureColor = Color(50, 50, 50)
+ val MiniatureHoverColor = Color(55, 55, 55)
+ val Foreground = Color(210, 210, 210)
+ val TranslucentBlack = Color(0, 0, 0, 60)
+ val TranslucentWhite = Color(255, 255, 255, 20)
+ val Transparent = Color.Transparent
+
+ fun buttonBackground(isHover: Boolean) = if (isHover) TranslucentBlack else Transparent
+}
+
+@Composable
+internal fun ImageViewerTheme(content: @Composable () -> Unit) {
+ isSystemInDarkTheme() // todo check and change colors
+ MaterialTheme(
+ colors = MaterialTheme.colors.copy(
+ primary = ImageviewerColors.Foreground,
+ secondary = ImageviewerColors.LightGray,
+ background = ImageviewerColors.DarkGray,
+ surface = ImageviewerColors.Gray,
+ onPrimary = ImageviewerColors.Foreground,
+ onSecondary = Color.Black,
+ onBackground = ImageviewerColors.Foreground,
+ onSurface = ImageviewerColors.Foreground
+ )
+ ) {
+ content()
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt
new file mode 100644
index 00000000..4675c84a
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt
@@ -0,0 +1,227 @@
+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.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowLeft
+import androidx.compose.material.icons.filled.KeyboardArrowRight
+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.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.orEmpty
+import org.jetbrains.compose.resources.rememberImageBitmap
+import org.jetbrains.compose.resources.resource
+
+@Composable
+internal fun FullscreenImage(
+ picture: Picture?,
+ getImage: suspend (Picture) -> ImageBitmap,
+ getFilter: (FilterType) -> BitmapFilter,
+ localization: Localization,
+ back: () -> Unit,
+ nextImage: () -> Unit,
+ previousImage: () -> Unit,
+) {
+ val filtersState = remember { mutableStateOf(emptySet()) }
+
+ val originalImageState = remember(picture) { mutableStateOf(null) }
+ LaunchedEffect(picture) {
+ if (picture != null) {
+ originalImageState.value = getImage(picture)
+ }
+ }
+
+ val originalImage = originalImageState.value
+ val filters = filtersState.value
+ val imageWithFilter = remember(originalImage, filters) {
+ if (originalImage != null) {
+ var result: ImageBitmap = originalImage
+ for (filter in filters.map { getFilter(it) }) {
+ result = filter.apply(result)
+ }
+ result
+ } else {
+ null
+ }
+ }
+
+ Box(Modifier.fillMaxSize().background(color = MaterialTheme.colors.background)) {
+ Column {
+ Toolbar(picture?.name ?: "", filtersState, localization, back)
+ if (imageWithFilter != null) {
+ val imageSize = IntSize(imageWithFilter.width, imageWithFilter.height)
+ val scalableState = remember(imageSize) { mutableStateOf(ScalableState(imageSize)) }
+ val visiblePartOfImage: IntRect = scalableState.value.visiblePart
+ Slider(
+ modifier = Modifier.fillMaxWidth(),
+ value = scalableState.value.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
+ )
+ }
+ } else {
+ LoadingScreen()
+ }
+ }
+
+ FloatingActionButton(modifier = Modifier.align(Alignment.BottomStart).padding(10.dp), onClick = previousImage) {
+ Icon(
+ imageVector = Icons.Filled.KeyboardArrowLeft,
+ contentDescription = "Previous",
+ tint = MaterialTheme.colors.primary
+ )
+ }
+ FloatingActionButton(modifier = Modifier.align(Alignment.BottomEnd).padding(10.dp), onClick = nextImage) {
+ Icon(
+ imageVector = Icons.Filled.KeyboardArrowRight,
+ contentDescription = "Next",
+ tint = MaterialTheme.colors.primary
+ )
+ }
+ }
+
+}
+
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+private fun Toolbar(
+ title: String,
+ filtersState: MutableState>,
+ localization: Localization,
+ back: () -> Unit
+) {
+ val backButtonInteractionSource = remember { MutableInteractionSource() }
+ val backButtonHover by backButtonInteractionSource.collectIsHoveredAsState()
+ Surface(
+ modifier = Modifier.height(44.dp)
+ ) {
+ Row(modifier = Modifier.padding(end = 30.dp)) {
+ Surface(
+ color = Color.Transparent,
+ modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
+ shape = CircleShape
+ ) {
+ Tooltip(localization.back) {
+ Image(
+ resource("back.png").rememberImageBitmap().orEmpty(),
+ contentDescription = null,
+ modifier = Modifier.size(38.dp)
+ .hoverable(backButtonInteractionSource)
+ .background(color = ImageviewerColors.buttonBackground(backButtonHover))
+ .clickable { back() }
+ )
+ }
+ }
+ Text(
+ title,
+ maxLines = 1,
+ modifier = Modifier.padding(start = 30.dp).weight(1f)
+ .align(Alignment.CenterVertically),
+ style = MaterialTheme.typography.body1
+ )
+
+ Surface(
+ color = Color(255, 255, 255, 40),
+ modifier = Modifier.size(154.dp, 38.dp)
+ .align(Alignment.CenterVertically),
+ shape = CircleShape
+ ) {
+ Row(Modifier.horizontalScroll(rememberScrollState())) {
+ for (type in FilterType.values()) {
+ FilterButton(filtersState.value.contains(type), type, onClick = {
+ filtersState.value = if (filtersState.value.contains(type)) {
+ filtersState.value - type
+ } else {
+ filtersState.value + 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(38.dp)
+ .hoverable(interactionSource)
+ .background(color = ImageviewerColors.buttonBackground(filterButtonHover))
+ .clickable { onClick() }
+ )
+ }
+ }
+ Spacer(Modifier.width(20.dp))
+}
+
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+private fun getFilterImage(active: Boolean, type: FilterType): ImageBitmap {
+ return when (type) {
+ FilterType.GrayScale -> if (active) {
+ resource("grayscale_on.png").rememberImageBitmap().orEmpty()
+ } else {
+ resource("grayscale_off.png").rememberImageBitmap().orEmpty()
+ }
+
+ FilterType.Pixel -> if (active) {
+ resource("pixel_on.png").rememberImageBitmap().orEmpty()
+ } else {
+ resource("pixel_off.png").rememberImageBitmap().orEmpty()
+ }
+
+ FilterType.Blur -> if (active) {
+ resource("blur_on.png").rememberImageBitmap().orEmpty()
+ } else {
+ resource("blur_off.png").rememberImageBitmap().orEmpty()
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
similarity index 52%
rename from experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
rename to experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
index 8a6a4191..f97df4b3 100644
--- a/experimental/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
@@ -1,11 +1,7 @@
package example.imageviewer.view
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
@@ -15,29 +11,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import example.imageviewer.style.DarkGray
-import example.imageviewer.style.DarkGreen
-import example.imageviewer.style.Foreground
-import example.imageviewer.style.TranslucentBlack
@Composable
-fun LoadingScreen(text: String = "") {
+internal fun LoadingScreen(text: String = "") {
Box(
- modifier = Modifier.fillMaxSize().background(color = TranslucentBlack)
+ modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colors.background)
) {
Box(modifier = Modifier.align(Alignment.Center)) {
- Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
+ Surface(elevation = 4.dp, shape = CircleShape) {
CircularProgressIndicator(
- modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp),
- color = DarkGreen
+ modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp)
)
}
}
Text(
text = text,
modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp),
- style = MaterialTheme.typography.body1,
- color = Foreground
+ style = MaterialTheme.typography.body1
)
}
-}
\ No newline at end of file
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MainScreen.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MainScreen.kt
new file mode 100755
index 00000000..97763df0
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MainScreen.kt
@@ -0,0 +1,85 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import example.imageviewer.Dependencies
+import example.imageviewer.model.*
+import example.imageviewer.model.State
+import example.imageviewer.style.*
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.orEmpty
+import org.jetbrains.compose.resources.rememberImageBitmap
+import org.jetbrains.compose.resources.resource
+
+@Composable
+internal fun MainScreen(state: MutableState, dependencies: Dependencies) {
+ Column {
+ TopContent(state, dependencies)
+ ScrollableColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ for (i in state.value.pictures.indices) {
+ val picture = state.value.pictures[i]
+ Miniature(
+ picture = picture,
+ image = state.value.miniatures[picture],
+ onClickSelect = {
+ state.setSelectedIndex(i)
+ },
+ onClickFullScreen = {
+ state.toFullscreen(i)
+ },
+ onClickInfo = {
+ dependencies.notification.notifyImageData(picture)
+ },
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+ }
+ if (!state.value.isContentReady) {
+ LoadingScreen(dependencies.localization.loading)
+ }
+}
+
+@Composable
+private fun TopContent(state: MutableState, dependencies: Dependencies) {
+ TitleBar(state, dependencies)
+ if (needShowPreview()) {
+ PreviewImage(state = state, getImage = { dependencies.imageRepository.loadContent(it.bigUrl) })
+ }
+}
+
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+private fun TitleBar(state: MutableState, dependencies: Dependencies) {
+ TopAppBar(
+ backgroundColor = MaterialTheme.colors.surface,
+ title = {
+ Row(Modifier.height(50.dp)) {
+ Text(
+ dependencies.localization.appName,
+ modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
+ )
+ Surface(
+ color = ImageviewerColors.Transparent,
+ modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
+ shape = CircleShape
+ ) {
+ Image(
+ bitmap = resource("refresh.png").rememberImageBitmap().orEmpty(),
+ contentDescription = null,
+ modifier = Modifier.size(35.dp).clickable {
+ state.refresh(dependencies)
+ }
+ )
+ }
+ }
+ })
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Miniature.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Miniature.kt
new file mode 100644
index 00000000..1e725cd8
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Miniature.kt
@@ -0,0 +1,71 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import example.imageviewer.model.*
+import example.imageviewer.style.*
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.orEmpty
+import org.jetbrains.compose.resources.rememberImageBitmap
+import org.jetbrains.compose.resources.resource
+
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+internal fun Miniature(
+ picture: Picture,
+ image: ImageBitmap?,
+ onClickSelect: () -> Unit,
+ onClickFullScreen: () -> Unit,
+ onClickInfo: () -> Unit,
+) {
+ Card(
+ backgroundColor = ImageviewerColors.MiniatureColor,
+ modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
+ .fillMaxWidth()
+ .clickable {
+ onClickSelect()
+ },
+ shape = RectangleShape,
+ elevation = 2.dp
+ ) {
+ Row(modifier = Modifier.padding(end = 30.dp)) {
+ val modifier = Modifier.height(70.dp)
+ .width(90.dp)
+ .padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp)
+ if (image != null) {
+ Image(
+ image,
+ contentDescription = null,
+ modifier = modifier.clickable { onClickFullScreen() },
+ contentScale = ContentScale.Crop
+ )
+ } else {
+ CircularProgressIndicator(modifier)
+ }
+ Text(
+ text = picture.name,
+ modifier = Modifier.weight(1f).align(Alignment.CenterVertically).padding(start = 16.dp),
+ style = MaterialTheme.typography.body1
+ )
+
+ Image(
+ resource("dots.png").rememberImageBitmap().orEmpty(),
+ contentDescription = null,
+ modifier = Modifier.height(70.dp)
+ .width(30.dp)
+ .padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp)
+ .clickable { onClickInfo() },
+ contentScale = ContentScale.FillHeight
+ )
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt
new file mode 100644
index 00000000..85ebb73c
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt
@@ -0,0 +1,53 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import example.imageviewer.model.*
+import example.imageviewer.model.State
+import example.imageviewer.style.*
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.orEmpty
+import org.jetbrains.compose.resources.rememberImageBitmap
+import org.jetbrains.compose.resources.resource
+
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+internal fun PreviewImage(state: MutableState, getImage: suspend (Picture) -> ImageBitmap) {
+ val pictures = state.value.pictures
+ val index = state.value.currentImageIndex
+ val imageState = remember(pictures, index) { mutableStateOf(null) }
+ LaunchedEffect(pictures, index) {
+ val picture = pictures.getOrNull(index)
+ if (picture != null) {
+ imageState.value = getImage(picture)
+ }
+ }
+
+ val image = imageState.value
+ Card(
+ backgroundColor = MaterialTheme.colors.background,
+ modifier = Modifier.height(200.dp)
+ .clickable { state.toFullscreen() },
+ shape = RectangleShape,
+ elevation = 1.dp
+ ) {
+ Image(
+ bitmap = image ?: resource("empty.png").rememberImageBitmap().orEmpty(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
+ contentScale = ContentScale.Fit
+ )
+ }
+}
+
+@Composable
+internal expect fun needShowPreview(): Boolean
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScalableImage.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScalableImage.common.kt
new file mode 100644
index 00000000..c21a4d03
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScalableImage.common.kt
@@ -0,0 +1,25 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.gestures.detectTransformGestures
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import example.imageviewer.model.ScalableState
+import example.imageviewer.model.addDragAmount
+import example.imageviewer.model.addScale
+import example.imageviewer.model.setScale
+
+expect fun Modifier.addUserInput(state: MutableState): Modifier
+
+fun Modifier.addTouchUserInput(state: MutableState): Modifier =
+ pointerInput(Unit) {
+ detectTransformGestures { _, pan, zoom, _ ->
+ state.addDragAmount(pan)
+ state.addScale(zoom - 1f)
+ }
+ }.pointerInput(Unit) {
+ detectTapGestures(
+ onDoubleTap = { state.setScale(1f) }
+ )
+ }
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScrollableColumn.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScrollableColumn.common.kt
new file mode 100644
index 00000000..58efdf94
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScrollableColumn.common.kt
@@ -0,0 +1,18 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+expect fun ScrollableColumn(modifier: Modifier, content: @Composable () -> Unit)
+
+@Composable
+fun TouchScrollableColumn(modifier: Modifier, content: @Composable () -> Unit) {
+ val scrollState = rememberScrollState()
+ Column(modifier.verticalScroll(scrollState)) {
+ content()
+ }
+}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
similarity index 53%
rename from experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt
rename to experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
index d2145528..6db9603f 100755
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
@@ -7,53 +7,44 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import example.imageviewer.style.Foreground
-import example.imageviewer.style.ToastBackground
+import example.imageviewer.model.TOAST_DURATION
+import example.imageviewer.style.ImageviewerColors
import kotlinx.coroutines.delay
-enum class ToastDuration(val value: Int) {
- Short(1000), Long(3000)
+sealed interface ToastState {
+ object Hidden : ToastState
+ class Shown(val message: String) : ToastState
}
-private var isShown: Boolean = false
-
@Composable
-fun Toast(
- text: String,
- visibility: MutableState = mutableStateOf(false),
- duration: ToastDuration = ToastDuration.Long
+internal fun Toast(
+ state: MutableState
) {
- if (isShown) {
- return
- }
-
- if (visibility.value) {
- isShown = true
+ val value = state.value
+ if (value is ToastState.Shown) {
Box(
modifier = Modifier.fillMaxSize().padding(bottom = 20.dp),
contentAlignment = Alignment.BottomCenter
) {
Surface(
modifier = Modifier.size(300.dp, 70.dp),
- color = ToastBackground,
+ color = ImageviewerColors.ToastBackground,
shape = RoundedCornerShape(4.dp)
) {
Box(contentAlignment = Alignment.Center) {
- Text(
- text = text,
- color = Foreground
- )
+ Text(value.message)
}
- LaunchedEffect(Unit) {
- delay(duration.value.toLong())
- isShown = false
- visibility.value = false
+ LaunchedEffect(value.message) {
+ delay(TOAST_DURATION)
+ state.value = ToastState.Hidden
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Tooltip.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Tooltip.common.kt
new file mode 100644
index 00000000..e1fa2f74
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Tooltip.common.kt
@@ -0,0 +1,9 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+
+@Composable
+internal expect fun Tooltip(
+ text: String,
+ content: @Composable () -> Unit
+)
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/back.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/back.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/back.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/back.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/blur_off.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/blur_off.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/blur_off.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/blur_off.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/blur_on.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/blur_on.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/blur_on.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/blur_on.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/dots.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/dots.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/dots.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/dots.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/empty.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/empty.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/empty.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/empty.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/grayscale_off.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_off.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/grayscale_off.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_off.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/grayscale_on.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_on.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/grayscale_on.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/grayscale_on.png
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/ic_imageviewer_round.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/ic_imageviewer_round.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/desktopMain/resources/images/ic_imageviewer_round.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/ic_imageviewer_round.png
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/icon-linux.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/icon-linux.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/desktopMain/resources/images/icon-linux.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/icon-linux.png
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/icon-mac.icns b/experimental/examples/imageviewer/shared/src/commonMain/resources/icon-mac.icns
similarity index 100%
rename from experimental/examples/imageviewer/common/src/desktopMain/resources/images/icon-mac.icns
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/icon-mac.icns
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/resources/images/icon-windows.ico b/experimental/examples/imageviewer/shared/src/commonMain/resources/icon-windows.ico
similarity index 100%
rename from experimental/examples/imageviewer/common/src/desktopMain/resources/images/icon-windows.ico
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/icon-windows.ico
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/pixel_off.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/pixel_off.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/pixel_off.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/pixel_off.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/pixel_on.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/pixel_on.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/pixel_on.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/pixel_on.png
diff --git a/experimental/examples/imageviewer/common/src/androidMain/res/drawable/refresh.png b/experimental/examples/imageviewer/shared/src/commonMain/resources/refresh.png
similarity index 100%
rename from experimental/examples/imageviewer/common/src/androidMain/res/drawable/refresh.png
rename to experimental/examples/imageviewer/shared/src/commonMain/resources/refresh.png
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/ImageBitmap.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/ImageBitmap.desktop.kt
new file mode 100644
index 00000000..a04dcda0
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/ImageBitmap.desktop.kt
@@ -0,0 +1,8 @@
+package example.imageviewer
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import org.jetbrains.skia.Image
+
+actual fun ByteArray.toImageBitmap(): ImageBitmap =
+ Image.makeFromEncoded(this).toComposeImageBitmap()
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/R.kt
similarity index 100%
rename from experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt
rename to experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/R.kt
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
new file mode 100755
index 00000000..bbd17363
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
@@ -0,0 +1,14 @@
+package example.imageviewer.model.filtration
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toAwtImage
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.utils.applyBlurFilter
+
+class BlurFilter : BitmapFilter {
+
+ override fun apply(bitmap: ImageBitmap): ImageBitmap {
+ return applyBlurFilter(bitmap.toAwtImage()).toComposeImageBitmap()
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
new file mode 100755
index 00000000..bc6c728b
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
@@ -0,0 +1,14 @@
+package example.imageviewer.model.filtration
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toAwtImage
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.utils.applyGrayScaleFilter
+
+class GrayScaleFilter : BitmapFilter {
+
+ override fun apply(bitmap: ImageBitmap): ImageBitmap {
+ return applyGrayScaleFilter(bitmap.toAwtImage()).toComposeImageBitmap()
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
new file mode 100755
index 00000000..e6820310
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
@@ -0,0 +1,14 @@
+package example.imageviewer.model.filtration
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toAwtImage
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.utils.applyPixelFilter
+
+class PixelFilter : BitmapFilter {
+
+ override fun apply(bitmap: ImageBitmap): ImageBitmap {
+ return applyPixelFilter(bitmap.toAwtImage()).toComposeImageBitmap()
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt
new file mode 100755
index 00000000..e025d844
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt
@@ -0,0 +1,76 @@
+package example.imageviewer.utils
+
+import example.imageviewer.model.ContentRepository
+import example.imageviewer.model.getNameURL
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import java.io.File
+import kotlin.math.absoluteValue
+
+fun ContentRepository.decorateWithDiskCache(
+ backgroundScope: CoroutineScope,
+ cacheDir: File
+): ContentRepository {
+
+ class FileSystemLock
+
+ val origin = this
+ val locksCount = 100
+ val locks = Array(locksCount) { FileSystemLock() }
+
+ fun getLock(url: String) = locks[url.hashCode().absoluteValue % locksCount]
+
+ return object : ContentRepository {
+ init {
+ try {
+ if (!cacheDir.exists()) {
+ cacheDir.mkdirs()
+ }
+ } catch (t: Throwable) {
+ t.printStackTrace()
+ println("Can't create cache dir $cacheDir")
+ }
+ }
+
+ override suspend fun loadContent(url: String): ByteArray {
+ if (!cacheDir.exists()) {
+ return origin.loadContent(url)
+ }
+ val file = cacheDir.resolve("cache-${getNameURL(url)}.png")
+ val fromCache: ByteArray? = synchronized(getLock(url)) {
+ if (file.exists()) {
+ try {
+ file.readBytes()
+ } catch (t: Throwable) {
+ t.printStackTrace()
+ println("Can't read file $file")
+ println("Will work without disk cache")
+ null
+ }
+ } else {
+ null
+ }
+ }
+
+ val result = if (fromCache != null) {
+ fromCache
+ } else {
+ val image = origin.loadContent(url)
+ backgroundScope.launch {
+ synchronized(getLock(url)) {
+ // save to cacheDir
+ try {
+ file.writeBytes(image)
+ } catch (t: Throwable) {
+ println("Can't save image to file $file")
+ println("Will work without disk cache")
+ }
+ }
+ }
+ image
+ }
+ return result
+ }
+
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
new file mode 100755
index 00000000..ca43fd59
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
@@ -0,0 +1,96 @@
+package example.imageviewer.utils
+
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import java.awt.Dimension
+import java.awt.Toolkit
+import java.awt.image.BufferedImage
+import java.awt.image.ConvolveOp
+import java.awt.image.Kernel
+
+fun scaleBitmapAspectRatio(
+ bitmap: BufferedImage,
+ width: Int,
+ height: Int
+): BufferedImage {
+ val boundW: Float = width.toFloat()
+ val boundH: Float = height.toFloat()
+
+ val ratioX: Float = boundW / bitmap.width
+ val ratioY: Float = boundH / bitmap.height
+ val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
+
+ val resultH = (bitmap.height * ratio).toInt()
+ val resultW = (bitmap.width * ratio).toInt()
+
+ val result = BufferedImage(resultW, resultH, BufferedImage.TYPE_INT_ARGB)
+ val graphics = result.createGraphics()
+ graphics.drawImage(bitmap, 0, 0, resultW, resultH, null)
+ graphics.dispose()
+
+ return result
+}
+
+fun applyGrayScaleFilter(bitmap: BufferedImage): BufferedImage {
+
+ val result = BufferedImage(
+ bitmap.width,
+ bitmap.height,
+ BufferedImage.TYPE_BYTE_GRAY
+ )
+
+ val graphics = result.graphics
+ graphics.drawImage(bitmap, 0, 0, null)
+ graphics.dispose()
+
+ return result
+}
+
+fun applyPixelFilter(bitmap: BufferedImage): BufferedImage {
+
+ val w: Int = bitmap.width
+ val h: Int = bitmap.height
+
+ var result = scaleBitmapAspectRatio(bitmap, w / 20, h / 20)
+ result = scaleBitmapAspectRatio(result, w, h)
+
+ return result
+}
+
+fun applyBlurFilter(bitmap: BufferedImage): BufferedImage {
+
+ var result = BufferedImage(bitmap.width, bitmap.height, bitmap.type)
+
+ val graphics = result.graphics
+ graphics.drawImage(bitmap, 0, 0, null)
+ graphics.dispose()
+
+ val radius = 11
+ val size = 11
+ val weight: Float = 1.0f / (size * size)
+ val matrix = FloatArray(size * size)
+
+ for (i in matrix.indices) {
+ matrix[i] = weight
+ }
+
+ val kernel = Kernel(radius, size, matrix)
+ val op = ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null)
+ result = op.filter(result, null)
+
+ return result.getSubimage(
+ radius,
+ radius,
+ result.width - radius * 2,
+ result.height - radius * 2
+ )
+}
+
+fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): DpSize {
+ val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
+ val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
+ val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
+ val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
+ val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
+ return DpSize(width.dp, height.dp)
+}
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt
new file mode 100755
index 00000000..9a4a6380
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt
@@ -0,0 +1,120 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.window.*
+import example.imageviewer.*
+import example.imageviewer.Notification
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.core.FilterType
+import example.imageviewer.model.*
+import example.imageviewer.model.State
+import example.imageviewer.model.filtration.BlurFilter
+import example.imageviewer.model.filtration.GrayScaleFilter
+import example.imageviewer.model.filtration.PixelFilter
+import example.imageviewer.style.ImageViewerTheme
+import example.imageviewer.utils.decorateWithDiskCache
+import example.imageviewer.utils.getPreferredWindowSize
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import java.io.File
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun ApplicationScope.ImageViewerDesktop() {
+ val toastState = remember { mutableStateOf(ToastState.Hidden) }
+ val state = remember { mutableStateOf(State()) }
+ val ioScope: CoroutineScope = rememberCoroutineScope { Dispatchers.IO }
+ val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) }
+
+ Window(
+ onCloseRequest = ::exitApplication,
+ title = "Image Viewer",
+ state = WindowState(
+ position = WindowPosition.Aligned(Alignment.Center),
+ size = getPreferredWindowSize(800, 1000)
+ ),
+ icon = painterResource("ic_imageviewer_round.png"),
+ onKeyEvent = {
+ if (it.type == KeyEventType.KeyUp) {
+ when (it.key) {
+ Key.DirectionLeft -> state.previousImage()
+ Key.DirectionRight -> state.nextImage()
+ }
+ }
+ false
+ }
+ ) {
+ ImageViewerTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ ImageViewerCommon(
+ state = state,
+ dependencies = dependencies
+ )
+ Toast(toastState)
+ }
+ }
+ }
+}
+
+private fun getDependencies(ioScope: CoroutineScope, toastState: MutableState) = object : Dependencies {
+ override val ioScope: CoroutineScope = ioScope
+ override fun getFilter(type: FilterType): BitmapFilter = when (type) {
+ FilterType.GrayScale -> GrayScaleFilter()
+ FilterType.Pixel -> PixelFilter()
+ FilterType.Blur -> BlurFilter()
+ }
+
+ override val localization: Localization = object : Localization {
+ override val back: String get() = ResString.back
+ override val appName: String get() = ResString.appName
+ override val loading: String get() = ResString.loading
+ override val repoInvalid: String get() = ResString.repoInvalid
+ override val repoEmpty: String get() = ResString.repoEmpty
+ override val noInternet: String get() = ResString.noInternet
+ override val loadImageUnavailable: String get() = ResString.loadImageUnavailable
+ override val lastImage: String get() = ResString.lastImage
+ override val firstImage: String get() = ResString.firstImage
+ override val picture: String get() = ResString.picture
+ override val size: String get() = ResString.size
+ override val pixels: String get() = ResString.pixels
+ override val refreshUnavailable: String get() = ResString.refreshUnavailable
+ }
+
+ override val httpClient: HttpClient = HttpClient(CIO)
+
+ val userHome: String? = System.getProperty("user.home")
+ override val imageRepository: ContentRepository =
+ createNetworkRepository(httpClient)
+ .run {
+ if (userHome != null) {
+ decorateWithDiskCache(
+ ioScope,
+ File(userHome).resolve("Pictures").resolve("imageviewer")
+ )
+ } else {
+ this
+ }
+ }
+ .adapter { it.toImageBitmap() }
+
+ override val notification: Notification = object : PopupNotification(localization) {
+ override fun showPopUpMessage(text: String) {
+ toastState.value = ToastState.Shown(text)
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/PreviewImage.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/PreviewImage.desktop.kt
new file mode 100644
index 00000000..10031947
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/PreviewImage.desktop.kt
@@ -0,0 +1,6 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun needShowPreview(): Boolean = true
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScalaImage.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScalaImage.desktop.kt
new file mode 100644
index 00000000..23f214db
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScalaImage.desktop.kt
@@ -0,0 +1,29 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.pointerInput
+import example.imageviewer.model.ScalableState
+import example.imageviewer.model.addDragAmount
+import example.imageviewer.model.addScale
+
+actual fun Modifier.addUserInput(state: MutableState): Modifier =
+ pointerInput(Unit) {
+ detectDragGestures { change, dragAmount: Offset ->
+ state.addDragAmount(dragAmount)
+ change.consume()
+ }
+ }.pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ val event = awaitPointerEvent()
+ if (event.type == PointerEventType.Scroll) {
+ val delta = event.changes.getOrNull(0)?.scrollDelta ?: Offset.Zero
+ state.addScale(delta.y / 100)
+ }
+ }
+ }
+ }
diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScrollableColumn.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScrollableColumn.desktop.kt
new file mode 100644
index 00000000..48d18135
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScrollableColumn.desktop.kt
@@ -0,0 +1,29 @@
+package example.imageviewer.view
+
+import androidx.compose.foundation.VerticalScrollbar
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.rememberScrollbarAdapter
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+actual fun ScrollableColumn(modifier: Modifier, content: @Composable () -> Unit) {
+ val scrollState = rememberScrollState()
+ Modifier.verticalScroll(scrollState)
+
+ Box(modifier) {
+ Column(modifier.verticalScroll(scrollState)) {
+ content()
+ }
+ VerticalScrollbar(
+ modifier = Modifier.align(Alignment.CenterEnd)
+ .padding(4.dp)
+ .fillMaxHeight(),
+ adapter = rememberScrollbarAdapter(scrollState),
+ )
+ }
+}
diff --git a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.desktop.kt
similarity index 91%
rename from experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt
rename to experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.desktop.kt
index 4cee5684..58489467 100644
--- a/experimental/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt
+++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.desktop.kt
@@ -1,22 +1,21 @@
package example.imageviewer.view
-import androidx.compose.foundation.BoxWithTooltip
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
import androidx.compose.material.Surface
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun Tooltip(
- text: String = "Tooltip",
+internal actual fun Tooltip(
+ text: String,
content: @Composable () -> Unit
) {
TooltipArea(
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageBitmap.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageBitmap.ios.kt
new file mode 100644
index 00000000..75df10bf
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageBitmap.ios.kt
@@ -0,0 +1,7 @@
+package example.imageviewer
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.toComposeImageBitmap
+import org.jetbrains.skia.Image
+
+actual fun ByteArray.toImageBitmap(): ImageBitmap = Image.makeFromEncoded(this).toComposeImageBitmap()
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt
new file mode 100755
index 00000000..34dcbc76
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt
@@ -0,0 +1,83 @@
+package example.imageviewer
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Surface
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ImageBitmap
+import example.imageviewer.core.BitmapFilter
+import example.imageviewer.core.FilterType
+import example.imageviewer.model.ContentRepository
+import example.imageviewer.model.State
+import example.imageviewer.model.adapter
+import example.imageviewer.model.createNetworkRepository
+import example.imageviewer.style.ImageViewerTheme
+import example.imageviewer.view.Toast
+import example.imageviewer.view.ToastState
+import io.ktor.client.*
+import io.ktor.client.engine.darwin.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+
+@Composable
+internal fun ImageViewerIos() {
+ val toastState = remember { mutableStateOf(ToastState.Hidden) }
+ val state = remember { mutableStateOf(State()) }
+ val ioScope: CoroutineScope = rememberCoroutineScope { Dispatchers.Default }
+ val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) }
+
+ ImageViewerTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ ImageViewerCommon(
+ state = state,
+ dependencies = dependencies
+ )
+ Toast(toastState)
+ }
+ }
+}
+
+class StubFilter : BitmapFilter {
+ override fun apply(bitmap: ImageBitmap): ImageBitmap {
+ return bitmap
+ }
+}
+
+private fun getDependencies(ioScope: CoroutineScope, toastState: MutableState) = object : Dependencies {
+ override val ioScope: CoroutineScope = ioScope
+ override fun getFilter(type: FilterType): BitmapFilter = when (type) {
+ FilterType.GrayScale -> StubFilter()
+ FilterType.Pixel -> StubFilter()
+ FilterType.Blur -> StubFilter()
+ }
+
+ override val localization: Localization = object : Localization {
+ override val appName = "ImageViewer"
+ override val loading = "Loading images..."
+ override val repoEmpty = "Repository is empty."
+ override val noInternet = "No internet access."
+ override val repoInvalid = "List of images in current repository is invalid or empty."
+ override val refreshUnavailable = "Cannot refresh images."
+ override val loadImageUnavailable = "Cannot load full size image."
+ override val lastImage = "This is last image."
+ override val firstImage = "This is first image."
+ override val picture = "Picture:"
+ override val size = "Size:"
+ override val pixels = "pixels."
+ override val back = "Back"
+ }
+
+ override val httpClient: HttpClient = HttpClient(Darwin)
+
+ override val imageRepository: ContentRepository =
+ createNetworkRepository(httpClient)
+ .adapter { it.toImageBitmap() }
+
+ override val notification: Notification = object : PopupNotification(localization) {
+ override fun showPopUpMessage(text: String) {
+ toastState.value = ToastState.Shown(text)
+ }
+ }
+}
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/main.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/main.ios.kt
new file mode 100644
index 00000000..20041749
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/main.ios.kt
@@ -0,0 +1,21 @@
+package example.imageviewer
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Application
+import platform.UIKit.UIViewController
+
+fun MainViewController(): UIViewController =
+ Application("Imageviewer") {
+ Column {
+ // To skip upper part of screen.
+ Box(
+ modifier = Modifier
+ .height(30.dp)
+ )
+ ImageViewerIos()
+ }
+ }
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/PreviewImage.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/PreviewImage.ios.kt
new file mode 100644
index 00000000..10031947
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/PreviewImage.ios.kt
@@ -0,0 +1,6 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun needShowPreview(): Boolean = true
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/ScrollableColumn.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/ScrollableColumn.ios.kt
new file mode 100644
index 00000000..7ef544a3
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/ScrollableColumn.ios.kt
@@ -0,0 +1,8 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+actual fun ScrollableColumn(modifier: Modifier, content: @Composable () -> Unit) =
+ TouchScrollableColumn(modifier, content)
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/Tooltip.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/Tooltip.ios.kt
new file mode 100644
index 00000000..3806b151
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/Tooltip.ios.kt
@@ -0,0 +1,12 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun Tooltip(
+ text: String,
+ content: @Composable () -> Unit
+) {
+ //No tooltip for iOS
+ content()
+}
diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/addUserInput.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/addUserInput.kt
new file mode 100644
index 00000000..67889370
--- /dev/null
+++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/addUserInput.kt
@@ -0,0 +1,8 @@
+package example.imageviewer.view
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Modifier
+import example.imageviewer.model.ScalableState
+
+actual fun Modifier.addUserInput(state: MutableState): Modifier =
+ addTouchUserInput(state)