mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
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
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
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
|
||||
|
||||
@@ -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%
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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() },
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
galleryScreenState.refresh(dependencies)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
externalEvents.collect {
|
||||
when (it) {
|
||||
ExternalImageViewerEvent.Foward -> galleryScreenState.nextImage()
|
||||
ExternalImageViewerEvent.Back -> galleryScreenState.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() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
expect val ioDispatcher: CoroutineDispatcher
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
@@ -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."
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package example.imageviewer.utils
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
actual val ioDispatcher = Dispatchers.IO
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
275
experimental/examples/visual-effects/gradlew
vendored
275
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
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
14
experimental/examples/visual-effects/gradlew.bat
vendored
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
|
||||
|
||||
Reference in New Issue
Block a user