From 2cea802cecfdf418c4c93b9689abd965aa376e90 Mon Sep 17 00:00:00 2001 From: "dima.avdeev" <99798741+dima-avdeev-jb@users.noreply.github.com> Date: Mon, 3 Apr 2023 16:21:20 +0300 Subject: [PATCH] ImageViewer delete, edit and share memories (#2957) --- .../imageviewer/shared/build.gradle.kts | 1 + .../src/androidMain/AndroidManifest.xml | 11 +- .../imageviewer/ImageViewerFileProvider.kt | 6 + .../example/imageviewer/platform.android.kt | 8 ++ .../storage/AndroidImageStorage.kt | 69 ++++++++++-- .../view/EditMemoryDialog.android.kt | 77 +++++++++++++ .../imageviewer/view/ImageViewer.android.kt | 27 ++++- .../src/androidMain/res/xml/file_paths.xml | 3 + .../example/imageviewer/Dependencies.kt | 59 ++++++++-- .../example/imageviewer/ImageViewer.common.kt | 10 +- .../kotlin/example/imageviewer/model/Page.kt | 4 +- .../example/imageviewer/model/PictureData.kt | 6 +- .../example/imageviewer/platform.common.kt | 6 + .../example/imageviewer/view/CameraScreen.kt | 6 +- .../view/EditMemoryDialog.common.kt | 11 ++ .../example/imageviewer/view/MemoryScreen.kt | 106 ++++++++++++------ .../imageviewer/DesktopImageStorage.kt | 24 ++-- .../example/imageviewer/platform.desktop.kt | 8 ++ .../view/EditMemoryDialog.desktop.kt | 87 ++++++++++++++ .../imageviewer/view/ImageViewer.desktop.kt | 9 +- .../example/imageviewer/ImageViewer.ios.kt | 37 +++++- .../example/imageviewer/IosShareIcon.kt | 41 +++++++ .../kotlin/example/imageviewer/main.ios.kt | 5 +- .../example/imageviewer/platform.ios.kt | 5 + .../imageviewer/storage/FileExtensions.kt | 4 + .../storage/IosImageStorage.ios.kt | 48 ++++++-- .../imageviewer/view/EditMemoryDialog.ios.kt | 77 +++++++++++++ .../view/LocationVisualizer.ios.kt | 28 +++-- 28 files changed, 683 insertions(+), 100 deletions(-) create mode 100644 experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageViewerFileProvider.kt create mode 100644 experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/EditMemoryDialog.android.kt create mode 100644 experimental/examples/imageviewer/shared/src/androidMain/res/xml/file_paths.xml create mode 100644 experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/EditMemoryDialog.common.kt create mode 100644 experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/EditMemoryDialog.desktop.kt create mode 100644 experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/IosShareIcon.kt create mode 100644 experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/EditMemoryDialog.ios.kt diff --git a/experimental/examples/imageviewer/shared/build.gradle.kts b/experimental/examples/imageviewer/shared/build.gradle.kts index 6826d46797..eaccfc7d17 100755 --- a/experimental/examples/imageviewer/shared/build.gradle.kts +++ b/experimental/examples/imageviewer/shared/build.gradle.kts @@ -35,6 +35,7 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) + //implementation(compose.materialIconsExtended) // TODO not working on iOS @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.components.resources) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") diff --git a/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml b/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml index a077657347..c77ec92f4d 100755 --- a/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml +++ b/experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml @@ -1,7 +1,16 @@ + package="example.imageviewer.shared"> + + + + + \ No newline at end of file diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageViewerFileProvider.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageViewerFileProvider.kt new file mode 100644 index 0000000000..208dd91882 --- /dev/null +++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageViewerFileProvider.kt @@ -0,0 +1,6 @@ +package example.imageviewer + +import androidx.core.content.FileProvider +import example.imageviewer.shared.R + +class ImageViewerFileProvider : FileProvider(R.xml.file_paths) diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt index 1474fff74e..7f11252044 100644 --- a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt +++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt @@ -4,8 +4,12 @@ import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap +import example.imageviewer.model.PictureData import kotlinx.coroutines.Dispatchers import java.util.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.ui.graphics.vector.ImageVector actual fun Modifier.notchPadding(): Modifier = displayCutoutPadding().statusBarsPadding() @@ -18,3 +22,7 @@ actual typealias PlatformStorableImage = AndroidStorableImage actual fun createUUID(): String = UUID.randomUUID().toString() actual val ioDispatcher = Dispatchers.IO + +actual val isShareFeatureSupported: Boolean = true + +actual val shareIcon: ImageVector = Icons.Filled.Share diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/storage/AndroidImageStorage.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/storage/AndroidImageStorage.kt index 2943e88f39..5210b17a85 100644 --- a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/storage/AndroidImageStorage.kt +++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/storage/AndroidImageStorage.kt @@ -2,23 +2,29 @@ package example.imageviewer.storage import android.content.Context import android.graphics.Bitmap +import android.net.Uri import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.content.FileProvider import androidx.core.graphics.scale import example.imageviewer.ImageStorage import example.imageviewer.PlatformStorableImage import example.imageviewer.model.PictureData -import example.imageviewer.toAndroidBitmap import example.imageviewer.toImageBitmap import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.resource import java.io.File +import java.util.UUID + private const val maxStorableImageSizePx = 2000 private const val storableThumbnailSizePx = 200 @@ -29,10 +35,15 @@ class AndroidImageStorage( private val ioScope: CoroutineScope, context: Context ) : ImageStorage { - private val savePictureDir = File(context.filesDir, "takenPhotos") + private val savePictureDir = File(context.filesDir, "taken_photos") + private val sharedImagesDir = File(context.filesDir, "share_images") private val PictureData.Camera.jpgFile get() = File(savePictureDir, "$id.jpg") - private val PictureData.Camera.thumbnailJpgFile get() = File(savePictureDir, "$id-thumbnail.jpg") + private val PictureData.Camera.thumbnailJpgFile + get() = File( + savePictureDir, + "$id-thumbnail.jpg" + ) private val PictureData.Camera.jsonFile get() = File(savePictureDir, "$id.json") init { @@ -53,30 +64,64 @@ class AndroidImageStorage( } } - override fun saveImage(pictureData: PictureData.Camera, image: PlatformStorableImage) { + override fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) { if (image.imageBitmap.width == 0 || image.imageBitmap.height == 0) { return } ioScope.launch { with(image.imageBitmap) { - pictureData.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx)) - pictureData.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx)) + picture.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx)) + picture.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx)) } - pictures.add(0, pictureData) - pictureData.jsonFile.writeText(pictureData.toJson()) + pictures.add(0, picture) + picture.jsonFile.writeText(picture.toJson()) + } + } + + override fun delete(picture: PictureData.Camera) { + ioScope.launch { + picture.jsonFile.delete() + picture.jpgFile.delete() + picture.thumbnailJpgFile.delete() } } - override suspend fun getThumbnail(pictureData: PictureData.Camera): ImageBitmap = + override fun rewrite(picture: PictureData.Camera) { + ioScope.launch { + picture.jsonFile.delete() + picture.jsonFile.writeText(picture.toJson()) + } + } + + override suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap = withContext(ioScope.coroutineContext) { - pictureData.thumbnailJpgFile.readBytes().toImageBitmap() + picture.thumbnailJpgFile.readBytes().toImageBitmap() } - override suspend fun getImage(pictureData: PictureData.Camera): ImageBitmap = + override suspend fun getImage(picture: PictureData.Camera): ImageBitmap = withContext(ioScope.coroutineContext) { - pictureData.jpgFile.readBytes().toImageBitmap() + picture.jpgFile.readBytes().toImageBitmap() } + + @OptIn(ExperimentalResourceApi::class) + suspend fun getUri(context: Context, picture: PictureData): Uri = withContext(Dispatchers.IO) { + val tempFileToShare: File = sharedImagesDir.resolve("share_picture.jpg") + when (picture) { + is PictureData.Camera -> { + picture.jpgFile.copyTo(tempFileToShare, overwrite = true) + } + + is PictureData.Resource -> { + tempFileToShare.writeBytes(resource(picture.resource).readBytes()) + } + } + FileProvider.getUriForFile( + context, + "example.imageviewer.fileprovider", + tempFileToShare + ) + } } private fun ImageBitmap.fitInto(px: Int): ImageBitmap { diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/EditMemoryDialog.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/EditMemoryDialog.android.kt new file mode 100644 index 0000000000..8d748110ba --- /dev/null +++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/EditMemoryDialog.android.kt @@ -0,0 +1,77 @@ +package example.imageviewer.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +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.draw.clip +import androidx.compose.ui.graphics.Color +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 + +@Composable +internal actual fun BoxScope.EditMemoryDialog( + previousName: String, + previousDescription: String, + save: (name: String, description: String) -> Unit +) { + var name by remember { mutableStateOf(previousName) } + var description by remember { mutableStateOf(previousDescription) } + Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + .clickable { + save(name, description) + } + ) { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(30.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.White, + ), + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + TextField( + value = description, + onValueChange = { description = it }, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.White, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ) + ) + } + } +} diff --git a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt index f3dcb74270..54fa4364e4 100755 --- a/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt +++ b/experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt @@ -1,15 +1,22 @@ package example.imageviewer.view import android.content.Context +import android.content.Intent import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import example.imageviewer.* +import example.imageviewer.filter.PlatformContext +import example.imageviewer.model.PictureData import example.imageviewer.storage.AndroidImageStorage import example.imageviewer.style.ImageViewerTheme import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + @Composable fun ImageViewerAndroid() { @@ -27,5 +34,23 @@ private fun getDependencies(context: Context, ioScope: CoroutineScope) = object Toast.makeText(context, text, Toast.LENGTH_SHORT).show() } } - override val imageStorage: ImageStorage = AndroidImageStorage(pictures, ioScope, context) + override val imageStorage: AndroidImageStorage = AndroidImageStorage(pictures, ioScope, context) + override val sharePicture: SharePicture = object : SharePicture { + override fun share(context: PlatformContext, picture: PictureData) { + ioScope.launch { + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra( + Intent.EXTRA_STREAM, + imageStorage.getUri(context.androidContext, picture) + ) + type = "image/jpeg" + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + withContext(Dispatchers.Main) { + context.androidContext.startActivity(Intent.createChooser(shareIntent, null)) + } + } + } + } } diff --git a/experimental/examples/imageviewer/shared/src/androidMain/res/xml/file_paths.xml b/experimental/examples/imageviewer/shared/src/androidMain/res/xml/file_paths.xml new file mode 100644 index 0000000000..30ca54464b --- /dev/null +++ b/experimental/examples/imageviewer/shared/src/androidMain/res/xml/file_paths.xml @@ -0,0 +1,3 @@ + + + diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt index 00cda2474f..c976f982e4 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.ImageBitmap +import example.imageviewer.filter.PlatformContext import example.imageviewer.model.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -14,6 +15,7 @@ import org.jetbrains.compose.resources.resource abstract class Dependencies { abstract val notification: Notification abstract val imageStorage: ImageStorage + abstract val sharePicture: SharePicture val pictures: SnapshotStateList = mutableStateListOf(*resourcePictures) open val externalEvents: Flow = emptyFlow() val localization: Localization = getCurrentLocalization() @@ -37,6 +39,40 @@ abstract class Dependencies { imageStorage.getThumbnail(picture) } } + + override fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) { + imageStorage.saveImage(picture, image) + } + + override fun delete(picture: PictureData) { + pictures.remove(picture) + if (picture is PictureData.Camera) { + imageStorage.delete(picture) + } + } + + override fun edit(picture: PictureData, name: String, description: String): PictureData { + when (picture) { + is PictureData.Resource -> { + val edited = picture.copy( + name = name, + description = description, + ) + pictures[pictures.indexOf(picture)] = edited + return edited + } + + is PictureData.Camera -> { + val edited = picture.copy( + name = name, + description = description, + ) + pictures[pictures.indexOf(picture)] = edited + imageStorage.rewrite(edited) + return edited + } + } + } } } @@ -66,12 +102,21 @@ interface Localization { interface ImageProvider { suspend fun getImage(picture: PictureData): ImageBitmap suspend fun getThumbnail(picture: PictureData): ImageBitmap + fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) + fun delete(picture: PictureData) + fun edit(picture: PictureData, name: String, description: String): PictureData } interface ImageStorage { - fun saveImage(pictureData: PictureData.Camera, image: PlatformStorableImage) - suspend fun getThumbnail(pictureData: PictureData.Camera): ImageBitmap - suspend fun getImage(pictureData: PictureData.Camera): ImageBitmap + fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) + fun delete(picture: PictureData.Camera) + fun rewrite(picture: PictureData.Camera) + suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap + suspend fun getImage(picture: PictureData.Camera): ImageBitmap +} + +interface SharePicture { + fun share(context: PlatformContext, picture: PictureData) } internal val LocalLocalization = staticCompositionLocalOf { @@ -86,14 +131,14 @@ internal val LocalImageProvider = staticCompositionLocalOf { noLocalProvidedFor("LocalImageProvider") } -internal val LocalImageStorage = staticCompositionLocalOf { - noLocalProvidedFor("LocalImageStorage") -} - internal val LocalInternalEvents = staticCompositionLocalOf> { noLocalProvidedFor("LocalInternalEvents") } +internal val LocalSharePicture = staticCompositionLocalOf { + noLocalProvidedFor("LocalSharePicture") +} + private fun noLocalProvidedFor(name: String): Nothing { error("CompositionLocal $name not present") } diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt index d4aad33394..f9a7491b6e 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt @@ -21,8 +21,8 @@ internal fun ImageViewerCommon( LocalLocalization provides dependencies.localization, LocalNotification provides dependencies.notification, LocalImageProvider provides dependencies.imageProvider, - LocalImageStorage provides dependencies.imageStorage, - LocalInternalEvents provides dependencies.externalEvents + LocalInternalEvents provides dependencies.externalEvents, + LocalSharePicture provides dependencies.sharePicture, ) { ImageViewerWithProvidedDependencies(dependencies.pictures) } @@ -63,7 +63,7 @@ internal fun ImageViewerWithProvidedDependencies( pictures = pictures, selectedPictureIndex = selectedPictureIndex, onClickPreviewPicture = { previewPictureId -> - navigationStack.push(MemoryPage(previewPictureId)) + navigationStack.push(MemoryPage(mutableStateOf(previewPictureId))) } ) { navigationStack.push(CameraPage()) @@ -83,8 +83,8 @@ internal fun ImageViewerWithProvidedDependencies( MemoryScreen( pictures = pictures, memoryPage = page, - onSelectRelatedMemory = { galleryId -> - navigationStack.push(MemoryPage(galleryId)) + onSelectRelatedMemory = { picture -> + navigationStack.push(MemoryPage(mutableStateOf(picture))) }, onBack = { navigationStack.back() diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt index 100bbf6e04..6b57aa0362 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt @@ -1,8 +1,10 @@ package example.imageviewer.model +import androidx.compose.runtime.MutableState + sealed interface Page -class MemoryPage(val picture: PictureData) : Page +class MemoryPage(val pictureState: MutableState) : Page class CameraPage : Page class FullScreenPage(val picture: PictureData) : Page class GalleryPage : Page diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt index 63c91a268d..d98d8354e7 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt @@ -19,7 +19,7 @@ sealed interface PictureData { val gps: GpsPosition val dateString: String - class Resource( + data class Resource( val resource: String, val thumbnailResource: String, override val name: String, @@ -29,15 +29,13 @@ sealed interface PictureData { ) : PictureData @Serializable - class Camera( + data class Camera( val id: String, val timeStampSeconds: Long, override val name: String, override val description: String, override val gps: GpsPosition, ) : PictureData { - override fun equals(other: Any?): Boolean = (other as? Camera)?.id == id - override fun hashCode(): Int = id.hashCode() override val dateString: String get(): String { val instantTime = Instant.fromEpochSeconds(timeStampSeconds, 0) diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt index 12e657fb48..2080192de1 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt @@ -1,6 +1,8 @@ package example.imageviewer import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import example.imageviewer.model.PictureData import kotlinx.coroutines.CoroutineDispatcher expect fun Modifier.notchPadding(): Modifier @@ -10,3 +12,7 @@ expect class PlatformStorableImage expect fun createUUID(): String expect val ioDispatcher: CoroutineDispatcher + +expect val isShareFeatureSupported: Boolean + +expect val shareIcon: ImageVector diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt index 7495c810ce..fcdc33ada2 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt @@ -6,12 +6,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import example.imageviewer.LocalImageStorage +import example.imageviewer.LocalImageProvider import kotlinx.coroutines.delay @Composable internal fun CameraScreen(onBack: (resetSelectedPicture: Boolean) -> Unit) { - val storage = LocalImageStorage.current + val imageProvider = LocalImageProvider.current var showCamera by remember { mutableStateOf(false) } LaunchedEffect(onBack) { if (!showCamera) { @@ -22,7 +22,7 @@ internal fun CameraScreen(onBack: (resetSelectedPicture: Boolean) -> Unit) { Box(Modifier.fillMaxSize().background(Color.Black)) { if (showCamera) { CameraView(Modifier.fillMaxSize(), onCapture = { picture, image -> - storage.saveImage(picture, image) + imageProvider.saveImage(picture, image) onBack(true) }) } diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/EditMemoryDialog.common.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/EditMemoryDialog.common.kt new file mode 100644 index 0000000000..e96bca9222 --- /dev/null +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/EditMemoryDialog.common.kt @@ -0,0 +1,11 @@ +package example.imageviewer.view + +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable + +@Composable +internal expect fun BoxScope.EditMemoryDialog( + previousName: String, + previousDescription: String, + save: (name: String, description: String) -> Unit +) diff --git a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt index 7283e8b486..5ccd2679ff 100644 --- a/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt +++ b/experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt @@ -10,6 +10,9 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment @@ -20,18 +23,22 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale 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.LocalImageProvider +import example.imageviewer.LocalSharePicture +import example.imageviewer.filter.getPlatformContext +import example.imageviewer.isShareFeatureSupported import example.imageviewer.model.* +import example.imageviewer.shareIcon import example.imageviewer.style.ImageviewerColors import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource -@OptIn(ExperimentalResourceApi::class) @Composable internal fun MemoryScreen( pictures: SnapshotStateList, @@ -41,9 +48,13 @@ internal fun MemoryScreen( onHeaderClick: (PictureData) -> Unit, ) { val imageProvider = LocalImageProvider.current - var headerImage: ImageBitmap? by remember(memoryPage.picture) { mutableStateOf(null) } - LaunchedEffect(memoryPage.picture) { - headerImage = imageProvider.getImage(memoryPage.picture) + val sharePicture = LocalSharePicture.current + var edit: Boolean by remember { mutableStateOf(false) } + val picture = memoryPage.pictureState.value + var headerImage: ImageBitmap? by remember(picture) { mutableStateOf(null) } + val platformContext = getPlatformContext() + LaunchedEffect(picture) { + headerImage = imageProvider.getImage(picture) } Box { val scrollState = rememberScrollState() @@ -65,17 +76,20 @@ internal fun MemoryScreen( headerImage?.let { MemoryHeader( it, - picture = memoryPage.picture, - onClick = { onHeaderClick(memoryPage.picture) } + picture = picture, + onClick = { onHeaderClick(picture) } ) } } Box(modifier = Modifier.background(MaterialTheme.colors.background)) { - Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Headliner("Note") - Collapsible(memoryPage.picture.description) + Collapsible(picture.description) Headliner("Related memories") - RelatedMemoriesVisualizer(pictures, onSelectRelatedMemory) + RelatedMemoriesVisualizer( + pictures = remember { (pictures - picture).shuffled().take(8) }, + onSelectRelatedMemory = onSelectRelatedMemory + ) Headliner("Place") val locationShape = RoundedCornerShape(10.dp) LocationVisualizer( @@ -84,30 +98,23 @@ internal fun MemoryScreen( .border(1.dp, Color.Gray, locationShape) .fillMaxWidth() .height(200.dp), - gps = memoryPage.picture.gps, - title = memoryPage.picture.name, + gps = picture.gps, + title = picture.name, ) Spacer(Modifier.height(50.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - 8.dp, - Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painterResource("trash.png"), - contentDescription = null, - modifier = Modifier.size(14.dp) - ) - Text( - text = "Delete Memory", - textAlign = TextAlign.Left, - color = ImageviewerColors.onBackground, - fontSize = 14.sp, - fontWeight = FontWeight.Normal - ) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { + IconWithText(Icons.Default.Delete, "Delete") { + imageProvider.delete(picture) + onBack() + } + IconWithText(Icons.Default.Edit, "Edit") { + edit = true + } + if (isShareFeatureSupported) { + IconWithText(shareIcon, "Share") { + sharePicture.share(platformContext, picture) + } + } } Spacer(Modifier.height(50.dp)) } @@ -119,6 +126,39 @@ internal fun MemoryScreen( }, alignRightContent = {}, ) + if (edit) { + EditMemoryDialog(picture.name, picture.description) { name, description -> + val edited = imageProvider.edit(picture, name, description) + memoryPage.pictureState.value = edited + edit = false + } + } + } +} + +@Composable +private fun IconWithText(icon: ImageVector, text: String, onClick: () -> Unit) { + Row( + modifier = Modifier.clickable { + onClick() + }, + horizontalArrangement = Arrangement.spacedBy( + 8.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.Bottom + ) { + Icon( + imageVector = icon, + contentDescription = text, + ) + Text( + text = text, + textAlign = TextAlign.Left, + color = ImageviewerColors.onBackground, + fontSize = 14.sp, + fontWeight = FontWeight.Normal + ) } } @@ -222,7 +262,7 @@ internal fun Headliner(s: String) { @Composable internal fun RelatedMemoriesVisualizer( - ps: List, + pictures: List, onSelectRelatedMemory: (PictureData) -> Unit ) { Box( @@ -232,7 +272,7 @@ internal fun RelatedMemoriesVisualizer( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - itemsIndexed(ps) { idx, item -> + itemsIndexed(pictures) { idx, item -> RelatedMemory(item, onSelectRelatedMemory) } } diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/DesktopImageStorage.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/DesktopImageStorage.kt index a893c619ef..0161ac1474 100644 --- a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/DesktopImageStorage.kt +++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/DesktopImageStorage.kt @@ -20,23 +20,31 @@ class DesktopImageStorage( private val largeImages = mutableMapOf() private val thumbnails = mutableMapOf() - override fun saveImage(pictureData: PictureData.Camera, image: PlatformStorableImage) { + override fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) { if (image.imageBitmap.width == 0 || image.imageBitmap.height == 0) { return } ioScope.launch { - largeImages[pictureData] = image.imageBitmap.fitInto(maxStorableImageSizePx) - thumbnails[pictureData] = image.imageBitmap.fitInto(storableThumbnailSizePx) - pictures.add(0, pictureData) + largeImages[picture] = image.imageBitmap.fitInto(maxStorableImageSizePx) + thumbnails[picture] = image.imageBitmap.fitInto(storableThumbnailSizePx) + pictures.add(0, picture) } } - override suspend fun getThumbnail(pictureData: PictureData.Camera): ImageBitmap { - return thumbnails[pictureData]!! + override fun delete(picture: PictureData.Camera) { + // For now, on Desktop pictures saving in memory. We don't need additional delete logic. } - override suspend fun getImage(pictureData: PictureData.Camera): ImageBitmap { - return largeImages[pictureData]!! + override fun rewrite(picture: PictureData.Camera) { + // For now, on Desktop pictures saving in memory. We don't need additional rewrite logic. + } + + override suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap { + return thumbnails[picture]!! + } + + override suspend fun getImage(picture: PictureData.Camera): ImageBitmap { + return largeImages[picture]!! } } diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/platform.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/platform.desktop.kt index 0eb2dc4123..7db77389f9 100644 --- a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/platform.desktop.kt +++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/platform.desktop.kt @@ -1,9 +1,13 @@ package example.imageviewer import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp +import example.imageviewer.model.PictureData import kotlinx.coroutines.Dispatchers import java.util.* @@ -18,3 +22,7 @@ actual typealias PlatformStorableImage = DesktopStorableImage actual fun createUUID(): String = UUID.randomUUID().toString() actual val ioDispatcher = Dispatchers.IO + +actual val isShareFeatureSupported: Boolean = false + +actual val shareIcon: ImageVector = Icons.Filled.Share diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/EditMemoryDialog.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/EditMemoryDialog.desktop.kt new file mode 100644 index 0000000000..8190d2f8ce --- /dev/null +++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/EditMemoryDialog.desktop.kt @@ -0,0 +1,87 @@ +package example.imageviewer.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.AlertDialog +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +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.draw.clip +import androidx.compose.ui.graphics.Color +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 + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal actual fun BoxScope.EditMemoryDialog( + previousName: String, + previousDescription: String, + save: (name: String, description: String) -> Unit +) { + var name by remember { mutableStateOf(previousName) } + var description by remember { mutableStateOf(previousDescription) } + AlertDialog( + onDismissRequest = { + save(name, description) + }, + buttons = { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(30.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.White, + ), + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + TextField( + value = description, + onValueChange = { description = it }, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.White, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ) + ) + } + }, + ) + Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + .clickable { + + } + ) { + } +} diff --git a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt index d1f478bb9e..7d9abafa43 100755 --- a/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt +++ b/experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import example.imageviewer.* import example.imageviewer.Notification +import example.imageviewer.filter.PlatformContext import example.imageviewer.model.* import example.imageviewer.style.ImageViewerTheme import kotlinx.coroutines.CoroutineScope @@ -23,6 +24,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import java.awt.Desktop import java.awt.Dimension import java.awt.Toolkit @@ -100,7 +102,12 @@ private fun getDependencies( toastState.value = ToastState.Shown(text) } } - override val imageStorage: ImageStorage = DesktopImageStorage(pictures, ioScope) + override val imageStorage: DesktopImageStorage = DesktopImageStorage(pictures, ioScope) + override val sharePicture: SharePicture = object : SharePicture { + override fun share(context: PlatformContext, picture: PictureData) { + // On Desktop share feature not supported + } + } override val externalEvents = events } diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt index 6b17e44fe6..d693b51a7d 100755 --- a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt +++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt @@ -4,17 +4,28 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Surface import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import example.imageviewer.filter.PlatformContext +import example.imageviewer.model.PictureData import example.imageviewer.storage.IosImageStorage import example.imageviewer.style.ImageViewerTheme import example.imageviewer.view.Toast import example.imageviewer.view.ToastState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import platform.UIKit.UIActivityViewController +import platform.UIKit.UIApplication +import platform.UIKit.UIImage +import platform.UIKit.UIWindow @Composable internal fun ImageViewerIos() { val toastState = remember { mutableStateOf(ToastState.Hidden) } val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher } - val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) } + val dependencies = remember(ioScope) { + getDependencies(ioScope, toastState) + } ImageViewerTheme { Surface( @@ -35,5 +46,27 @@ fun getDependencies(ioScope: CoroutineScope, toastState: MutableState Boolean) = ?.map { File(this, it) } ?.toTypedArray() +fun NSURL.delete() { + NSFileManager.defaultManager.removeItemAtURL(this, null) +} + suspend fun NSURL.readData(): NSData { while (true) { val data = NSData.dataWithContentsOfURL(this) diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/IosImageStorage.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/IosImageStorage.ios.kt index ea916a34ca..cf3901ff88 100644 --- a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/IosImageStorage.ios.kt +++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/IosImageStorage.ios.kt @@ -56,26 +56,56 @@ class IosImageStorage( } } - override fun saveImage(pictureData: PictureData.Camera, image: PlatformStorableImage) { + override fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage) { ioScope.launch { with(image.rawValue) { - pictureData.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx)) - pictureData.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx)) + picture.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx)) + picture.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx)) } - pictures.add(0, pictureData) - pictureData.jsonFile.writeText(pictureData.toJson()) + pictures.add(0, picture) + picture.jsonFile.writeText(picture.toJson()) } } - override suspend fun getThumbnail(pictureData: PictureData.Camera): ImageBitmap = + override fun delete(picture: PictureData.Camera) { + ioScope.launch { + picture.jsonFile.delete() + picture.jpgFile.delete() + picture.thumbnailJpgFile.delete() + } + } + + override fun rewrite(picture: PictureData.Camera) { + ioScope.launch { + picture.jsonFile.delete() + picture.jsonFile.writeText(picture.toJson()) + } + } + + override suspend fun getThumbnail(picture: PictureData.Camera): ImageBitmap = withContext(ioScope.coroutineContext) { - pictureData.thumbnailJpgFile.readBytes().toImageBitmap() + picture.thumbnailJpgFile.readBytes().toImageBitmap() } - override suspend fun getImage(pictureData: PictureData.Camera): ImageBitmap = + override suspend fun getImage(picture: PictureData.Camera): ImageBitmap = withContext(ioScope.coroutineContext) { - pictureData.jpgFile.readBytes().toImageBitmap() + picture.jpgFile.readBytes().toImageBitmap() } + + suspend fun getNSDataToShare(picture: PictureData): NSData = withContext(Dispatchers.IO) { + when (picture) { + is PictureData.Camera -> { + picture.jpgFile + } + + is PictureData.Resource -> { + NSURL( + fileURLWithPath = NSBundle.mainBundle.resourcePath + "/" + picture.resource, + isDirectory = false + ) + } + }.readData() + } } private fun UIImage.fitInto(px: Int): UIImage { diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/EditMemoryDialog.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/EditMemoryDialog.ios.kt new file mode 100644 index 0000000000..8d748110ba --- /dev/null +++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/EditMemoryDialog.ios.kt @@ -0,0 +1,77 @@ +package example.imageviewer.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +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.draw.clip +import androidx.compose.ui.graphics.Color +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 + +@Composable +internal actual fun BoxScope.EditMemoryDialog( + previousName: String, + previousDescription: String, + save: (name: String, description: String) -> Unit +) { + var name by remember { mutableStateOf(previousName) } + var description by remember { mutableStateOf(previousDescription) } + Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + .clickable { + save(name, description) + } + ) { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(30.dp) + .clip(RoundedCornerShape(20.dp)) + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.White, + ), + textStyle = LocalTextStyle.current.copy( + textAlign = TextAlign.Center, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + ), + ) + TextField( + value = description, + onValueChange = { description = it }, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.White, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ) + ) + } + } +} diff --git a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt index aaa46d4545..5cc8453ad2 100644 --- a/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt +++ b/experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt @@ -1,6 +1,7 @@ package example.imageviewer.view import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.interop.UIKitInteropView import example.imageviewer.model.GpsPosition @@ -11,26 +12,29 @@ import platform.MapKit.MKPointAnnotation @Composable internal actual fun LocationVisualizer(modifier: Modifier, gps: GpsPosition, title: String) { + val location = CLLocationCoordinate2DMake(gps.latitude, gps.longitude) + val annotation = remember { + MKPointAnnotation( + location, + title = null, + subtitle = null + ) + } + val mkMapView = remember { MKMapView().apply { addAnnotation(annotation) } } + annotation.setTitle(title) UIKitInteropView( modifier = modifier, factory = { - val mkMapView = MKMapView() - val cityAmsterdam = CLLocationCoordinate2DMake(gps.latitude, gps.longitude) + mkMapView + }, + update = { mkMapView.setRegion( MKCoordinateRegionMakeWithDistance( - centerCoordinate = cityAmsterdam, + centerCoordinate = location, 10_000.0, 10_000.0 ), animated = false ) - mkMapView.addAnnotation( - MKPointAnnotation( - cityAmsterdam, - title = title, - subtitle = null - ) - ) - mkMapView - }, + } ) }