diff --git a/artwork/imageviewerrepo/pictures.json b/artwork/imageviewerrepo/pictures.json new file mode 100644 index 0000000000..adecca674d --- /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 0000000000..1543e7e338 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 0000000000..cd09c38c2f 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 0000000000..ea908b312c 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 0000000000..e9134db062 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 0000000000..c74d019768 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 0000000000..9c466759b8 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 0000000000..17a4e4fc36 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 0000000000..7269b9dfb8 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 0000000000..40630f7132 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 0000000000..4b8cc6c393 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 0000000000..08335a4fe4 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 0000000000..7da656b68e 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 0000000000..f7dc5754c1 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 a32b16597b..994fd0a8a4 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 63% rename from experimental/examples/imageviewer/.run/desktop.run.xml rename to experimental/examples/imageviewer/.run/desktopApp.run.xml index d9335c1be5..95395e11ee --- 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 0000000000..9784842232 --- /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 0000000000..3a59900d23 --- /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 3d79c52613..c9fca538d1 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. - ![desktop-run-configuration.png](screenshots/desktop-run-configuration.png) +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. + +![run-configurations.png](run-configurations.png) + +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 d5807ca5f3..0000000000 --- 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 53bb8c6160..0000000000 --- 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 0000000000..cdb92d493a --- /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 91% rename from experimental/examples/imageviewer/android/src/main/AndroidManifest.xml rename to experimental/examples/imageviewer/androidApp/src/main/AndroidManifest.xml index 5b1501c058..025f2c4a29 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 bf5d0b8c88..0000000000 --- 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 00d4b026bc..0000000000 --- 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 627b36ac5a..0000000000 --- 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 50a9f33b0d..0000000000 --- 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 aea277489c..0000000000 --- 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 2f31b01069..0000000000 --- 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 35f16ab315..0000000000 --- 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 5567048d68..0000000000 --- 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 a269b37f4a..0000000000 --- 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 77cea5c2be..0000000000 --- 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 7059938cb8..0000000000 --- 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 ab006ef147..0000000000 --- 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 32e234da08..0000000000 --- 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 dacce3b7c3..0000000000 --- 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 1c0e7d73b4..0000000000 --- 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 5509e28050..0000000000 --- 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 9193c3f33e..0000000000 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 f31ab9ca61..0000000000 --- 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 df6cd11ff0..0000000000 --- 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 14178fa238..0000000000 --- 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 4daaca3940..0000000000 --- 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 8e38a79e52..0000000000 --- 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 ca4b822fe4..0000000000 --- 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 d35b09543e..0000000000 --- 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 c2d5a23bfa..0000000000 --- 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 8eae0782da..0000000000 --- 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 eabbadc4ef..0000000000 --- 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 ef9887c4f6..0000000000 --- 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 544121d2d2..0000000000 --- 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 b8dcdbae47..0000000000 --- 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 36e0983da8..0000000000 --- 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 5b02b75527..0000000000 --- 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 1113afb440..0000000000 --- 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 7ae798b3ec..0000000000 --- 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 1174489ac8..0000000000 --- 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 37fc441888..0000000000 --- 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 6b10bbf3b0..0000000000 --- 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 5b3a2cf9c2..0000000000 --- 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 7c06d90124..0000000000 --- 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 192289f20b..0000000000 --- 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 ab006ef147..0000000000 --- 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 711a629426..0000000000 --- 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 ef002711bf..0000000000 --- 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 e8e91e4930..0000000000 --- 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 edf1f4b27b..0000000000 --- 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 e9f6321be0..0000000000 --- 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 206b8d4678..0000000000 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 e632616157..0000000000 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 7f5ad81bd6..0000000000 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 4eb0c9f1e4..0000000000 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 54e9007671..0000000000 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 9193c3f33e..0000000000 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 57fbe7891c..0000000000 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 ffe1f6102b..0000000000 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 a41ebfe04e..0000000000 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 1482ff8583..0000000000 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 3be99c1944..0000000000 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 c17682c405..0000000000 --- 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 2116a0481a..95effc0263 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 0000000000..96fe7556df --- /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 c18fa7d46c..48afff0860 --- 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 f3d88b1c2f..249e5832f0 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 fbd7c51583..a69d9cb6c2 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 +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 -# 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 +# 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. # 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 +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 - 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\"" + 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 - i=`expr $i + 1` + # 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 - 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" ;; - 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; +# * $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. +# -# 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" +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 5093609d51..f127cfd49d 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 0000000000..bf06eb27e9 --- /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 0000000000..2e83e0d5ea --- /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 0000000000..5f87c3d554 --- /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 0000000000..9a269f5eaa --- /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 0000000000..b42016a6fc --- /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 0000000000..0840602f9b 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 f4ac8d1ff7..a2c5064a25 --- 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 0000000000..9d7a377757 --- /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 0000000000..513200737a --- /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 0000000000..a1c412135d --- /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 0000000000..5f9c242d9a --- /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 0000000000..e4ad86770b --- /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 0000000000..ae0a7197f5 --- /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 0000000000..901cd77de7 --- /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 0000000000..366e5427ef --- /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 0000000000..455de53506 --- /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 0000000000..fa88d1804b --- /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 0000000000..7ef544a39d --- /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 0000000000..8e638fd7d1 --- /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 95% 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 840f22cdb2..adb38dc804 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 93% 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 e515aed2e2..cbde2d5a37 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 0000000000..3f4aad2ab4 --- /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 0000000000..953bbc5db9 --- /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 0000000000..292836ead9 --- /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 0000000000..ad8da1231a --- /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 0000000000..28bfc3122f --- /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 0000000000..73b19297ff --- /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 0000000000..7463f87381 --- /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 0000000000..6c9bf68752 --- /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 0000000000..593b830976 --- /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 0000000000..b63fafac57 --- /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 0000000000..4675c84a21 --- /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 8a6a4191f6..f97df4b357 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 0000000000..97763df099 --- /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 0000000000..1e725cd8b0 --- /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 0000000000..85ebb73cc0 --- /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 0000000000..c21a4d0339 --- /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 0000000000..58efdf94f5 --- /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 d214552818..6db9603f25 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 0000000000..e1fa2f7496 --- /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 0000000000..a04dcda010 --- /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 0000000000..bbd1736347 --- /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 0000000000..bc6c728b21 --- /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 0000000000..e6820310e8 --- /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 0000000000..e025d844fe --- /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 0000000000..ca43fd5925 --- /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 0000000000..9a4a6380ea --- /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 0000000000..1003194719 --- /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 0000000000..23f214db17 --- /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 0000000000..48d18135c4 --- /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 4cee5684fc..5848946793 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 0000000000..75df10bf0d --- /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 0000000000..34dcbc76a1 --- /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 0000000000..200417490f --- /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 0000000000..1003194719 --- /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 0000000000..7ef544a39d --- /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 0000000000..3806b15180 --- /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 0000000000..67889370aa --- /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)