Browse Source

Visual refresh for experimental Image Viewer (#2748)

* Design changes; move to material3

* Use animations to move between different images

* More design changes, rounded corners and animations

* Introduce square gallery view, start with granularizing state management

* Introduce square gallery view, start with granularizing state management

* Make PreviewImage not depend on the whole gallery state

* Move in initialization logic from composition into launched effect

* Highlight currently selected image

* Hoist state for FullscreenImage TopAppBar
Move from Custom Implementation to Material App Bar, use color scheme from main page
Extract hardcoded colors to ImageViewerColors

* Provide floating action buttons with nicer colors

* Provide keyboard events via SharedFlow (remove passing around MutableState in the composable hierarchy as it may potentially violate UDF)
Commonize IOScope initialization

* Provide German translation in shared R-strings

* Move from immutable data classes to Compose-aware State Holders.

* Fix gradlew formatting issue?

* Regenerate gradle wrapper after Android Studio autoformatting debacle

* Resolve rememberCoroutineScope issue

* Provide mock name for remaining picture in repo

* Restore TEAM_ID in project.pbxproj

* Use emptyFlow as default to simplify nullability handling for external events

* Remove extraneous newline and unnecessary print statement

* Provide German translation in XML format
Consistently rename title to "My Memories"

* Remove commented-out code, cleanup rendundant modifiers
Make Title Bar use callbacks instead of accessing ViewModel directly
Add toggle & icon for list and grid view
pull/2761/head
Sebastian Aigner 2 years ago committed by GitHub
parent
commit
0dfba6086b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      experimental/examples/imageviewer/desktopApp/src/jvmMain/kotlin/example/imageviewer/Main.kt
  2. BIN
      experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.jar
  3. 3
      experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties
  4. 12
      experimental/examples/imageviewer/gradlew
  5. 1
      experimental/examples/imageviewer/gradlew.bat
  6. 6
      experimental/examples/imageviewer/iosApp/iosApp.xcodeproj/project.pbxproj
  7. 2
      experimental/examples/imageviewer/shared/build.gradle.kts
  8. 7
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/utils/IOScope.android.kt
  9. 25
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt
  10. 3
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ScalableImage.android.kt
  11. 17
      experimental/examples/imageviewer/shared/src/androidMain/res/values-de/strings.xml
  12. 16
      experimental/examples/imageviewer/shared/src/androidMain/res/values-ru/strings.xml
  13. 3
      experimental/examples/imageviewer/shared/src/androidMain/res/values/strings.xml
  14. 77
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
  15. 77
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/GalleryState.kt
  16. 21
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Picture.kt
  17. 61
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/ScalableState.kt
  18. 85
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/State.kt
  19. 27
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
  20. 5
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/utils/IOScope.kt
  21. 137
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  22. 11
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
  23. 237
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MainScreen.kt
  24. 57
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Miniature.kt
  25. 90
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/PreviewImage.common.kt
  26. 5
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/ScalableImage.common.kt
  27. 3
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Toast.kt
  28. BIN
      experimental/examples/imageviewer/shared/src/commonMain/resources/list_view.png
  29. 32
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/R.kt
  30. 5
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/utils/IOScope.desktop.kt
  31. 117
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt
  32. 3
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScalaImage.desktop.kt
  33. 17
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt
  34. 6
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/utils/IOScope.ios.kt
  35. 3
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/addUserInput.kt
  36. BIN
      experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.jar
  37. 263
      experimental/examples/visual-effects/gradlew
  38. 14
      experimental/examples/visual-effects/gradlew.bat

5
experimental/examples/imageviewer/desktopApp/src/jvmMain/kotlin/example/imageviewer/Main.kt

@ -1,11 +1,8 @@
package example.imageviewer
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.window.application
import example.imageviewer.view.ImageViewerDesktop
fun main() = application {
MaterialTheme {
ImageViewerDesktop()
}
ImageViewerDesktop()
}

BIN
experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

3
experimental/examples/imageviewer/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

12
experimental/examples/imageviewer/gradlew vendored

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -80,10 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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"'
@ -143,12 +143,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac

1
experimental/examples/imageviewer/gradlew.bat vendored

@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

6
experimental/examples/imageviewer/iosApp/iosApp.xcodeproj/project.pbxproj

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
@ -340,7 +340,7 @@
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Imageviewer${TEAM_ID}";
PRODUCT_NAME = "Imageviewer";
PRODUCT_NAME = Imageviewer;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -364,7 +364,7 @@
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Imageviewer${TEAM_ID}";
PRODUCT_NAME = "Imageviewer";
PRODUCT_NAME = Imageviewer;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

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

@ -36,6 +36,8 @@ kotlin {
implementation(compose.material)
implementation("org.jetbrains.compose.components:components-resources:1.3.0-beta04-dev879")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.material3)
}
}
val androidMain by getting {

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

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

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

@ -3,16 +3,18 @@ package example.imageviewer.view
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalContext
import example.imageviewer.*
import example.imageviewer.Dependencies
import example.imageviewer.ImageViewerCommon
import example.imageviewer.Localization
import example.imageviewer.Notification
import example.imageviewer.PopupNotification
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
import example.imageviewer.model.ContentRepository
import example.imageviewer.model.State
import example.imageviewer.model.adapter
import example.imageviewer.model.createNetworkRepository
import example.imageviewer.model.filtration.BlurFilter
@ -20,21 +22,20 @@ import example.imageviewer.model.filtration.GrayScaleFilter
import example.imageviewer.model.filtration.PixelFilter
import example.imageviewer.shared.R
import example.imageviewer.style.ImageViewerTheme
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import example.imageviewer.toImageBitmap
import example.imageviewer.utils.ioDispatcher
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Composable
fun ImageViewerAndroid() {
val context: Context = LocalContext.current
val ioScope = rememberCoroutineScope { Dispatchers.IO }
val ioScope = rememberCoroutineScope { ioDispatcher }
val dependencies = remember(context, ioScope) { getDependencies(context, ioScope) }
val state = remember { mutableStateOf(State()) }
ImageViewerTheme {
ImageViewerCommon(state, dependencies)
ImageViewerCommon(dependencies)
}
}
@ -70,9 +71,7 @@ private fun getDependencies(context: Context, ioScope: CoroutineScope) = object
override val notification: Notification = object : PopupNotification(localization) {
override fun showPopUpMessage(text: String) {
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
}
}

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

@ -1,8 +1,7 @@
package example.imageviewer.view
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import example.imageviewer.model.ScalableState
actual fun Modifier.addUserInput(state: MutableState<ScalableState>) =
actual fun Modifier.addUserInput(state: ScalableState) =
addTouchUserInput(state)

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

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

16
experimental/examples/imageviewer/shared/src/androidMain/res/values-ru/strings.xml

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ImageViewer</string>
<string name="loading">Загружаем изображения...</string>
<string name="repo_empty">Репозиторий пуст.</string>
<string name="no_internet">Нет доступа в интернет.</string>
<string name="repo_invalid">Список изображений в репозитории пуст или имеет неверный формат.</string>
<string name="refresh_unavailable">Невозможно обновить изображения.</string>
<string name="load_image_unavailable">Невозможно загузить полное изображение.</string>
<string name="last_image">Это последнее изображение.</string>
<string name="first_image">Это первое изображение.</string>
<string name="picture">Изображение:</string>
<string name="size">Размеры:</string>
<string name="pixels">пикселей.</string>
<string name="back">назад</string>
</resources>

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

@ -1,5 +1,5 @@
<resources>
<string name="app_name">ImageViewer</string>
<string name="app_name">My Memories</string>
<string name="loading">Loading images...</string>
<string name="repo_empty">Repository is empty.</string>
<string name="no_internet">No internet access.</string>
@ -12,4 +12,5 @@
<string name="size">Size:</string>
<string name="pixels">pixels.</string>
<string name="back">back</string>
<string name="refresh">Refresh</string>
</resources>

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

@ -1,35 +1,72 @@
package example.imageviewer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import example.imageviewer.model.*
import example.imageviewer.model.GalleryScreenState
import example.imageviewer.model.ScreenState
import example.imageviewer.model.bigUrl
import example.imageviewer.view.FullscreenImage
import example.imageviewer.view.MainScreen
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
enum class ExternalImageViewerEvent {
Foward,
Back
}
@Composable
internal fun ImageViewerCommon(state: MutableState<State>, dependencies: Dependencies) {
state.refresh(dependencies)
internal fun ImageViewerCommon(
dependencies: Dependencies,
externalEvents: Flow<ExternalImageViewerEvent> = emptyFlow()
) {
val galleryScreenState = remember { GalleryScreenState() }
Surface(modifier = Modifier.fillMaxSize()) {
when (state.value.screen) {
ScreenState.Miniatures -> {
MainScreen(state, dependencies)
LaunchedEffect(Unit) {
galleryScreenState.refresh(dependencies)
}
LaunchedEffect(Unit) {
externalEvents.collect {
when (it) {
ExternalImageViewerEvent.Foward -> galleryScreenState.nextImage()
ExternalImageViewerEvent.Back -> galleryScreenState.previousImage()
}
}
}
ScreenState.FullScreen -> {
FullscreenImage(
picture = state.value.picture,
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
getFilter = { dependencies.getFilter(it) },
localization = dependencies.localization,
back = { state.value = state.value.copy(screen = ScreenState.Miniatures) },
nextImage = { state.nextImage() },
previousImage = { state.previousImage() },
)
}
Surface(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(
galleryScreenState.screen == ScreenState.Miniatures,
enter = fadeIn(),
exit = fadeOut()
) {
MainScreen(galleryScreenState, dependencies)
}
AnimatedVisibility(
galleryScreenState.screen == ScreenState.FullScreen,
enter = slideInHorizontally { -it },
exit = slideOutHorizontally { -it }) {
FullscreenImage(
picture = galleryScreenState.picture,
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
getFilter = { dependencies.getFilter(it) },
localization = dependencies.localization,
back = {
galleryScreenState.screen = ScreenState.Miniatures
},
nextImage = { galleryScreenState.nextImage() },
previousImage = { galleryScreenState.previousImage() },
)
}
}
}

77
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/GalleryState.kt

@ -0,0 +1,77 @@
package example.imageviewer.model
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.Dependencies
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.ListSerializer
data class PictureWithThumbnail(val picture: Picture, val thumbnail: ImageBitmap)
class GalleryScreenState {
var currentPictureIndex by mutableStateOf(0)
val picturesWithThumbnail = mutableStateListOf<PictureWithThumbnail>()
var screen by mutableStateOf<ScreenState>(ScreenState.Miniatures)
val isContentReady get() = picturesWithThumbnail.isNotEmpty()
val picture get(): Picture? = picturesWithThumbnail.getOrNull(currentPictureIndex)?.picture
fun nextImage() {
currentPictureIndex = (currentPictureIndex + 1).mod(picturesWithThumbnail.lastIndex)
}
fun previousImage() {
currentPictureIndex = (currentPictureIndex - 1).mod(picturesWithThumbnail.lastIndex)
}
fun selectPicture(picture: Picture) {
currentPictureIndex = picturesWithThumbnail.indexOfFirst { it.picture == picture }
}
fun toFullscreen(idx: Int = currentPictureIndex) {
currentPictureIndex = idx
screen = ScreenState.FullScreen
}
fun refresh(dependencies: Dependencies) {
dependencies.ioScope.launch {
try {
val pictures = dependencies.json.decodeFromString(
ListSerializer(Picture.serializer()),
dependencies.httpClient.get(PICTURES_DATA_URL).bodyAsText()
)
val miniatures = pictures
.map { picture ->
async {
picture to dependencies.imageRepository.loadContent(picture.smallUrl)
}
}
.awaitAll()
.map { (pic, bit) -> PictureWithThumbnail(pic, bit) }
picturesWithThumbnail.clear()
picturesWithThumbnail.addAll(miniatures)
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
// https://kotlinlang.org/docs/exception-handling.html#exceptions-aggregation
throw e
} catch (e: Exception) {
e.printStackTrace()
dependencies.notification.notifyNoInternet()
}
}
}
}
sealed interface ScreenState {
object Miniatures : ScreenState
object FullScreen : ScreenState
}

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

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

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

@ -1,17 +1,18 @@
package example.imageviewer.model
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
data class ScalableState(
val imageSize: IntSize,
val boxSize: IntSize = IntSize(1, 1),
val offset: IntOffset = IntOffset.Zero,
val scale: Float = 1f
)
class ScalableState(val imageSize: IntSize) {
var boxSize by mutableStateOf(IntSize(1, 1))
var offset by mutableStateOf(IntOffset.Zero)
var scale by mutableStateOf(1f)
}
val ScalableState.visiblePart
get() : IntRect {
@ -32,46 +33,46 @@ val ScalableState.visiblePart
return IntRect(offset = offset, size = size)
}
fun MutableState<ScalableState>.changeBoxSize(size: IntSize) = modifyState {
copy(boxSize = size)
.updateOffsetLimits()
fun ScalableState.changeBoxSize(size: IntSize) {
boxSize = size
updateOffsetLimits()
}
fun MutableState<ScalableState>.setScale(scale: Float) = modifyState {
copy(scale = scale)
.updateOffsetLimits()
fun ScalableState.setScale(scale: Float) {
this.scale = scale
}
fun MutableState<ScalableState>.addScale(diff: Float) = modifyState {
if (scale + diff > MAX_SCALE) {
copy(scale = MAX_SCALE)
fun ScalableState.addScale(diff: Float) {
scale = if (scale + diff > MAX_SCALE) {
MAX_SCALE
} else if (scale + diff < MIN_SCALE) {
copy(scale = MIN_SCALE)
MIN_SCALE
} else {
copy(scale = scale + diff)
}.updateOffsetLimits()
scale + diff
}
updateOffsetLimits()
}
fun MutableState<ScalableState>.addDragAmount(diff: Offset) = modifyState {
copy(offset = offset - IntOffset((diff.x + 1).toInt(), (diff.y + 1).toInt()))
.updateOffsetLimits()
fun ScalableState.addDragAmount(diff: Offset) {
offset -= IntOffset((diff.x + 1).toInt(), (diff.y + 1).toInt())
updateOffsetLimits()
}
private fun ScalableState.updateOffsetLimits(): ScalableState {
var result = this
private fun ScalableState.updateOffsetLimits() {
if (offset.x + visiblePart.width > imageSize.width) {
result = result.changeOffset(x = imageSize.width - visiblePart.width)
changeOffset(x = imageSize.width - visiblePart.width)
}
if (offset.y + visiblePart.height > imageSize.height) {
result = result.changeOffset(y = imageSize.height - visiblePart.height)
changeOffset(y = imageSize.height - visiblePart.height)
}
if (offset.x < 0) {
result = result.changeOffset(x = 0)
changeOffset(x = 0)
}
if (offset.y < 0) {
result = result.changeOffset(y = 0)
changeOffset(y = 0)
}
return result
}
private fun ScalableState.changeOffset(x: Int = offset.x, y: Int = offset.y) = copy(offset = IntOffset(x, y))
private fun ScalableState.changeOffset(x: Int = offset.x, y: Int = offset.y) {
offset = IntOffset(x, y)
}

85
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/State.kt

@ -1,85 +0,0 @@
package example.imageviewer.model
import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.Dependencies
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.serialization.builtins.ListSerializer
data class State(
val currentImageIndex: Int = 0,
val miniatures: Map<Picture, ImageBitmap> = emptyMap(),
val pictures: List<Picture> = emptyList(),
val screen: ScreenState = ScreenState.Miniatures
)
sealed interface ScreenState {
object Miniatures : ScreenState
object FullScreen : ScreenState
}
val State.isContentReady get() = pictures.isNotEmpty()
val State.picture get():Picture? = pictures.getOrNull(currentImageIndex)
fun <T> MutableState<T>.modifyState(modification: T.() -> T) {
value = value.modification()
}
fun MutableState<State>.nextImage() = modifyState {
var newIndex = currentImageIndex + 1
if (newIndex > pictures.lastIndex) {
newIndex = 0
}
copy(currentImageIndex = newIndex)
}
fun MutableState<State>.previousImage() = modifyState {
var newIndex = currentImageIndex - 1
if (newIndex < 0) {
newIndex = pictures.lastIndex
}
copy(currentImageIndex = newIndex)
}
fun MutableState<State>.refresh(dependencies: Dependencies) {
dependencies.ioScope.launch {
try {
val pictures = dependencies.json.decodeFromString(
ListSerializer(Picture.serializer()),
dependencies.httpClient.get(PICTURES_DATA_URL).bodyAsText()
)
val miniatures = pictures.map { picture ->
async {
picture to dependencies.imageRepository.loadContent(picture.smallUrl)
}
}.awaitAll().toMap()
modifyState {
copy(pictures = pictures, miniatures = miniatures)
}
} catch (e: CancellationException) {
println("Rethrowing CancellationException with original cause")
// https://kotlinlang.org/docs/exception-handling.html#exceptions-aggregation
throw e
} catch (e: Exception) {
e.printStackTrace()
dependencies.notification.notifyNoInternet()
}
}
}
fun MutableState<State>.setSelectedIndex(index: Int) = modifyState {
copy(currentImageIndex = index)
}
fun MutableState<State>.toFullscreen(index: Int = value.currentImageIndex) = modifyState {
copy(
currentImageIndex = index,
screen = ScreenState.FullScreen
)
}

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

@ -1,8 +1,9 @@
package example.imageviewer.style
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
object ImageviewerColors {
@ -18,6 +19,18 @@ object ImageviewerColors {
val TranslucentWhite = Color(255, 255, 255, 20)
val Transparent = Color.Transparent
val KotlinGradient0 = Color(0xFF7F52FF)
val KotlinGradient50 = Color(0xFFC811E2)
val KotlinGradient100 = Color(0xFFE54857)
val kotlinHorizontalGradientBrush = Brush.horizontalGradient(
colors = listOf(
KotlinGradient0,
KotlinGradient50,
KotlinGradient100
)
)
fun buttonBackground(isHover: Boolean) = if (isHover) TranslucentBlack else Transparent
}
@ -25,15 +38,9 @@ object ImageviewerColors {
internal fun ImageViewerTheme(content: @Composable () -> Unit) {
isSystemInDarkTheme() // todo check and change colors
MaterialTheme(
colors = MaterialTheme.colors.copy(
primary = ImageviewerColors.Foreground,
secondary = ImageviewerColors.LightGray,
background = ImageviewerColors.DarkGray,
surface = ImageviewerColors.Gray,
onPrimary = ImageviewerColors.Foreground,
onSecondary = Color.Black,
onBackground = ImageviewerColors.Foreground,
onSurface = ImageviewerColors.Foreground
colorScheme = MaterialTheme.colorScheme.copy(
background = Color(0xFF1B1B1B),
onBackground = Color(0xFFFFFFFF)
)
) {
content()

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

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

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

@ -5,15 +5,14 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.input.key.*
@ -41,7 +40,8 @@ internal fun FullscreenImage(
nextImage: () -> Unit,
previousImage: () -> Unit,
) {
val filtersState = remember { mutableStateOf(emptySet<FilterType>()) }
val availableFilters = FilterType.values().toList()
var selectedFilters by remember { mutableStateOf(emptySet<FilterType>()) }
val originalImageState = remember(picture) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(picture) {
@ -51,11 +51,10 @@ internal fun FullscreenImage(
}
val originalImage = originalImageState.value
val filters = filtersState.value
val imageWithFilter = remember(originalImage, filters) {
val imageWithFilter = remember(originalImage, selectedFilters) {
if (originalImage != null) {
var result: ImageBitmap = originalImage
for (filter in filters.map { getFilter(it) }) {
for (filter in selectedFilters.map { getFilter(it) }) {
result = filter.apply(result)
}
result
@ -63,17 +62,28 @@ internal fun FullscreenImage(
null
}
}
Box(Modifier.fillMaxSize().background(color = MaterialTheme.colors.background)) {
Box(Modifier.fillMaxSize().background(color = MaterialTheme.colorScheme.background)) {
Column {
Toolbar(picture?.name ?: "", filtersState, localization, back)
FullscreenImageBar(
localization,
picture?.name,
back,
availableFilters,
selectedFilters,
onSelectFilter = {
if (it !in selectedFilters) {
selectedFilters += it
} else {
selectedFilters -= it
}
})
if (imageWithFilter != null) {
val imageSize = IntSize(imageWithFilter.width, imageWithFilter.height)
val scalableState = remember(imageSize) { mutableStateOf(ScalableState(imageSize)) }
val visiblePartOfImage: IntRect = scalableState.value.visiblePart
val scalableState = remember(imageSize) { ScalableState(imageSize) }
val visiblePartOfImage: IntRect = scalableState.visiblePart
Slider(
modifier = Modifier.fillMaxWidth(),
value = scalableState.value.scale,
value = scalableState.scale,
valueRange = MIN_SCALE..MAX_SCALE,
onValueChange = { scalableState.setScale(it) },
)
@ -99,84 +109,75 @@ internal fun FullscreenImage(
}
}
FloatingActionButton(modifier = Modifier.align(Alignment.BottomStart).padding(10.dp), onClick = previousImage) {
FloatingActionButton(
modifier = Modifier.align(Alignment.BottomStart).padding(10.dp),
containerColor = ImageviewerColors.KotlinGradient0,
onClick = previousImage
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowLeft,
contentDescription = "Previous",
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.onBackground
)
}
FloatingActionButton(modifier = Modifier.align(Alignment.BottomEnd).padding(10.dp), onClick = nextImage) {
FloatingActionButton(
modifier = Modifier.align(Alignment.BottomEnd).padding(10.dp),
containerColor = ImageviewerColors.KotlinGradient0,
onClick = nextImage
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowRight,
contentDescription = "Next",
tint = MaterialTheme.colors.primary
tint = MaterialTheme.colorScheme.onBackground
)
}
}
}
@OptIn(ExperimentalResourceApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class)
@Composable
private fun Toolbar(
title: String,
filtersState: MutableState<Set<FilterType>>,
private fun FullscreenImageBar(
localization: Localization,
back: () -> Unit
pictureName: String?,
onBack: () -> Unit,
filters: List<FilterType>,
selectedFilters: Set<FilterType>,
onSelectFilter: (FilterType) -> Unit
) {
val backButtonInteractionSource = remember { MutableInteractionSource() }
val backButtonHover by backButtonInteractionSource.collectIsHoveredAsState()
Surface(
modifier = Modifier.height(44.dp)
) {
Row(modifier = Modifier.padding(end = 30.dp)) {
Surface(
color = Color.Transparent,
modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
shape = CircleShape
) {
Tooltip(localization.back) {
Image(
resource("back.png").rememberImageBitmap().orEmpty(),
contentDescription = null,
modifier = Modifier.size(38.dp)
.hoverable(backButtonInteractionSource)
.background(color = ImageviewerColors.buttonBackground(backButtonHover))
.clickable { back() }
)
}
TopAppBar(
modifier = Modifier.background(brush = ImageviewerColors.kotlinHorizontalGradientBrush),
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = ImageviewerColors.Transparent,
titleContentColor = MaterialTheme.colorScheme.onBackground
),
title = {
Text("${localization.picture} ${pictureName ?: "Unknown"}")
},
navigationIcon = {
Tooltip(localization.back) {
Image(
resource("back.png").rememberImageBitmap().orEmpty(),
contentDescription = null,
modifier = Modifier.size(38.dp)
.clip(CircleShape)
.clickable { onBack() }
)
}
Text(
title,
maxLines = 1,
modifier = Modifier.padding(start = 30.dp).weight(1f)
.align(Alignment.CenterVertically),
style = MaterialTheme.typography.body1
)
Surface(
color = Color(255, 255, 255, 40),
modifier = Modifier.size(154.dp, 38.dp)
.align(Alignment.CenterVertically),
shape = CircleShape
) {
Row(Modifier.horizontalScroll(rememberScrollState())) {
for (type in FilterType.values()) {
FilterButton(filtersState.value.contains(type), type, onClick = {
filtersState.value = if (filtersState.value.contains(type)) {
filtersState.value - type
} else {
filtersState.value + type
}
})
}
}
},
actions = {
for (type in filters) {
FilterButton(active = type in selectedFilters,
type,
onClick = {
onSelectFilter(type)
})
}
}
}
)
}
@Composable
private fun FilterButton(
active: Boolean,

11
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt

@ -3,10 +3,7 @@ package example.imageviewer.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -15,10 +12,10 @@ import androidx.compose.ui.unit.dp
@Composable
internal fun LoadingScreen(text: String = "") {
Box(
modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colors.background)
modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colorScheme.background)
) {
Box(modifier = Modifier.align(Alignment.Center)) {
Surface(elevation = 4.dp, shape = CircleShape) {
Surface(/*elevation = 4.dp, */shape = CircleShape) {
CircularProgressIndicator(
modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp)
)
@ -27,7 +24,7 @@ internal fun LoadingScreen(text: String = "") {
Text(
text = text,
modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp),
style = MaterialTheme.typography.body1
style = MaterialTheme.typography.bodyMedium
)
}
}

237
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MainScreen.kt

@ -1,72 +1,229 @@
package example.imageviewer.view
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import example.imageviewer.Dependencies
import example.imageviewer.model.*
import example.imageviewer.model.State
import example.imageviewer.style.*
import example.imageviewer.model.GalleryScreenState
import example.imageviewer.model.Picture
import example.imageviewer.model.PictureWithThumbnail
import example.imageviewer.model.bigUrl
import example.imageviewer.style.ImageviewerColors
import example.imageviewer.style.ImageviewerColors.kotlinHorizontalGradientBrush
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.orEmpty
import org.jetbrains.compose.resources.rememberImageBitmap
import org.jetbrains.compose.resources.resource
@Composable
internal fun MainScreen(state: MutableState<State>, dependencies: Dependencies) {
Column {
TopContent(state, dependencies)
ScrollableColumn(
modifier = Modifier.fillMaxSize()
) {
for (i in state.value.pictures.indices) {
val picture = state.value.pictures[i]
Miniature(
picture = picture,
image = state.value.miniatures[picture],
onClickSelect = {
state.setSelectedIndex(i)
},
onClickFullScreen = {
state.toFullscreen(i)
},
onClickInfo = {
dependencies.notification.notifyImageData(picture)
},
)
Spacer(modifier = Modifier.height(4.dp))
}
internal fun GalleryHeader() {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(10.dp).fillMaxWidth()
) {
Text(
"My Gallery",
fontSize = 25.sp,
color = MaterialTheme.colorScheme.onBackground,
fontStyle = FontStyle.Italic
)
}
}
enum class GalleryStyle {
SQUARES,
LIST
}
fun GalleryStyle.toggled(): GalleryStyle {
return if (this == GalleryStyle.SQUARES) GalleryStyle.LIST else GalleryStyle.SQUARES
}
@Composable
internal fun MainScreen(galleryScreenState: GalleryScreenState, dependencies: Dependencies) {
var galleryStyle by remember { mutableStateOf(GalleryStyle.SQUARES) }
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
TitleBar(
onRefresh = { galleryScreenState.refresh(dependencies) },
onToggle = { galleryStyle = galleryStyle.toggled() },
dependencies
)
if (needShowPreview()) {
PreviewImage(
getImage = { dependencies.imageRepository.loadContent(it.bigUrl) },
picture = galleryScreenState.picture, onClick = {
galleryScreenState.toFullscreen()
})
}
when (galleryStyle) {
GalleryStyle.SQUARES -> SquaresGalleryView(
galleryScreenState.picturesWithThumbnail,
galleryScreenState.picturesWithThumbnail.getOrNull(galleryScreenState.currentPictureIndex),
onSelect = { galleryScreenState.selectPicture(it) }
)
GalleryStyle.LIST -> ListGalleryView(
galleryScreenState.picturesWithThumbnail,
dependencies,
onSelect = { galleryScreenState.selectPicture(it) },
onFullScreen = { galleryScreenState.toFullscreen(it) }
)
}
}
if (!state.value.isContentReady) {
if (!galleryScreenState.isContentReady) {
LoadingScreen(dependencies.localization.loading)
}
}
@Composable
private fun TopContent(state: MutableState<State>, dependencies: Dependencies) {
TitleBar(state, dependencies)
if (needShowPreview()) {
PreviewImage(state = state, getImage = { dependencies.imageRepository.loadContent(it.bigUrl) })
private fun SquaresGalleryView(
images: List<PictureWithThumbnail>,
selectedImage: PictureWithThumbnail?,
onSelect: (Picture) -> Unit
) {
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
item {
MakeNewMemoryMiniature()
}
itemsIndexed(images) { idx, image ->
val isSelected = image == selectedImage
val (picture, bitmap) = image
SquareMiniature(bitmap, onClick = { onSelect(picture) }, isHighlighted = isSelected)
}
}
}
@OptIn(ExperimentalResourceApi::class)
@Composable
private fun TitleBar(state: MutableState<State>, dependencies: Dependencies) {
private fun MakeNewMemoryMiniature() {
Box(
Modifier.aspectRatio(1.0f)
.clickable {
// TODO: Open Camera!
}, contentAlignment = Alignment.Center
) {
Text(
"+",
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.onBackground,
textAlign = TextAlign.Center,
fontSize = 50.sp
)
}
}
@Composable
private fun SquareMiniature(image: ImageBitmap, isHighlighted: Boolean, onClick: () -> Unit) {
Image(
bitmap = image,
contentDescription = null,
modifier = Modifier.aspectRatio(1.0f).clickable { onClick() }.then(
if (isHighlighted) {
Modifier.border(BorderStroke(5.dp, Color.White))
} else Modifier
),
contentScale = ContentScale.Crop
)
}
@Composable
private fun ListGalleryView(
pictures: List<PictureWithThumbnail>,
dependencies: Dependencies,
onSelect: (Picture) -> Unit,
onFullScreen: (Int) -> Unit
) {
GalleryHeader()
Spacer(modifier = Modifier.height(10.dp))
ScrollableColumn(
modifier = Modifier.fillMaxSize()
) {
for ((idx, picWithThumb) in pictures.withIndex()) {
val (picture, miniature) = picWithThumb
Miniature(
picture = picture,
image = miniature,
onClickSelect = {
onSelect(picture)
},
onClickFullScreen = {
onFullScreen(idx)
},
onClickInfo = {
dependencies.notification.notifyImageData(picture)
},
)
Spacer(modifier = Modifier.height(10.dp))
}
}
}
@OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun TitleBar(onRefresh: () -> Unit, onToggle: () -> Unit, dependencies: Dependencies) {
TopAppBar(
backgroundColor = MaterialTheme.colors.surface,
modifier = Modifier.background(brush = kotlinHorizontalGradientBrush),
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = ImageviewerColors.Transparent,
titleContentColor = MaterialTheme.colorScheme.onBackground
),
title = {
Row(Modifier.height(50.dp)) {
Text(
dependencies.localization.appName,
modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
modifier = Modifier.weight(1f).align(Alignment.CenterVertically),
fontWeight = FontWeight.Bold
)
Surface(
color = ImageviewerColors.Transparent,
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
shape = CircleShape
) {
Image(
bitmap = resource("list_view.png").rememberImageBitmap().orEmpty(),
contentDescription = null,
modifier = Modifier.size(35.dp).clickable {
onToggle()
}
)
}
Surface(
color = ImageviewerColors.Transparent,
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
@ -76,7 +233,7 @@ private fun TitleBar(state: MutableState<State>, dependencies: Dependencies) {
bitmap = resource("refresh.png").rememberImageBitmap().orEmpty(),
contentDescription = null,
modifier = Modifier.size(35.dp).clickable {
state.refresh(dependencies)
onRefresh()
}
)
}

57
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/Miniature.kt

@ -1,24 +1,38 @@
package example.imageviewer.view
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.model.*
import example.imageviewer.style.*
import example.imageviewer.model.Picture
import example.imageviewer.model.name
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.orEmpty
import org.jetbrains.compose.resources.rememberImageBitmap
import org.jetbrains.compose.resources.resource
@OptIn(ExperimentalResourceApi::class)
@OptIn(ExperimentalResourceApi::class, ExperimentalMaterial3Api::class)
@Composable
internal fun Miniature(
picture: Picture,
@ -28,24 +42,28 @@ internal fun Miniature(
onClickInfo: () -> Unit,
) {
Card(
backgroundColor = ImageviewerColors.MiniatureColor,
modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
.fillMaxWidth()
.clickable {
onClickSelect()
},
shape = RectangleShape,
elevation = 2.dp
.fillMaxWidth(),
onClick = { onClickSelect() },
shape = RoundedCornerShape(200.dp),
border = BorderStroke(1.dp, Color.White),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground
)
) {
Row(modifier = Modifier.padding(end = 30.dp)) {
val modifier = Modifier.height(70.dp)
.width(90.dp)
.padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp)
.width(70.dp)
if (image != null) {
Image(
image,
contentDescription = null,
modifier = modifier.clickable { onClickFullScreen() },
modifier = modifier
.clip(CircleShape)
.border(BorderStroke(1.dp, Color.White), CircleShape)
.clickable { onClickFullScreen() },
contentScale = ContentScale.Crop
)
} else {
@ -53,8 +71,9 @@ internal fun Miniature(
}
Text(
text = picture.name,
modifier = Modifier.weight(1f).align(Alignment.CenterVertically).padding(start = 16.dp),
style = MaterialTheme.typography.body1
modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
.padding(start = 16.dp),
style = MaterialTheme.typography.titleLarge
)
Image(

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

@ -1,51 +1,81 @@
package example.imageviewer.view
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.with
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.model.*
import example.imageviewer.model.State
import example.imageviewer.style.*
import example.imageviewer.model.Picture
import example.imageviewer.style.ImageviewerColors.kotlinHorizontalGradientBrush
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.orEmpty
import org.jetbrains.compose.resources.rememberImageBitmap
import org.jetbrains.compose.resources.resource
@OptIn(ExperimentalResourceApi::class)
@OptIn(ExperimentalResourceApi::class, ExperimentalAnimationApi::class)
@Composable
internal fun PreviewImage(state: MutableState<State>, getImage: suspend (Picture) -> ImageBitmap) {
val pictures = state.value.pictures
val index = state.value.currentImageIndex
val imageState = remember(pictures, index) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(pictures, index) {
val picture = pictures.getOrNull(index)
internal fun PreviewImage(
picture: Picture?,
onClick: () -> Unit,
getImage: suspend (Picture) -> ImageBitmap
) {
var image by remember(picture) { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(picture) {
if (picture != null) {
imageState.value = getImage(picture)
image = getImage(picture)
}
}
val image = imageState.value
Spacer(
modifier = Modifier.height(5.dp).fillMaxWidth()
.background(brush = kotlinHorizontalGradientBrush)
)
Card(
backgroundColor = MaterialTheme.colors.background,
modifier = Modifier.height(200.dp)
.clickable { state.toFullscreen() },
shape = RectangleShape,
elevation = 1.dp
.background(brush = kotlinHorizontalGradientBrush)
.padding(10.dp)
.clickable { onClick() },
shape = RoundedCornerShape(10.dp, 10.dp, 10.dp, 10.dp),
) {
Image(
bitmap = image ?: resource("empty.png").rememberImageBitmap().orEmpty(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
contentScale = ContentScale.Fit
)
AnimatedContent(
targetState = image,
transitionSpec = {
slideInVertically(initialOffsetY = { it }) with slideOutVertically(targetOffsetY = { -it })
}
) { imageBitmap ->
if (imageBitmap != null) {
Image(
bitmap = imageBitmap,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
,
contentScale = ContentScale.Crop
)
} else {
Spacer(
modifier = Modifier.fillMaxSize()
.background(brush = kotlinHorizontalGradientBrush)
)
}
}
}
}

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

@ -2,7 +2,6 @@ package example.imageviewer.view
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import example.imageviewer.model.ScalableState
@ -10,9 +9,9 @@ import example.imageviewer.model.addDragAmount
import example.imageviewer.model.addScale
import example.imageviewer.model.setScale
expect fun Modifier.addUserInput(state: MutableState<ScalableState>): Modifier
expect fun Modifier.addUserInput(state: ScalableState): Modifier
fun Modifier.addTouchUserInput(state: MutableState<ScalableState>): Modifier =
fun Modifier.addTouchUserInput(state: ScalableState): Modifier =
pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
state.addDragAmount(pan)

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

@ -5,8 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

32
experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/R.kt

@ -18,23 +18,23 @@ object ResString {
val refresh: String
init {
if (System.getProperty("user.language").equals("ru")) {
appName = "ImageViewer"
loading = "Загружаем изображения..."
repoEmpty = "Репозиторий пуст."
noInternet = "Нет доступа в интернет."
repoInvalid = "Список изображений в репозитории пуст или имеет неверный формат."
refreshUnavailable = "Невозможно обновить изображения."
loadImageUnavailable = "Невозможно загузить полное изображение."
lastImage = "Это последнее изображение."
firstImage = "Это первое изображение."
picture = "Изображение:"
size = "Размеры:"
pixels = "пикселей."
back = "Назад"
refresh = "Обновить"
if (System.getProperty("user.language").equals("de")) {
appName = "Meine Erinnerungen"
loading = "Bilder werden geladen..."
repoEmpty = "Bildverzeichnis ist leer."
noInternet = "Kein Internetzugriff."
repoInvalid = "Bildverzeichnis beschädigt oder leer."
refreshUnavailable = "Kann Bilder nicht aktualisieren."
loadImageUnavailable = "Kann volles Bild nicht laden."
lastImage = "Dies ist das letzte Bild."
firstImage = "Dies ist das erste Bild."
picture = "Bild:"
size = "Abmessungen:"
pixels = "Pixel."
back = "Zurück"
refresh = "Aktualisieren"
} else {
appName = "ImageViewer"
appName = "My Memories"
loading = "Loading images..."
repoEmpty = "Repository is empty."
noInternet = "No internet access."

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

@ -0,0 +1,5 @@
package example.imageviewer.utils
import kotlinx.coroutines.Dispatchers
actual val ioDispatcher = Dispatchers.IO

117
experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt

@ -18,26 +18,40 @@ import example.imageviewer.Notification
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
import example.imageviewer.model.*
import example.imageviewer.model.State
import example.imageviewer.model.filtration.BlurFilter
import example.imageviewer.model.filtration.GrayScaleFilter
import example.imageviewer.model.filtration.PixelFilter
import example.imageviewer.style.ImageViewerTheme
import example.imageviewer.utils.decorateWithDiskCache
import example.imageviewer.utils.getPreferredWindowSize
import example.imageviewer.utils.ioDispatcher
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import java.io.File
class ExternalNavigationEventBus {
private val _events = MutableSharedFlow<ExternalImageViewerEvent>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_LATEST
)
val events = _events.asSharedFlow()
fun produceEvent(event: ExternalImageViewerEvent) {
_events.tryEmit(event)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ApplicationScope.ImageViewerDesktop() {
val toastState = remember { mutableStateOf<ToastState>(ToastState.Hidden) }
val state = remember { mutableStateOf(State()) }
val ioScope: CoroutineScope = rememberCoroutineScope { Dispatchers.IO }
val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher }
val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) }
val externalNavigationEventBus = remember { ExternalNavigationEventBus() }
Window(
onCloseRequest = ::exitApplication,
@ -47,11 +61,17 @@ fun ApplicationScope.ImageViewerDesktop() {
size = getPreferredWindowSize(800, 1000)
),
icon = painterResource("ic_imageviewer_round.png"),
// https://github.com/JetBrains/compose-jb/issues/2741
onKeyEvent = {
if (it.type == KeyEventType.KeyUp) {
when (it.key) {
Key.DirectionLeft -> state.previousImage()
Key.DirectionRight -> state.nextImage()
Key.DirectionLeft -> externalNavigationEventBus.produceEvent(
ExternalImageViewerEvent.Back
)
Key.DirectionRight -> externalNavigationEventBus.produceEvent(
ExternalImageViewerEvent.Foward
)
}
}
false
@ -62,8 +82,8 @@ fun ApplicationScope.ImageViewerDesktop() {
modifier = Modifier.fillMaxSize()
) {
ImageViewerCommon(
state = state,
dependencies = dependencies
dependencies = dependencies,
externalEvents = externalNavigationEventBus.events
)
Toast(toastState)
}
@ -71,50 +91,51 @@ fun ApplicationScope.ImageViewerDesktop() {
}
}
private fun getDependencies(ioScope: CoroutineScope, toastState: MutableState<ToastState>) = object : Dependencies {
override val ioScope: CoroutineScope = ioScope
override fun getFilter(type: FilterType): BitmapFilter = when (type) {
FilterType.GrayScale -> GrayScaleFilter()
FilterType.Pixel -> PixelFilter()
FilterType.Blur -> BlurFilter()
}
private fun getDependencies(ioScope: CoroutineScope, toastState: MutableState<ToastState>) =
object : Dependencies {
override val ioScope: CoroutineScope = ioScope
override fun getFilter(type: FilterType): BitmapFilter = when (type) {
FilterType.GrayScale -> GrayScaleFilter()
FilterType.Pixel -> PixelFilter()
FilterType.Blur -> BlurFilter()
}
override val localization: Localization = object : Localization {
override val back: String get() = ResString.back
override val appName: String get() = ResString.appName
override val loading: String get() = ResString.loading
override val repoInvalid: String get() = ResString.repoInvalid
override val repoEmpty: String get() = ResString.repoEmpty
override val noInternet: String get() = ResString.noInternet
override val loadImageUnavailable: String get() = ResString.loadImageUnavailable
override val lastImage: String get() = ResString.lastImage
override val firstImage: String get() = ResString.firstImage
override val picture: String get() = ResString.picture
override val size: String get() = ResString.size
override val pixels: String get() = ResString.pixels
override val refreshUnavailable: String get() = ResString.refreshUnavailable
}
override val localization: Localization = object : Localization {
override val back: String get() = ResString.back
override val appName: String get() = ResString.appName
override val loading: String get() = ResString.loading
override val repoInvalid: String get() = ResString.repoInvalid
override val repoEmpty: String get() = ResString.repoEmpty
override val noInternet: String get() = ResString.noInternet
override val loadImageUnavailable: String get() = ResString.loadImageUnavailable
override val lastImage: String get() = ResString.lastImage
override val firstImage: String get() = ResString.firstImage
override val picture: String get() = ResString.picture
override val size: String get() = ResString.size
override val pixels: String get() = ResString.pixels
override val refreshUnavailable: String get() = ResString.refreshUnavailable
}
override val httpClient: HttpClient = HttpClient(CIO)
override val httpClient: HttpClient = HttpClient(CIO)
val userHome: String? = System.getProperty("user.home")
override val imageRepository: ContentRepository<ImageBitmap> =
createNetworkRepository(httpClient)
.run {
if (userHome != null) {
decorateWithDiskCache(
ioScope,
File(userHome).resolve("Pictures").resolve("imageviewer")
)
} else {
this
val userHome: String? = System.getProperty("user.home")
override val imageRepository: ContentRepository<ImageBitmap> =
createNetworkRepository(httpClient)
.run {
if (userHome != null) {
decorateWithDiskCache(
ioScope,
File(userHome).resolve("Pictures").resolve("imageviewer")
)
} else {
this
}
}
}
.adapter { it.toImageBitmap() }
.adapter { it.toImageBitmap() }
override val notification: Notification = object : PopupNotification(localization) {
override fun showPopUpMessage(text: String) {
toastState.value = ToastState.Shown(text)
override val notification: Notification = object : PopupNotification(localization) {
override fun showPopUpMessage(text: String) {
toastState.value = ToastState.Shown(text)
}
}
}
}

3
experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ScalaImage.desktop.kt

@ -1,7 +1,6 @@
package example.imageviewer.view
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEventType
@ -10,7 +9,7 @@ import example.imageviewer.model.ScalableState
import example.imageviewer.model.addDragAmount
import example.imageviewer.model.addScale
actual fun Modifier.addUserInput(state: MutableState<ScalableState>): Modifier =
actual fun Modifier.addUserInput(state: ScalableState): Modifier =
pointerInput(Unit) {
detectDragGestures { change, dragAmount: Offset ->
state.addDragAmount(dragAmount)

17
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt

@ -2,31 +2,33 @@ package example.imageviewer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
import example.imageviewer.model.ContentRepository
import example.imageviewer.model.State
import example.imageviewer.model.adapter
import example.imageviewer.model.createNetworkRepository
import example.imageviewer.model.filtration.BlurFilter
import example.imageviewer.model.filtration.GrayScaleFilter
import example.imageviewer.model.filtration.PixelFilter
import example.imageviewer.style.ImageViewerTheme
import example.imageviewer.utils.ioDispatcher
import example.imageviewer.view.Toast
import example.imageviewer.view.ToastState
import io.ktor.client.*
import io.ktor.client.engine.darwin.*
import io.ktor.client.HttpClient
import io.ktor.client.engine.darwin.Darwin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@Composable
internal fun ImageViewerIos() {
val toastState = remember { mutableStateOf<ToastState>(ToastState.Hidden) }
val state = remember { mutableStateOf(State()) }
val ioScope: CoroutineScope = rememberCoroutineScope { Dispatchers.Default }
val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher }
val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) }
ImageViewerTheme {
@ -34,7 +36,6 @@ internal fun ImageViewerIos() {
modifier = Modifier.fillMaxSize()
) {
ImageViewerCommon(
state = state,
dependencies = dependencies
)
Toast(toastState)

6
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/utils/IOScope.ios.kt

@ -0,0 +1,6 @@
package example.imageviewer.utils
import kotlinx.coroutines.Dispatchers
// https://github.com/Kotlin/kotlinx.coroutines/issues/3205
actual val ioDispatcher = Dispatchers.Default

3
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/addUserInput.kt

@ -1,8 +1,7 @@
package example.imageviewer.view
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import example.imageviewer.model.ScalableState
actual fun Modifier.addUserInput(state: MutableState<ScalableState>): Modifier =
actual fun Modifier.addUserInput(state: ScalableState): Modifier =
addTouchUserInput(state)

BIN
experimental/examples/visual-effects/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

263
experimental/examples/visual-effects/gradlew vendored

@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,67 +17,101 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -106,80 +140,101 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
i=`expr $i + 1`
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

14
experimental/examples/visual-effects/gradlew.bat vendored

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

Loading…
Cancel
Save