diff --git a/examples/imageviewer/.gitignore b/examples/imageviewer/.gitignore new file mode 100644 index 0000000000..3603efad10 --- /dev/null +++ b/examples/imageviewer/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.gradle +build \ No newline at end of file diff --git a/examples/imageviewer/.idea/codeStyles/Project.xml b/examples/imageviewer/.idea/codeStyles/Project.xml new file mode 100755 index 0000000000..3cc336b934 --- /dev/null +++ b/examples/imageviewer/.idea/codeStyles/Project.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/examples/imageviewer/.idea/codeStyles/codeStyleConfig.xml b/examples/imageviewer/.idea/codeStyles/codeStyleConfig.xml new file mode 100755 index 0000000000..79ee123c2b --- /dev/null +++ b/examples/imageviewer/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/examples/imageviewer/.idea/vcs.xml b/examples/imageviewer/.idea/vcs.xml new file mode 100755 index 0000000000..35eb1ddfbb --- /dev/null +++ b/examples/imageviewer/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/imageviewer/.run/desktop.run.xml b/examples/imageviewer/.run/desktop.run.xml new file mode 100755 index 0000000000..d9335c1be5 --- /dev/null +++ b/examples/imageviewer/.run/desktop.run.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/examples/imageviewer/README.md b/examples/imageviewer/README.md new file mode 100755 index 0000000000..f5c6e45ccf --- /dev/null +++ b/examples/imageviewer/README.md @@ -0,0 +1 @@ +An example of image gallery for remote server image viewing, based on Jetpack Compose UI library (desktop). diff --git a/examples/imageviewer/android/build.gradle.kts b/examples/imageviewer/android/build.gradle.kts new file mode 100755 index 0000000000..f0a7bb5417 --- /dev/null +++ b/examples/imageviewer/android/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("com.android.application") + kotlin("android") + id("org.jetbrains.compose") +} + +android { + compileSdkVersion(30) + + defaultConfig { + minSdkVersion(21) + targetSdkVersion(30) + versionCode = 1 + versionName = "1.0" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation(project(":common")) +} \ No newline at end of file diff --git a/examples/imageviewer/android/src/main/AndroidManifest.xml b/examples/imageviewer/android/src/main/AndroidManifest.xml new file mode 100755 index 0000000000..48862a9d07 --- /dev/null +++ b/examples/imageviewer/android/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/imageviewer/android/src/main/java/imageviewer/MainActivity.kt b/examples/imageviewer/android/src/main/java/imageviewer/MainActivity.kt new file mode 100755 index 0000000000..e71065eb70 --- /dev/null +++ b/examples/imageviewer/android/src/main/java/imageviewer/MainActivity.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.setContent +import example.imageviewer.view.BuildAppUI +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://spvessel.com/iv/images/fetching.list" + ) + + setContent { + BuildAppUI(content) + } + } +} \ No newline at end of file diff --git a/examples/imageviewer/build.gradle.kts b/examples/imageviewer/build.gradle.kts new file mode 100755 index 0000000000..caeaa20b92 --- /dev/null +++ b/examples/imageviewer/build.gradle.kts @@ -0,0 +1,7 @@ +allprojects { + repositories { + google() + jcenter() + maven("https://packages.jetbrains.team/maven/p/ui/dev") + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/build.gradle.kts b/examples/imageviewer/common/build.gradle.kts new file mode 100755 index 0000000000..d67363227f --- /dev/null +++ b/examples/imageviewer/common/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.compose.compose + +plugins { + id("com.android.library") + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + android() + jvm("desktop") + + sourceSets { + named("commonMain") { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + } + } + named("androidMain") { + dependencies { + api("androidx.appcompat:appcompat:1.1.0") + api("androidx.core:core-ktx:1.3.1") + } + } + named("desktopMain") { + dependencies { + api(compose.desktop.common) + } + } + } +} + +android { + compileSdkVersion(30) + + defaultConfig { + minSdkVersion(21) + targetSdkVersion(30) + versionCode = 1 + versionName = "1.0" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + sourceSets { + named("main") { + manifest.srcFile("src/androidMain/AndroidManifest.xml") + res.srcDirs("src/androidMain/res") + } + } +} diff --git a/examples/imageviewer/common/src/androidMain/AndroidManifest.xml b/examples/imageviewer/common/src/androidMain/AndroidManifest.xml new file mode 100755 index 0000000000..69c7572d0f --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt new file mode 100755 index 0000000000..1e14522811 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt @@ -0,0 +1,7 @@ +package example.imageviewer.core + +import android.graphics.Bitmap + +interface BitmapFilter { + fun apply(bitmap: Bitmap) : Bitmap +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt new file mode 100755 index 0000000000..9a26915889 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt @@ -0,0 +1,375 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + +import android.content.Context +import android.graphics.* +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +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 example.imageviewer.R +import java.io.File +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) + isAppUIReady.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 isAppUIReady = mutableStateOf(false) + fun isContentReady(): Boolean { + return isAppUIReady.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 + // @Composable + fun initData() { + if (isAppUIReady.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 + ) + isAppUIReady.value = true + } + return@execute + } + + val pictureList = loadImages(directory, imageList) + + if (pictureList.isEmpty()) { + handler.post { + showPopUpMessage( + getString(R.string.repo_empty), + context + ) + isAppUIReady.value = true + } + } 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() + } + isAppUIReady.value = true + } + } + } else { + handler.post { + showPopUpMessage( + getString(R.string.no_internet), + context + ) + isAppUIReady.value = true + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + // preview/fullscreen image managing + fun isMainImageEmpty(): Boolean { + return MainImageWrapper.isEmpty() + } + + fun setMainImage(picture: Picture) { + if (MainImageWrapper.getId() == picture.id) + return + + executor.execute { + if (isInternetAvailable()) { + + val fullSizePicture = loadFullImage(picture.source) + fullSizePicture.id = picture.id + + handler.post { + wrapPictureIntoMainImage(fullSizePicture) + } + } else { + handler.post { + showPopUpMessage( + "${getString(R.string.no_internet)}\n${getString(R.string.load_image_unavailable)}", + context + ) + wrapPictureIntoMainImage(picture) + } + } + } + } + + 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) + miniatures.clear() + isAppUIReady.value = false + } + } 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 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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt new file mode 100755 index 0000000000..6c2df5f1df --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt new file mode 100755 index 0000000000..894c6fdad0 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + +import android.graphics.Bitmap + +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt new file mode 100755 index 0000000000..3e05416472 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt @@ -0,0 +1,13 @@ +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt new file mode 100755 index 0000000000..d353a55d00 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt @@ -0,0 +1,12 @@ +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt new file mode 100755 index 0000000000..5427c63a0d --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt @@ -0,0 +1,57 @@ +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) + else -> { + EmptyFilter() + } + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt new file mode 100755 index 0000000000..44ed1f1e3c --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt @@ -0,0 +1,12 @@ +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt new file mode 100755 index 0000000000..c9a265a7e2 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt @@ -0,0 +1,12 @@ +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt new file mode 100755 index 0000000000..e89a60fa70 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.style + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageAsset +import androidx.compose.ui.res.imageResource +import example.imageviewer.R + +@Composable +fun icEmpty(): ImageAsset = imageResource(R.raw.empty) + +@Composable +fun icBack(): ImageAsset = imageResource(R.raw.back) + +@Composable +fun icRefresh(): ImageAsset = imageResource(R.raw.refresh) + +@Composable +fun icDots(): ImageAsset = imageResource(R.raw.dots) + +@Composable +fun icFilterGrayscaleOn(): ImageAsset = imageResource(R.raw.grayscale_on) + +@Composable +fun icFilterGrayscaleOff(): ImageAsset = imageResource(R.raw.grayscale_off) + +@Composable +fun icFilterPixelOn(): ImageAsset = imageResource(R.raw.pixel_on) + +@Composable +fun icFilterPixelOff(): ImageAsset = imageResource(R.raw.pixel_off) + +@Composable +fun icFilterBlurOn(): ImageAsset = imageResource(R.raw.blur_on) + +@Composable +fun icFilterBlurOff(): ImageAsset = imageResource(R.raw.blur_off) + +@Composable +fun icFilterUnknown(): ImageAsset = imageResource(R.raw.filter_unknown) diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt new file mode 100755 index 0000000000..5931f8f91f --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt new file mode 100755 index 0000000000..c45adf6571 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 + +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 +} diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt new file mode 100755 index 0000000000..c51a1b63df --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 BuildAppUI(content: ContentState) { + + Surface( + modifier = Modifier.fillMaxSize(), + color = Gray + ) { + when (AppState.screenState()) { + ScreenType.Main -> { + if (content.isContentReady()) { + setMainScreen(content) + } else { + setLoadingScreen(content) + } + } + ScreenType.FullscreenImage -> { + setImageFullScreen(content) + } + } + } +} + +fun showPopUpMessage(text: String, context: Context) { + Toast.makeText( + context, + text, + Toast.LENGTH_SHORT + ).show() +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt new file mode 100755 index 0000000000..a1b07c621f --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt @@ -0,0 +1,288 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import android.content.Context +import android.content.res.Configuration +import android.graphics.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.foundation.clickable +import androidx.compose.foundation.ScrollableRow +import androidx.compose.foundation.Image +import androidx.compose.foundation.Text +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageAsset +import androidx.compose.ui.graphics.asImageAsset +import androidx.compose.material.MaterialTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.RowScope.gravity +import androidx.compose.material.Surface +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.Foreground +import example.imageviewer.style.MiniatureColor +import example.imageviewer.style.Transparent +import example.imageviewer.style.icBack +import example.imageviewer.style.icFilterGrayscaleOn +import example.imageviewer.style.icFilterGrayscaleOff +import example.imageviewer.style.icFilterPixelOn +import example.imageviewer.style.icFilterPixelOff +import example.imageviewer.style.icFilterBlurOn +import example.imageviewer.style.icFilterBlurOff +import example.imageviewer.style.icFilterUnknown +import example.imageviewer.style.DarkGray +import example.imageviewer.utils.displayHeight +import example.imageviewer.utils.displayWidth +import example.imageviewer.utils.getDisplayBounds +import example.imageviewer.utils.adjustImageScale +import kotlin.math.abs +import kotlin.math.pow +import kotlin.math.roundToInt + +@Composable +fun setImageFullScreen( + content: ContentState +) { + + Column { + setToolBar(content.getSelectedImageName(), content) + setImage(content) + } +} + +@Composable +fun setToolBar( + text: String, + content: ContentState +) { + + Surface(color = MiniatureColor, modifier = Modifier.preferredHeight(44.dp)) { + Row(modifier = Modifier.padding(end = 30.dp)) { + Surface( + color = Transparent, + modifier = Modifier.padding(start = 20.dp).gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + Clickable( + onClick = { + if (content.isContentReady()) { + content.restoreMainImage() + AppState.screenState(ScreenType.Main) + } + }) { + Image( + icBack(), + modifier = Modifier.preferredSize(38.dp) + ) + } + } + Text( + text, + color = Foreground, + maxLines = 1, + modifier = Modifier.padding(start = 30.dp).weight(1f) + .gravity(Alignment.CenterVertically), + style = MaterialTheme.typography.body1 + ) + + Surface( + color = Color(255, 255, 255, 40), + modifier = Modifier.preferredSize(154.dp, 38.dp) + .gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + ScrollableRow { + Row { + for (type in FilterType.values()) { + FilterButton(content, type) + } + } + } + } + } + } +} + +@Composable +fun FilterButton( + content: ContentState, + type: FilterType, + modifier: Modifier = Modifier.gravity(Alignment.CenterVertically).preferredSize(38.dp) +) { + Surface( + color = Transparent, + modifier = Modifier.gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + Clickable( + onClick = { content.toggleFilter(type) } + ) { + Image( + getFilterImage(type = type, content = content), + modifier + ) + } + } + + Spacer(Modifier.width(20.dp)) +} + +@Composable +fun getFilterImage(type: FilterType, content: ContentState): ImageAsset { + + 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() + else -> { + icFilterUnknown() + } + } +} + +@Composable +fun setImage(content: ContentState) { + + val drag = DragHandler() + val scale = ScaleHandler() + + Surface( + color = DarkGray, + modifier = Modifier.fillMaxSize() + ) { + Draggable(onDrag = drag, modifier = Modifier.fillMaxSize()) { + Scalable(onScale = scale, modifier = Modifier.fillMaxSize()) { + val bitmap = imageByGesture(content, scale, drag) + Image( + asset = bitmap.asImageAsset(), + 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.onCancel() + } + + return bitmap +} + +private 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 + ) +} + +private 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() + ) +} diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt new file mode 100755 index 0000000000..22850700c9 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import android.content.Context +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.ScrollableColumn +import androidx.compose.foundation.Text +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Stack +import androidx.compose.foundation.Box +import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.material.Surface +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.TopAppBar +import androidx.compose.material.Card +import androidx.compose.material.Divider +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.asImageAsset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import example.imageviewer.model.AppState +import example.imageviewer.model.Picture +import example.imageviewer.model.ScreenType +import example.imageviewer.model.ContentState +import example.imageviewer.style.DarkGray +import example.imageviewer.style.DarkGreen +import example.imageviewer.style.Foreground +import example.imageviewer.style.Transparent +import example.imageviewer.style.MiniatureColor +import example.imageviewer.style.LightGray +import example.imageviewer.style.icRefresh +import example.imageviewer.style.icEmpty +import example.imageviewer.style.icDots +import example.imageviewer.R + + +@Composable +fun setMainScreen(content: ContentState) { + Column { + setTopContent(content) + setScrollableArea(content) + } +} + +@Composable +fun setLoadingScreen(content: ContentState) { + + Stack { + Column { + setTopContent(content) + } + Box(modifier = Modifier.gravity(Alignment.Center)) { + Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { + CircularProgressIndicator( + modifier = Modifier.preferredSize(50.dp).padding(4.dp), + color = DarkGreen + ) + } + } + Text( + text = content.getString(R.string.loading), + modifier = Modifier.gravity(Alignment.Center).offset(0.dp, 70.dp), + style = MaterialTheme.typography.body1, + color = Foreground + ) + } +} + +@Composable +fun setTopContent(content: ContentState) { + + setTitleBar(text = "ImageViewer", content = content) + if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) { + setPreviewImageUI(content) + setSpacer(h = 10) + setDivider() + } + setSpacer(h = 5) +} + +@Composable +fun setTitleBar(text: String, content: ContentState) { + + TopAppBar( + backgroundColor = DarkGreen, + title = { + Row(Modifier.preferredHeight(50.dp)) { + Text( + text, + color = Foreground, + modifier = Modifier.weight(1f).gravity(Alignment.CenterVertically) + ) + Surface( + color = Transparent, + modifier = Modifier.padding(end = 20.dp).gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + Clickable( + onClick = { + if (content.isContentReady()) + content.refresh() + }) { + Image( + icRefresh(), + modifier = Modifier.preferredSize(35.dp) + ) + } + } + } + }) +} + +@Composable +fun setPreviewImageUI(content: ContentState) { + + Clickable(onClick = { + AppState.screenState(ScreenType.FullscreenImage) + }) { + Card( + backgroundColor = DarkGray, + modifier = Modifier.preferredHeight(250.dp), + shape = RectangleShape, + elevation = 1.dp + ) { + Image( + if (content.isMainImageEmpty()) { + icEmpty() + } + else { + content.getSelectedImage().asImageAsset() + }, + modifier = Modifier + .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp), + contentScale = ContentScale.Fit + ) + } + } +} + +@Composable +fun setMiniatureUI( + picture: Picture, + content: ContentState +) { + + Card( + backgroundColor = MiniatureColor, + modifier = Modifier.padding(start = 10.dp, end = 10.dp).preferredHeight(70.dp) + .fillMaxWidth() + .clickable { + content.setMainImage(picture) + }, + shape = RectangleShape, + elevation = 2.dp + ) { + Row(modifier = Modifier.padding(end = 30.dp)) { + Clickable( + onClick = { + content.setMainImage(picture) + AppState.screenState(ScreenType.FullscreenImage) + } + ) { + Image( + picture.image.asImageAsset(), + modifier = Modifier.preferredHeight(70.dp) + .preferredWidth(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).gravity(Alignment.CenterVertically).padding(start = 16.dp), + style = MaterialTheme.typography.body1 + ) + + Clickable( + modifier = Modifier.preferredHeight(70.dp) + .preferredWidth(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(), + modifier = Modifier.preferredHeight(70.dp) + .preferredWidth(30.dp) + .padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp), + contentScale = ContentScale.FillHeight + ) + } + } + } +} + +@Composable +fun setScrollableArea(content: ContentState) { + + ScrollableColumn { + var index = 1 + Column { + for (picture in content.getMiniatures()) { + setMiniatureUI( + picture = picture, + content = content + ) + Spacer(modifier = Modifier.height(5.dp)) + index++ + } + } + } +} + +@Composable +fun setDivider() { + + Divider( + color = LightGray, + modifier = Modifier.padding(start = 10.dp, end = 10.dp) + ) +} + +@Composable +fun setSpacer(h: Int) { + + Spacer(modifier = Modifier.height(h.dp)) +} diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml b/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml new file mode 100755 index 0000000000..706b9a150c --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml b/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml new file mode 100755 index 0000000000..706b9a150c --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png new file mode 100755 index 0000000000..048bc8e40f Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png new file mode 100755 index 0000000000..c9208944b7 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png new file mode 100755 index 0000000000..2d2fcf2c58 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png new file mode 100755 index 0000000000..38ee5762aa Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png new file mode 100755 index 0000000000..97ee3fe7a5 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png new file mode 100755 index 0000000000..a52e38227a Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png new file mode 100755 index 0000000000..fcab73e062 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png new file mode 100755 index 0000000000..44e98c60bc Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png new file mode 100755 index 0000000000..371b00a186 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png new file mode 100755 index 0000000000..1c05cfbd31 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png new file mode 100755 index 0000000000..98565fc895 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png new file mode 100755 index 0000000000..ba87f288ff Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png new file mode 100755 index 0000000000..2facfba33e Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png new file mode 100755 index 0000000000..98c5f7b447 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png new file mode 100755 index 0000000000..bad1d38f5a Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png new file mode 100755 index 0000000000..9b44b12b16 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png new file mode 100755 index 0000000000..efe8478dd5 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png new file mode 100755 index 0000000000..0023fa325b Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png new file mode 100755 index 0000000000..422d7185b8 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png new file mode 100755 index 0000000000..1843b0f85b Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/back.png b/examples/imageviewer/common/src/androidMain/res/raw/back.png new file mode 100755 index 0000000000..206b8d4678 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/back.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/blur_off.png b/examples/imageviewer/common/src/androidMain/res/raw/blur_off.png new file mode 100755 index 0000000000..e632616157 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/blur_off.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/blur_on.png b/examples/imageviewer/common/src/androidMain/res/raw/blur_on.png new file mode 100755 index 0000000000..7f5ad81bd6 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/blur_on.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/dots.png b/examples/imageviewer/common/src/androidMain/res/raw/dots.png new file mode 100755 index 0000000000..4eb0c9f1e4 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/dots.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/empty.png b/examples/imageviewer/common/src/androidMain/res/raw/empty.png new file mode 100755 index 0000000000..54e9007671 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/empty.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/filter_unknown.png b/examples/imageviewer/common/src/androidMain/res/raw/filter_unknown.png new file mode 100755 index 0000000000..9193c3f33e Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/filter_unknown.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/grayscale_off.png b/examples/imageviewer/common/src/androidMain/res/raw/grayscale_off.png new file mode 100755 index 0000000000..57fbe7891c Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/grayscale_off.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/grayscale_on.png b/examples/imageviewer/common/src/androidMain/res/raw/grayscale_on.png new file mode 100755 index 0000000000..ffe1f6102b Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/grayscale_on.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/pixel_off.png b/examples/imageviewer/common/src/androidMain/res/raw/pixel_off.png new file mode 100755 index 0000000000..a41ebfe04e Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/pixel_off.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/pixel_on.png b/examples/imageviewer/common/src/androidMain/res/raw/pixel_on.png new file mode 100755 index 0000000000..1482ff8583 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/pixel_on.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/raw/refresh.png b/examples/imageviewer/common/src/androidMain/res/raw/refresh.png new file mode 100755 index 0000000000..3be99c1944 Binary files /dev/null and b/examples/imageviewer/common/src/androidMain/res/raw/refresh.png differ diff --git a/examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml b/examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml new file mode 100755 index 0000000000..1f3b09a7d3 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml @@ -0,0 +1,15 @@ + + + ImageViewer + Загружаем изображения... + Репозиторий пуст. + Нет доступа в интернет. + Список изображений в репозитории пуст или имеет неверный формат. + Невозможно обновить изображения. + Невозможно загузить полное изображение. + Это последнее изображение. + Это первое изображение. + Изображение: + Размеры: + пикселей. + \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/res/values/strings.xml b/examples/imageviewer/common/src/androidMain/res/values/strings.xml new file mode 100755 index 0000000000..e2ae85b003 --- /dev/null +++ b/examples/imageviewer/common/src/androidMain/res/values/strings.xml @@ -0,0 +1,14 @@ + + ImageViewer + Loading images... + Repository is empty. + No internet access. + List of images in current repository is invalid or empty. + Cannot refresh images. + Cannot load full size image. + This is last image. + This is first image. + Picture: + Size: + pixels. + \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt new file mode 100755 index 0000000000..663e4a44c6 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt new file mode 100755 index 0000000000..bdacb1f46f --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.core + +enum class FilterType { + GrayScale, Pixel, Blur +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt new file mode 100755 index 0000000000..ec0c3c7c14 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.core + +interface Repository { + fun get() : T +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt new file mode 100755 index 0000000000..eaced94c9e --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + +import example.imageviewer.core.Repository +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URL + +class ImageRepository( + private val httpsURL: String +) : Repository> { + + override fun get(): MutableList { + val list: MutableList = ArrayList() + try { + val url = URL(httpsURL) + val connection: HttpURLConnection = url.openConnection() as HttpURLConnection + connection.connectTimeout = 5000 + connection.connect() + + val read = BufferedReader(InputStreamReader(connection.inputStream)) + + var line: String? = read.readLine() + while (line != null) { + list.add(line) + line = read.readLine() + } + read.close() + return list + } catch (e: Exception) { + e.printStackTrace() + } + + return list + } +} diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt new file mode 100755 index 0000000000..48fa7eed4c --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + + +class Miniatures( + private var list: MutableList = ArrayList() +) { + fun get(index: Int): Picture { + return list[index] + } + + fun getMiniatures(): List { + return ArrayList(list) + } + + fun setMiniatures(list: List) { + this.list = ArrayList(list) + } + + fun size(): Int { + return list.size + } + + fun clear() { + list = ArrayList() + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt new file mode 100755 index 0000000000..4363a6ca9c --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf + +enum class ScreenType { + Main, FullscreenImage +} + +object AppState { + private var screen: MutableState + init { + screen = mutableStateOf(ScreenType.Main) + } + + fun screenState() : ScreenType { + return screen.value + } + + fun screenState(state: ScreenType) { + screen.value = state + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt new file mode 100755 index 0000000000..3b5ed68876 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 ToastBackground = Color(23, 23, 23) +val MiniatureColor = Color(50,50,50) +val Foreground = Color(210, 210, 210) +val Transparent = Color.Transparent \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt new file mode 100755 index 0000000000..03cda71f39 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.utils + +import java.net.InetAddress + +fun isInternetAvailable(): Boolean { + return try { + val ipAddress: InetAddress = InetAddress.getByName("google.com") + !ipAddress.equals("") + } catch (e: Exception) { + false + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt new file mode 100755 index 0000000000..68f16a541e --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.emptyContent +import androidx.compose.foundation.Box +import androidx.compose.foundation.clickable +import androidx.compose.ui.Modifier + +@Composable +fun Clickable( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + children: @Composable () -> Unit = emptyContent() +) { + Box( + modifier = modifier.clickable { + onClick?.invoke() + } + ) { + children() + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt new file mode 100755 index 0000000000..17df851e0c --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.onDispose +import androidx.compose.ui.Modifier +import androidx.compose.foundation.Interaction +import androidx.compose.foundation.InteractionState +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.gesture.DragObserver +import androidx.compose.ui.gesture.dragGestureFilter +import androidx.compose.runtime.mutableStateOf +import example.imageviewer.core.EventLocker +import example.imageviewer.style.Transparent + +@Composable +fun Draggable( + onDrag: DragHandler, + modifier: Modifier = Modifier, + children: @Composable() () -> Unit +) { + Surface( + color = Transparent, + modifier = modifier.dragGestureFilter( + dragObserver = onDrag + ) + ) { + children() + } +} + +class DragHandler : DragObserver { + + 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 + } + + override fun onStart(downPosition: Offset) { + distance.value = Point(Offset.Zero) + locker.unlock() + } + + override fun onStop(velocity: Offset) { + distance.value = Point(Offset.Zero) + locker.unlock() + } + + override fun onCancel() { + distance.value = Point(Offset.Zero) + locker.lock() + } + + override fun onDrag(dragDistance: Offset): 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) + + return dragDistance + } + + return Offset.Zero + } +} + +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 + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt new file mode 100755 index 0000000000..4bb8748483 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.onDispose +import androidx.compose.ui.gesture.RawScaleObserver +import androidx.compose.ui.gesture.doubleTapGestureFilter +import androidx.compose.ui.gesture.rawScaleGestureFilter +import androidx.compose.ui.Modifier +import androidx.compose.foundation.ContentGravity +import androidx.compose.foundation.Interaction +import androidx.compose.foundation.InteractionState +import androidx.compose.material.Surface +import example.imageviewer.style.Transparent +import androidx.compose.runtime.mutableStateOf + +@Composable +fun Scalable( + onScale: ScaleHandler, + modifier: Modifier = Modifier, + children: @Composable() () -> Unit +) { + Surface( + color = Transparent, + modifier = modifier.rawScaleGestureFilter( + scaleObserver = onScale + ).doubleTapGestureFilter(onDoubleTap = { onScale.resetFactor() }), + ) { + children() + } +} + +class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) : + RawScaleObserver { + val factor = mutableStateOf(1f) + + fun resetFactor() { + if (factor.value > minFactor) + factor.value = minFactor + } + + override 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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt new file mode 100755 index 0000000000..ca5eea4f18 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer + +object ResString { + + val appName: String + val loading: String + val repoEmpty: String + val noInternet: String + val repoInvalid: String + val refreshUnavailable: String + val loadImageUnavailable: String + val lastImage: String + val firstImage: String + val picture: String + val size: String + val pixels: String + + init { + if (System.getProperty("user.language").equals("ru")) { + appName = "ImageViewer" + loading = "Загружаем изображения..." + repoEmpty = "Репозиторий пуст." + noInternet = "Нет доступа в интернет." + repoInvalid = "Список изображений в репозитории пуст или имеет неверный формат." + refreshUnavailable = "Невозможно обновить изображения." + loadImageUnavailable = "Невозможно загузить полное изображение." + lastImage = "Это последнее изображение." + firstImage = "Это первое изображение." + picture = "Изображение:" + size = "Размеры:" + pixels = "пикселей." + } else { + appName = "ImageViewer" + loading = "Loading images..." + repoEmpty = "Repository is empty." + noInternet = "No internet access." + repoInvalid = "List of images in current repository is invalid or empty." + refreshUnavailable = "Cannot refresh images." + loadImageUnavailable = "Cannot load full size image." + lastImage = "This is last image." + firstImage = "This is first image." + picture = "Picture:" + size = "Size:" + pixels = "pixels." + } + } +} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt new file mode 100755 index 0000000000..7db9ab61cb --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ContentState.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ContentState.kt new file mode 100755 index 0000000000..8661385815 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ContentState.kt @@ -0,0 +1,335 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + +import java.awt.image.BufferedImage +import androidx.compose.runtime.Composable +import example.imageviewer.core.FilterType +import example.imageviewer.model.filtration.FiltersManager +import example.imageviewer.utils.clearCache +import example.imageviewer.utils.cacheImagePath +import example.imageviewer.utils.isInternetAvailable +import example.imageviewer.view.showPopUpMessage +import example.imageviewer.ResString +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import javax.swing.SwingUtilities.invokeLater +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf + + +class ContentState( + private val repository: ImageRepository +) { + private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) } + + private val isAppUIReady = mutableStateOf(false) + fun isContentReady(): Boolean { + return isAppUIReady.value + } + + // drawable content + private val mainImageWrapper = MainImageWrapper + 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(): BufferedImage { + return mainImage.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 + } + } + + 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 + @Composable + fun initData() { + + if (isAppUIReady.value) + return + + val directory = File(cacheImagePath) + if (!directory.exists()) { + directory.mkdir() + } + + executor.execute { + try { + if (isInternetAvailable()) { + val imageList = repository.get() + + if (imageList.isEmpty()) { + invokeLater { + showPopUpMessage( + ResString.repoInvalid + ) + isAppUIReady.value = true + } + return@execute + } + + val pictureList = loadImages(cacheImagePath, imageList) + + if (pictureList.isEmpty()) { + invokeLater { + showPopUpMessage( + ResString.repoEmpty + ) + isAppUIReady.value = true + } + } else { + val picture = loadFullImage(imageList[0]) + + invokeLater { + miniatures.setMiniatures(pictureList) + + if (isMainImageEmpty()) { + wrapPictureIntoMainImage(picture) + } else { + appliedFilters.add(mainImageWrapper.getFilters()) + currentImageIndex.value = mainImageWrapper.getId() + } + isAppUIReady.value = true + } + } + } else { + invokeLater { + showPopUpMessage( + ResString.noInternet + ) + isAppUIReady.value = true + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + // preview/fullscreen image managing + fun isMainImageEmpty(): Boolean { + return mainImageWrapper.isEmpty() + } + + fun setMainImage(picture: Picture) { + if (mainImageWrapper.getId() == picture.id) + return + + executor.execute { + if (isInternetAvailable()) { + + val fullSizePicture = loadFullImage(picture.source) + fullSizePicture.id = picture.id + + invokeLater { + wrapPictureIntoMainImage(fullSizePicture) + } + } else { + invokeLater { + showPopUpMessage( + "${ResString.noInternet}\n${ResString.loadImageUnavailable}" + ) + wrapPictureIntoMainImage(picture) + } + } + } + } + + 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(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() { + executor.execute { + if (isInternetAvailable()) { + invokeLater { + clearCache() + miniatures.clear() + isAppUIReady.value = false + } + } else { + invokeLater { + 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) + } + + // 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 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 { + var result = BufferedImage(bitmap.width, bitmap.height, bitmap.type) + val graphics = result.createGraphics() + graphics.drawImage(bitmap, 0, 0, result.width, result.height, null) + return result + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt new file mode 100755 index 0000000000..16d13c83cc --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt new file mode 100755 index 0000000000..0eac8b4655 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.model + +import java.awt.image.BufferedImage + +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt new file mode 100755 index 0000000000..5324f27d83 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt new file mode 100755 index 0000000000..11d9ce6dfb --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt new file mode 100755 index 0000000000..6423ce32d2 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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() + else -> { + EmptyFilter() + } + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt new file mode 100755 index 0000000000..15c3f1f19f --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt new file mode 100755 index 0000000000..2a0eca496b --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt new file mode 100755 index 0000000000..04c6fab717 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.style + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageAsset +import androidx.compose.ui.res.imageResource + +@Composable +fun icEmpty(): ImageAsset = imageResource("images/empty.png") + +@Composable +fun icBack(): ImageAsset = imageResource("images/back.png") + +@Composable +fun icRefresh(): ImageAsset = imageResource("images/refresh.png") + +@Composable +fun icDots(): ImageAsset = imageResource("images/dots.png") + +@Composable +fun icFilterGrayscaleOn(): ImageAsset = imageResource("images/grayscale_on.png") + +@Composable +fun icFilterGrayscaleOff(): ImageAsset = imageResource("images/grayscale_off.png") + +@Composable +fun icFilterPixelOn(): ImageAsset = imageResource("images/pixel_on.png") + +@Composable +fun icFilterPixelOff(): ImageAsset = imageResource("images/pixel_off.png") + +@Composable +fun icFilterBlurOn(): ImageAsset = imageResource("images/blur_on.png") + +@Composable +fun icFilterBlurOff(): ImageAsset = imageResource("images/blur_off.png") + +@Composable +fun icFilterUnknown(): ImageAsset = imageResource("images/filter_unknown.png") diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt new file mode 100755 index 0000000000..ae8db76b4c --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt new file mode 100755 index 0000000000..1097869832 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.utils + +import androidx.compose.desktop.AppManager +import androidx.compose.ui.unit.IntSize +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 + +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): Rectangle { + + 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 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 displayWidth(): Int { + val window = AppManager.getCurrentFocusedWindow() + if (window != null) { + return window.width + } + return 0 +} + +fun displayHeight(): Int { + val window = AppManager.getCurrentFocusedWindow() + if (window != null) { + return window.height + } + return 0 +} + +fun toByteArray(bitmap: BufferedImage) : ByteArray { + val baos = ByteArrayOutputStream() + ImageIO.write(bitmap, "png", baos) + return baos.toByteArray() +} + +fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage { + val result = BufferedImage(crop.width, crop.height, bitmap.type) + val graphics = result.createGraphics() + graphics.drawImage(bitmap, crop.x, crop.y, crop.width, crop.height, null) + return result +} + +fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): IntSize { + 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 IntSize(width, height) +} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt new file mode 100755 index 0000000000..29ddfb46df --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 BuildAppUI(content: ContentState) { + + content.initData() + + Surface( + modifier = Modifier.fillMaxSize(), + color = Gray + ) { + when (AppState.screenState()) { + ScreenType.Main -> { + if (content.isContentReady()) { + setMainScreen(content) + } else { + setLoadingScreen(content) + } + } + ScreenType.FullscreenImage -> { + setImageFullScreen(content) + } + } + } + + Toast(message.value, state) +} + +fun showPopUpMessage(text: String) { + message.value = text + state.value = true +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt new file mode 100755 index 0000000000..7cdb649db3 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt @@ -0,0 +1,282 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import java.awt.image.BufferedImage +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.foundation.clickable +import androidx.compose.foundation.ScrollableRow +import androidx.compose.foundation.Image +import androidx.compose.foundation.Text +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageAsset +import androidx.compose.ui.graphics.asImageAsset +import androidx.compose.ui.layout.ContentScale +import androidx.compose.material.MaterialTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.RowScope.gravity +import androidx.compose.material.Surface +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.Foreground +import example.imageviewer.style.MiniatureColor +import example.imageviewer.style.Transparent +import example.imageviewer.style.icBack +import example.imageviewer.style.icFilterGrayscaleOn +import example.imageviewer.style.icFilterGrayscaleOff +import example.imageviewer.style.icFilterPixelOn +import example.imageviewer.style.icFilterPixelOff +import example.imageviewer.style.icFilterBlurOn +import example.imageviewer.style.icFilterBlurOff +import example.imageviewer.style.icFilterUnknown +import example.imageviewer.style.DarkGray +import example.imageviewer.utils.displayHeight +import example.imageviewer.utils.displayWidth +import example.imageviewer.utils.getDisplayBounds +import example.imageviewer.utils.toByteArray +import example.imageviewer.utils.cropImage +import kotlin.math.abs +import kotlin.math.pow +import kotlin.math.roundToInt +import org.jetbrains.skija.Image +import org.jetbrains.skija.IRect +import java.awt.Rectangle + +@Composable +fun setImageFullScreen( + content: ContentState +) { + + Column { + setToolBar(content.getSelectedImageName(), content) + setImage(content) + } +} + +@Composable +fun setToolBar( + text: String, + content: ContentState +) { + + Surface(color = MiniatureColor, modifier = Modifier.preferredHeight(44.dp)) { + Row(modifier = Modifier.padding(end = 30.dp)) { + Surface( + color = Transparent, + modifier = Modifier.padding(start = 20.dp).gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + Clickable( + onClick = { + if (content.isContentReady()) { + content.restoreMainImage() + AppState.screenState(ScreenType.Main) + } + }) { + Image( + icBack(), + modifier = Modifier.preferredSize(38.dp) + ) + } + } + Text( + text, + color = Foreground, + maxLines = 1, + modifier = Modifier.padding(start = 30.dp).weight(1f) + .gravity(Alignment.CenterVertically), + style = MaterialTheme.typography.body1 + ) + + Surface( + color = Color(255, 255, 255, 40), + modifier = Modifier.preferredSize(154.dp, 38.dp) + .gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + ScrollableRow { + Row { + for (type in FilterType.values()) { + FilterButton(content, type) + } + } + } + } + } + } +} + +@Composable +fun FilterButton( + content: ContentState, + type: FilterType, + modifier: Modifier = Modifier.preferredSize(38.dp) +) { + Surface( + color = Transparent, + modifier = Modifier.gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + Clickable( + onClick = { content.toggleFilter(type)} + ) { + Image( + getFilterImage(type = type, content = content), + modifier + ) + } + } + + Spacer(Modifier.width(20.dp)) +} + +@Composable +fun getFilterImage(type: FilterType, content: ContentState): ImageAsset { + + 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() + else -> { + icFilterUnknown() + } + } +} + +@Composable +fun setImage(content: ContentState) { + val drag = DragHandler() + val scale = ScaleHandler() + + Surface( + color = DarkGray, + modifier = Modifier.fillMaxSize() + ) { + Draggable(onDrag = drag, modifier = Modifier.fillMaxSize()) { + Scalable(onScale = scale, modifier = Modifier.fillMaxSize()) { + Image( + asset = imageByGesture(content, scale, drag).asImageAsset(), + contentScale = ContentScale.Fit + ) + } + } + } +} + +@Composable +fun imageByGesture( + content: ContentState, + scale: ScaleHandler, + drag: DragHandler +): Image { + val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag) + val image = Image.makeFromEncoded(toByteArray(bitmap), IRect(0, 0, bitmap.width, bitmap.height)) + if (scale.factor.value > 1f) + return image + + if (abs(drag.getDistance().x) > displayWidth() / 10) { + if (drag.getDistance().x < 0) { + content.swipeNext() + } else { + content.swipePrevious() + } + drag.onCancel() + } + + return image +} + +private fun cropBitmapByScale(bitmap: BufferedImage, scale: Float, drag: DragHandler): BufferedImage { + + val crop = cropBitmapByBounds( + bitmap, + getDisplayBounds(bitmap), + scale, + drag + ) + return cropImage( + bitmap, + Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y) + ) +} + +private fun cropBitmapByBounds( + bitmap: BufferedImage, + bounds: Rectangle, + 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 *= 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() + + 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) +} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt new file mode 100755 index 0000000000..bb02d51c64 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.asImageAsset +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.foundation.clickable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.ScrollableColumn +import androidx.compose.foundation.Text +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Stack +import androidx.compose.foundation.Box +import androidx.compose.foundation.layout.preferredHeight +import androidx.compose.foundation.layout.preferredWidth +import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.material.Surface +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.TopAppBar +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.ui.unit.dp +import example.imageviewer.model.AppState +import example.imageviewer.model.Picture +import example.imageviewer.model.ScreenType +import example.imageviewer.model.ContentState +import example.imageviewer.style.DarkGray +import example.imageviewer.style.DarkGreen +import example.imageviewer.style.Foreground +import example.imageviewer.style.Transparent +import example.imageviewer.style.MiniatureColor +import example.imageviewer.style.LightGray +import example.imageviewer.style.icRefresh +import example.imageviewer.style.icEmpty +import example.imageviewer.style.icDots +import example.imageviewer.utils.toByteArray +import example.imageviewer.ResString +import org.jetbrains.skija.Image +import org.jetbrains.skija.IRect + +@Composable +fun setMainScreen(content: ContentState) { + Column { + setTopContent(content) + setScrollableArea(content) + } +} + +@Composable +fun setLoadingScreen(content: ContentState) { + + Stack { + Column { + setTopContent(content) + } + Box(modifier = Modifier.gravity(Alignment.Center)) { + Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) { + CircularProgressIndicator( + modifier = Modifier.preferredSize(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp), + color = DarkGreen + ) + } + } + Text( + text = ResString.loading, + modifier = Modifier.gravity(Alignment.Center).offset(0.dp, 70.dp), + style = MaterialTheme.typography.body1, + color = Foreground + ) + } +} + +@Composable +fun setTopContent(content: ContentState) { + + setTitleBar(text = "ImageViewer", content = content) + setPreviewImageUI(content) + setSpacer(h = 10) + setDivider() + setSpacer(h = 5) +} + +@Composable +fun setTitleBar(text: String, content: ContentState) { + + TopAppBar( + backgroundColor = DarkGreen, + title = { + Row(Modifier.preferredHeight(50.dp)) { + Text( + text, + color = Foreground, + modifier = Modifier.weight(1f).gravity(Alignment.CenterVertically) + ) + Surface( + color = Transparent, + modifier = Modifier.padding(end = 20.dp).gravity(Alignment.CenterVertically), + shape = CircleShape + ) { + Clickable( + onClick = { + if (content.isContentReady()) + content.refresh() + }) { + Image( + icRefresh(), + modifier = Modifier.preferredSize(35.dp) + ) + } + } + } + }) +} + +@Composable +fun setPreviewImageUI(content: ContentState) { + + Clickable(onClick = { + AppState.screenState(ScreenType.FullscreenImage) + }) { + Card( + backgroundColor = DarkGray, + modifier = Modifier.preferredHeight(250.dp), + shape = RectangleShape, + elevation = 1.dp + ) { + Image( + if (content.isMainImageEmpty()) + icEmpty() + else Image.makeFromEncoded( + toByteArray(content.getSelectedImage()), + IRect(0, 0, content.getSelectedImage().width, content.getSelectedImage().height) + ).asImageAsset(), + modifier = Modifier + .fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp), + contentScale = ContentScale.Fit + ) + } + } +} + +@Composable +fun setMiniatureUI( + picture: Picture, + content: ContentState +) { + + Card( + backgroundColor = MiniatureColor, + modifier = Modifier.padding(start = 10.dp, end = 10.dp).preferredHeight(70.dp) + .fillMaxWidth() + .clickable { + content.setMainImage(picture) + }, + shape = RectangleShape, + elevation = 2.dp + ) { + Row(modifier = Modifier.padding(end = 30.dp)) { + Clickable( + onClick = { + content.setMainImage(picture) + AppState.screenState(ScreenType.FullscreenImage) + } + ) { + Image( + Image.makeFromEncoded( + toByteArray(picture.image), + IRect(0, 0, picture.image.width, picture.image.height) + ).asImageAsset(), + modifier = Modifier.preferredHeight(70.dp) + .preferredWidth(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).gravity(Alignment.CenterVertically).padding(start = 16.dp), + style = MaterialTheme.typography.body1 + ) + + Clickable( + modifier = Modifier.preferredHeight(70.dp) + .preferredWidth(30.dp), + onClick = { + showPopUpMessage( + "${ResString.picture} " + + "${picture.name} \n" + + "${ResString.size} " + + "${picture.width}x${picture.height} " + + "${ResString.pixels}" + ) + } + ) { + Image( + icDots(), + modifier = Modifier.preferredHeight(70.dp) + .preferredWidth(30.dp) + .padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp), + contentScale = ContentScale.FillHeight + ) + } + } + } +} + +@Composable +fun setScrollableArea(content: ContentState) { + + ScrollableColumn { + var index = 1 + Column { + for (picture in content.getMiniatures()) { + setMiniatureUI( + picture = picture, + content = content + ) + Spacer(modifier = Modifier.height(5.dp)) + index++ + } + } + } +} + +@Composable +fun setDivider() { + + Divider( + color = LightGray, + modifier = Modifier.padding(start = 10.dp, end = 10.dp) + ) +} + +@Composable +fun setSpacer(h: Int) { + + Spacer(modifier = Modifier.height(h.dp)) +} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt new file mode 100755 index 0000000000..cba9f8d4f5 --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer.view + +import androidx.compose.foundation.Box +import androidx.compose.foundation.ContentGravity +import androidx.compose.foundation.layout.preferredSize +import androidx.compose.foundation.Text +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.onActive +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Popup +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlinx.coroutines.delay +import kotlinx.coroutines.GlobalScope +import example.imageviewer.style.ToastBackground +import example.imageviewer.style.Foreground + +enum class ToastDuration(val value: Int) { + Short(1000), Long(3000) +} + +private var isShown: Boolean = false + +@Composable +fun Toast( + text: String, + visibility: MutableState = mutableStateOf(false), + duration: ToastDuration = ToastDuration.Long +) { + if (isShown) { + return + } + + if (visibility.value) { + isShown = true + Popup( + alignment = Alignment.BottomCenter + ) { + Box( + Modifier.preferredSize(300.dp, 70.dp), + backgroundColor = ToastBackground, + shape = RoundedCornerShape(4.dp), + gravity = ContentGravity.Center + ) { + Text( + text = text, + color = Foreground + ) + } + onActive { + GlobalScope.launch { + delay(duration.value.toLong()) + isShown = false + visibility.value = false + } + } + } + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/back.png b/examples/imageviewer/common/src/desktopMain/resources/images/back.png new file mode 100755 index 0000000000..206b8d4678 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/back.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png b/examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png new file mode 100755 index 0000000000..e632616157 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png b/examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png new file mode 100755 index 0000000000..7f5ad81bd6 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/dots.png b/examples/imageviewer/common/src/desktopMain/resources/images/dots.png new file mode 100755 index 0000000000..4eb0c9f1e4 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/dots.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/empty.png b/examples/imageviewer/common/src/desktopMain/resources/images/empty.png new file mode 100755 index 0000000000..54e9007671 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/empty.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png b/examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png new file mode 100755 index 0000000000..9193c3f33e Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png b/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png new file mode 100755 index 0000000000..57fbe7891c Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png b/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png new file mode 100755 index 0000000000..ffe1f6102b Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png b/examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png new file mode 100755 index 0000000000..a41ebfe04e Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png b/examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png new file mode 100755 index 0000000000..1482ff8583 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png differ diff --git a/examples/imageviewer/common/src/desktopMain/resources/images/refresh.png b/examples/imageviewer/common/src/desktopMain/resources/images/refresh.png new file mode 100755 index 0000000000..3be99c1944 Binary files /dev/null and b/examples/imageviewer/common/src/desktopMain/resources/images/refresh.png differ diff --git a/examples/imageviewer/desktop/build.gradle.kts b/examples/imageviewer/desktop/build.gradle.kts new file mode 100755 index 0000000000..981c3132df --- /dev/null +++ b/examples/imageviewer/desktop/build.gradle.kts @@ -0,0 +1,17 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") + java + application +} + +dependencies { + implementation(compose.desktop.all) + implementation(project(":common")) +} + +application { + mainClassName = "example.imageviewer.MainKt" +} \ No newline at end of file diff --git a/examples/imageviewer/desktop/src/main/kotlin/imageviewer/Main.kt b/examples/imageviewer/desktop/src/main/kotlin/imageviewer/Main.kt new file mode 100755 index 0000000000..4346705448 --- /dev/null +++ b/examples/imageviewer/desktop/src/main/kotlin/imageviewer/Main.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.imageviewer + +import androidx.compose.desktop.AppWindow +import example.imageviewer.utils.getPreferredWindowSize +import example.imageviewer.view.BuildAppUI +import example.imageviewer.model.ContentState +import example.imageviewer.model.ImageRepository + +fun main() { + + val content = ContentState( + ImageRepository("https://spvessel.com/iv/images/fetching.list") + ) + + AppWindow("ImageViewer", getPreferredWindowSize(800, 1000)).show { + BuildAppUI(content) + } +} \ No newline at end of file diff --git a/examples/imageviewer/gradle.properties b/examples/imageviewer/gradle.properties new file mode 100755 index 0000000000..4d15d015f8 --- /dev/null +++ b/examples/imageviewer/gradle.properties @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties b/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 0000000000..622ab64a3c --- /dev/null +++ b/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/imageviewer/gradlew b/examples/imageviewer/gradlew new file mode 100755 index 0000000000..fbd7c51583 --- /dev/null +++ b/examples/imageviewer/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$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" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + 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 +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 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, 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" + +exec "$JAVACMD" "$@" diff --git a/examples/imageviewer/gradlew.bat b/examples/imageviewer/gradlew.bat new file mode 100755 index 0000000000..a9f778a7a9 --- /dev/null +++ b/examples/imageviewer/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +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 + +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% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/imageviewer/settings.gradle.kts b/examples/imageviewer/settings.gradle.kts new file mode 100755 index 0000000000..c9e909feca --- /dev/null +++ b/examples/imageviewer/settings.gradle.kts @@ -0,0 +1,15 @@ +buildscript { + repositories { + google() + jcenter() + maven("https://packages.jetbrains.team/maven/p/ui/dev") + } + + dependencies { + classpath("org.jetbrains.compose:compose-gradle-plugin:0.1.0-demo3") + classpath("com.android.tools.build:gradle:4.0.1") + classpath(kotlin("gradle-plugin", version = "1.4.0")) + } +} + +include(":common", ":android", ":desktop") \ No newline at end of file