Browse Source

ImageViewer delete, edit and share memories (#2957)

pull/2968/head
dima.avdeev 2 years ago committed by GitHub
parent
commit
2cea802cec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      experimental/examples/imageviewer/shared/build.gradle.kts
  2. 11
      experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml
  3. 6
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/ImageViewerFileProvider.kt
  4. 8
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/platform.android.kt
  5. 69
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/storage/AndroidImageStorage.kt
  6. 77
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/EditMemoryDialog.android.kt
  7. 27
      experimental/examples/imageviewer/shared/src/androidMain/kotlin/example/imageviewer/view/ImageViewer.android.kt
  8. 3
      experimental/examples/imageviewer/shared/src/androidMain/res/xml/file_paths.xml
  9. 59
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/Dependencies.kt
  10. 10
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/ImageViewer.common.kt
  11. 4
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt
  12. 6
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt
  13. 6
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt
  14. 6
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/CameraScreen.kt
  15. 11
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/EditMemoryDialog.common.kt
  16. 106
      experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/view/MemoryScreen.kt
  17. 24
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/DesktopImageStorage.kt
  18. 8
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/platform.desktop.kt
  19. 87
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/EditMemoryDialog.desktop.kt
  20. 9
      experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/view/ImageViewer.desktop.kt
  21. 37
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/ImageViewer.ios.kt
  22. 41
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/IosShareIcon.kt
  23. 5
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/main.ios.kt
  24. 5
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/platform.ios.kt
  25. 4
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/FileExtensions.kt
  26. 48
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/IosImageStorage.ios.kt
  27. 77
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/EditMemoryDialog.ios.kt
  28. 28
      experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt

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

@ -35,6 +35,7 @@ kotlin {
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
implementation(compose.material) implementation(compose.material)
//implementation(compose.materialIconsExtended) // TODO not working on iOS
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources) implementation(compose.components.resources)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")

11
experimental/examples/imageviewer/shared/src/androidMain/AndroidManifest.xml

@ -1,7 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="example.imageviewer.shared"> package="example.imageviewer.shared">
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application>
<provider
android:name="example.imageviewer.ImageViewerFileProvider"
android:authorities="example.imageviewer.fileprovider"
android:exported="false"
android:grantUriPermissions="true"></provider>
</application>
</manifest> </manifest>

6
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)

8
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.foundation.layout.statusBarsPadding
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.model.PictureData
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.* 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() actual fun Modifier.notchPadding(): Modifier = displayCutoutPadding().statusBarsPadding()
@ -18,3 +22,7 @@ actual typealias PlatformStorableImage = AndroidStorableImage
actual fun createUUID(): String = UUID.randomUUID().toString() actual fun createUUID(): String = UUID.randomUUID().toString()
actual val ioDispatcher = Dispatchers.IO actual val ioDispatcher = Dispatchers.IO
actual val isShareFeatureSupported: Boolean = true
actual val shareIcon: ImageVector = Icons.Filled.Share

69
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.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.content.FileProvider
import androidx.core.graphics.scale import androidx.core.graphics.scale
import example.imageviewer.ImageStorage import example.imageviewer.ImageStorage
import example.imageviewer.PlatformStorableImage import example.imageviewer.PlatformStorableImage
import example.imageviewer.model.PictureData import example.imageviewer.model.PictureData
import example.imageviewer.toAndroidBitmap
import example.imageviewer.toImageBitmap import example.imageviewer.toImageBitmap
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resource
import java.io.File import java.io.File
import java.util.UUID
private const val maxStorableImageSizePx = 2000 private const val maxStorableImageSizePx = 2000
private const val storableThumbnailSizePx = 200 private const val storableThumbnailSizePx = 200
@ -29,10 +35,15 @@ class AndroidImageStorage(
private val ioScope: CoroutineScope, private val ioScope: CoroutineScope,
context: Context context: Context
) : ImageStorage { ) : 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.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") private val PictureData.Camera.jsonFile get() = File(savePictureDir, "$id.json")
init { 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) { if (image.imageBitmap.width == 0 || image.imageBitmap.height == 0) {
return return
} }
ioScope.launch { ioScope.launch {
with(image.imageBitmap) { with(image.imageBitmap) {
pictureData.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx)) picture.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx))
pictureData.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx)) picture.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx))
} }
pictures.add(0, pictureData) pictures.add(0, picture)
pictureData.jsonFile.writeText(pictureData.toJson()) 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) { 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) { 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 { private fun ImageBitmap.fitInto(px: Int): ImageBitmap {

77
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,
)
)
}
}
}

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

@ -1,15 +1,22 @@
package example.imageviewer.view package example.imageviewer.view
import android.content.Context import android.content.Context
import android.content.Intent
import android.widget.Toast import android.widget.Toast
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import example.imageviewer.* import example.imageviewer.*
import example.imageviewer.filter.PlatformContext
import example.imageviewer.model.PictureData
import example.imageviewer.storage.AndroidImageStorage import example.imageviewer.storage.AndroidImageStorage
import example.imageviewer.style.ImageViewerTheme import example.imageviewer.style.ImageViewerTheme
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun ImageViewerAndroid() { fun ImageViewerAndroid() {
@ -27,5 +34,23 @@ private fun getDependencies(context: Context, ioScope: CoroutineScope) = object
Toast.makeText(context, text, Toast.LENGTH_SHORT).show() 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))
}
}
}
}
} }

3
experimental/examples/imageviewer/shared/src/androidMain/res/xml/file_paths.xml

@ -0,0 +1,3 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="share_images/"/>
</paths>

59
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.snapshots.SnapshotStateList
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import example.imageviewer.filter.PlatformContext
import example.imageviewer.model.* import example.imageviewer.model.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
@ -14,6 +15,7 @@ import org.jetbrains.compose.resources.resource
abstract class Dependencies { abstract class Dependencies {
abstract val notification: Notification abstract val notification: Notification
abstract val imageStorage: ImageStorage abstract val imageStorage: ImageStorage
abstract val sharePicture: SharePicture
val pictures: SnapshotStateList<PictureData> = mutableStateListOf(*resourcePictures) val pictures: SnapshotStateList<PictureData> = mutableStateListOf(*resourcePictures)
open val externalEvents: Flow<ExternalImageViewerEvent> = emptyFlow() open val externalEvents: Flow<ExternalImageViewerEvent> = emptyFlow()
val localization: Localization = getCurrentLocalization() val localization: Localization = getCurrentLocalization()
@ -37,6 +39,40 @@ abstract class Dependencies {
imageStorage.getThumbnail(picture) 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 { interface ImageProvider {
suspend fun getImage(picture: PictureData): ImageBitmap suspend fun getImage(picture: PictureData): ImageBitmap
suspend fun getThumbnail(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 { interface ImageStorage {
fun saveImage(pictureData: PictureData.Camera, image: PlatformStorableImage) fun saveImage(picture: PictureData.Camera, image: PlatformStorableImage)
suspend fun getThumbnail(pictureData: PictureData.Camera): ImageBitmap fun delete(picture: PictureData.Camera)
suspend fun getImage(pictureData: PictureData.Camera): ImageBitmap 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<Localization> { internal val LocalLocalization = staticCompositionLocalOf<Localization> {
@ -86,14 +131,14 @@ internal val LocalImageProvider = staticCompositionLocalOf<ImageProvider> {
noLocalProvidedFor("LocalImageProvider") noLocalProvidedFor("LocalImageProvider")
} }
internal val LocalImageStorage = staticCompositionLocalOf<ImageStorage> {
noLocalProvidedFor("LocalImageStorage")
}
internal val LocalInternalEvents = staticCompositionLocalOf<Flow<ExternalImageViewerEvent>> { internal val LocalInternalEvents = staticCompositionLocalOf<Flow<ExternalImageViewerEvent>> {
noLocalProvidedFor("LocalInternalEvents") noLocalProvidedFor("LocalInternalEvents")
} }
internal val LocalSharePicture = staticCompositionLocalOf<SharePicture> {
noLocalProvidedFor("LocalSharePicture")
}
private fun noLocalProvidedFor(name: String): Nothing { private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present") error("CompositionLocal $name not present")
} }

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

@ -21,8 +21,8 @@ internal fun ImageViewerCommon(
LocalLocalization provides dependencies.localization, LocalLocalization provides dependencies.localization,
LocalNotification provides dependencies.notification, LocalNotification provides dependencies.notification,
LocalImageProvider provides dependencies.imageProvider, LocalImageProvider provides dependencies.imageProvider,
LocalImageStorage provides dependencies.imageStorage, LocalInternalEvents provides dependencies.externalEvents,
LocalInternalEvents provides dependencies.externalEvents LocalSharePicture provides dependencies.sharePicture,
) { ) {
ImageViewerWithProvidedDependencies(dependencies.pictures) ImageViewerWithProvidedDependencies(dependencies.pictures)
} }
@ -63,7 +63,7 @@ internal fun ImageViewerWithProvidedDependencies(
pictures = pictures, pictures = pictures,
selectedPictureIndex = selectedPictureIndex, selectedPictureIndex = selectedPictureIndex,
onClickPreviewPicture = { previewPictureId -> onClickPreviewPicture = { previewPictureId ->
navigationStack.push(MemoryPage(previewPictureId)) navigationStack.push(MemoryPage(mutableStateOf(previewPictureId)))
} }
) { ) {
navigationStack.push(CameraPage()) navigationStack.push(CameraPage())
@ -83,8 +83,8 @@ internal fun ImageViewerWithProvidedDependencies(
MemoryScreen( MemoryScreen(
pictures = pictures, pictures = pictures,
memoryPage = page, memoryPage = page,
onSelectRelatedMemory = { galleryId -> onSelectRelatedMemory = { picture ->
navigationStack.push(MemoryPage(galleryId)) navigationStack.push(MemoryPage(mutableStateOf(picture)))
}, },
onBack = { onBack = {
navigationStack.back() navigationStack.back()

4
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/Page.kt

@ -1,8 +1,10 @@
package example.imageviewer.model package example.imageviewer.model
import androidx.compose.runtime.MutableState
sealed interface Page sealed interface Page
class MemoryPage(val picture: PictureData) : Page class MemoryPage(val pictureState: MutableState<PictureData>) : Page
class CameraPage : Page class CameraPage : Page
class FullScreenPage(val picture: PictureData) : Page class FullScreenPage(val picture: PictureData) : Page
class GalleryPage : Page class GalleryPage : Page

6
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/model/PictureData.kt

@ -19,7 +19,7 @@ sealed interface PictureData {
val gps: GpsPosition val gps: GpsPosition
val dateString: String val dateString: String
class Resource( data class Resource(
val resource: String, val resource: String,
val thumbnailResource: String, val thumbnailResource: String,
override val name: String, override val name: String,
@ -29,15 +29,13 @@ sealed interface PictureData {
) : PictureData ) : PictureData
@Serializable @Serializable
class Camera( data class Camera(
val id: String, val id: String,
val timeStampSeconds: Long, val timeStampSeconds: Long,
override val name: String, override val name: String,
override val description: String, override val description: String,
override val gps: GpsPosition, override val gps: GpsPosition,
) : PictureData { ) : PictureData {
override fun equals(other: Any?): Boolean = (other as? Camera)?.id == id
override fun hashCode(): Int = id.hashCode()
override val dateString: String override val dateString: String
get(): String { get(): String {
val instantTime = Instant.fromEpochSeconds(timeStampSeconds, 0) val instantTime = Instant.fromEpochSeconds(timeStampSeconds, 0)

6
experimental/examples/imageviewer/shared/src/commonMain/kotlin/example/imageviewer/platform.common.kt

@ -1,6 +1,8 @@
package example.imageviewer package example.imageviewer
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import example.imageviewer.model.PictureData
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
expect fun Modifier.notchPadding(): Modifier expect fun Modifier.notchPadding(): Modifier
@ -10,3 +12,7 @@ expect class PlatformStorableImage
expect fun createUUID(): String expect fun createUUID(): String
expect val ioDispatcher: CoroutineDispatcher expect val ioDispatcher: CoroutineDispatcher
expect val isShareFeatureSupported: Boolean
expect val shareIcon: ImageVector

6
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.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import example.imageviewer.LocalImageStorage import example.imageviewer.LocalImageProvider
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@Composable @Composable
internal fun CameraScreen(onBack: (resetSelectedPicture: Boolean) -> Unit) { internal fun CameraScreen(onBack: (resetSelectedPicture: Boolean) -> Unit) {
val storage = LocalImageStorage.current val imageProvider = LocalImageProvider.current
var showCamera by remember { mutableStateOf(false) } var showCamera by remember { mutableStateOf(false) }
LaunchedEffect(onBack) { LaunchedEffect(onBack) {
if (!showCamera) { if (!showCamera) {
@ -22,7 +22,7 @@ internal fun CameraScreen(onBack: (resetSelectedPicture: Boolean) -> Unit) {
Box(Modifier.fillMaxSize().background(Color.Black)) { Box(Modifier.fillMaxSize().background(Color.Black)) {
if (showCamera) { if (showCamera) {
CameraView(Modifier.fillMaxSize(), onCapture = { picture, image -> CameraView(Modifier.fillMaxSize(), onCapture = { picture, image ->
storage.saveImage(picture, image) imageProvider.saveImage(picture, image)
onBack(true) onBack(true)
}) })
} }

11
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
)

106
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.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* 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.*
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment 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.ImageBitmap
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import example.imageviewer.LocalImageProvider import example.imageviewer.LocalImageProvider
import example.imageviewer.LocalSharePicture
import example.imageviewer.filter.getPlatformContext
import example.imageviewer.isShareFeatureSupported
import example.imageviewer.model.* import example.imageviewer.model.*
import example.imageviewer.shareIcon
import example.imageviewer.style.ImageviewerColors import example.imageviewer.style.ImageviewerColors
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
internal fun MemoryScreen( internal fun MemoryScreen(
pictures: SnapshotStateList<PictureData>, pictures: SnapshotStateList<PictureData>,
@ -41,9 +48,13 @@ internal fun MemoryScreen(
onHeaderClick: (PictureData) -> Unit, onHeaderClick: (PictureData) -> Unit,
) { ) {
val imageProvider = LocalImageProvider.current val imageProvider = LocalImageProvider.current
var headerImage: ImageBitmap? by remember(memoryPage.picture) { mutableStateOf(null) } val sharePicture = LocalSharePicture.current
LaunchedEffect(memoryPage.picture) { var edit: Boolean by remember { mutableStateOf(false) }
headerImage = imageProvider.getImage(memoryPage.picture) val picture = memoryPage.pictureState.value
var headerImage: ImageBitmap? by remember(picture) { mutableStateOf(null) }
val platformContext = getPlatformContext()
LaunchedEffect(picture) {
headerImage = imageProvider.getImage(picture)
} }
Box { Box {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -65,17 +76,20 @@ internal fun MemoryScreen(
headerImage?.let { headerImage?.let {
MemoryHeader( MemoryHeader(
it, it,
picture = memoryPage.picture, picture = picture,
onClick = { onHeaderClick(memoryPage.picture) } onClick = { onHeaderClick(picture) }
) )
} }
} }
Box(modifier = Modifier.background(MaterialTheme.colors.background)) { Box(modifier = Modifier.background(MaterialTheme.colors.background)) {
Column { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Headliner("Note") Headliner("Note")
Collapsible(memoryPage.picture.description) Collapsible(picture.description)
Headliner("Related memories") Headliner("Related memories")
RelatedMemoriesVisualizer(pictures, onSelectRelatedMemory) RelatedMemoriesVisualizer(
pictures = remember { (pictures - picture).shuffled().take(8) },
onSelectRelatedMemory = onSelectRelatedMemory
)
Headliner("Place") Headliner("Place")
val locationShape = RoundedCornerShape(10.dp) val locationShape = RoundedCornerShape(10.dp)
LocationVisualizer( LocationVisualizer(
@ -84,30 +98,23 @@ internal fun MemoryScreen(
.border(1.dp, Color.Gray, locationShape) .border(1.dp, Color.Gray, locationShape)
.fillMaxWidth() .fillMaxWidth()
.height(200.dp), .height(200.dp),
gps = memoryPage.picture.gps, gps = picture.gps,
title = memoryPage.picture.name, title = picture.name,
) )
Spacer(Modifier.height(50.dp)) Spacer(Modifier.height(50.dp))
Row( Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
modifier = Modifier.fillMaxWidth(), IconWithText(Icons.Default.Delete, "Delete") {
horizontalArrangement = Arrangement.spacedBy( imageProvider.delete(picture)
8.dp, onBack()
Alignment.CenterHorizontally }
), IconWithText(Icons.Default.Edit, "Edit") {
verticalAlignment = Alignment.CenterVertically edit = true
) { }
Image( if (isShareFeatureSupported) {
painterResource("trash.png"), IconWithText(shareIcon, "Share") {
contentDescription = null, sharePicture.share(platformContext, picture)
modifier = Modifier.size(14.dp) }
) }
Text(
text = "Delete Memory",
textAlign = TextAlign.Left,
color = ImageviewerColors.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.Normal
)
} }
Spacer(Modifier.height(50.dp)) Spacer(Modifier.height(50.dp))
} }
@ -119,6 +126,39 @@ internal fun MemoryScreen(
}, },
alignRightContent = {}, 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 @Composable
internal fun RelatedMemoriesVisualizer( internal fun RelatedMemoriesVisualizer(
ps: List<PictureData>, pictures: List<PictureData>,
onSelectRelatedMemory: (PictureData) -> Unit onSelectRelatedMemory: (PictureData) -> Unit
) { ) {
Box( Box(
@ -232,7 +272,7 @@ internal fun RelatedMemoriesVisualizer(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
itemsIndexed(ps) { idx, item -> itemsIndexed(pictures) { idx, item ->
RelatedMemory(item, onSelectRelatedMemory) RelatedMemory(item, onSelectRelatedMemory)
} }
} }

24
experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/DesktopImageStorage.kt

@ -20,23 +20,31 @@ class DesktopImageStorage(
private val largeImages = mutableMapOf<PictureData.Camera, ImageBitmap>() private val largeImages = mutableMapOf<PictureData.Camera, ImageBitmap>()
private val thumbnails = mutableMapOf<PictureData.Camera, ImageBitmap>() private val thumbnails = mutableMapOf<PictureData.Camera, ImageBitmap>()
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) { if (image.imageBitmap.width == 0 || image.imageBitmap.height == 0) {
return return
} }
ioScope.launch { ioScope.launch {
largeImages[pictureData] = image.imageBitmap.fitInto(maxStorableImageSizePx) largeImages[picture] = image.imageBitmap.fitInto(maxStorableImageSizePx)
thumbnails[pictureData] = image.imageBitmap.fitInto(storableThumbnailSizePx) thumbnails[picture] = image.imageBitmap.fitInto(storableThumbnailSizePx)
pictures.add(0, pictureData) pictures.add(0, picture)
} }
} }
override suspend fun getThumbnail(pictureData: PictureData.Camera): ImageBitmap { override fun delete(picture: PictureData.Camera) {
return thumbnails[pictureData]!! // For now, on Desktop pictures saving in memory. We don't need additional delete logic.
} }
override suspend fun getImage(pictureData: PictureData.Camera): ImageBitmap { override fun rewrite(picture: PictureData.Camera) {
return largeImages[pictureData]!! // 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]!!
} }
} }

8
experimental/examples/imageviewer/shared/src/desktopMain/kotlin/example/imageviewer/platform.desktop.kt

@ -1,9 +1,13 @@
package example.imageviewer package example.imageviewer
import androidx.compose.foundation.layout.padding 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.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import example.imageviewer.model.PictureData
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.* import java.util.*
@ -18,3 +22,7 @@ actual typealias PlatformStorableImage = DesktopStorableImage
actual fun createUUID(): String = UUID.randomUUID().toString() actual fun createUUID(): String = UUID.randomUUID().toString()
actual val ioDispatcher = Dispatchers.IO actual val ioDispatcher = Dispatchers.IO
actual val isShareFeatureSupported: Boolean = false
actual val shareIcon: ImageVector = Icons.Filled.Share

87
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 {
}
) {
}
}

9
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 androidx.compose.ui.window.*
import example.imageviewer.* import example.imageviewer.*
import example.imageviewer.Notification import example.imageviewer.Notification
import example.imageviewer.filter.PlatformContext
import example.imageviewer.model.* import example.imageviewer.model.*
import example.imageviewer.style.ImageViewerTheme import example.imageviewer.style.ImageViewerTheme
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -23,6 +24,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import java.awt.Desktop
import java.awt.Dimension import java.awt.Dimension
import java.awt.Toolkit import java.awt.Toolkit
@ -100,7 +102,12 @@ private fun getDependencies(
toastState.value = ToastState.Shown(text) 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 override val externalEvents = events
} }

37
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.material.Surface
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import example.imageviewer.filter.PlatformContext
import example.imageviewer.model.PictureData
import example.imageviewer.storage.IosImageStorage import example.imageviewer.storage.IosImageStorage
import example.imageviewer.style.ImageViewerTheme import example.imageviewer.style.ImageViewerTheme
import example.imageviewer.view.Toast import example.imageviewer.view.Toast
import example.imageviewer.view.ToastState import example.imageviewer.view.ToastState
import kotlinx.coroutines.CoroutineScope 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 @Composable
internal fun ImageViewerIos() { internal fun ImageViewerIos() {
val toastState = remember { mutableStateOf<ToastState>(ToastState.Hidden) } val toastState = remember { mutableStateOf<ToastState>(ToastState.Hidden) }
val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher } val ioScope: CoroutineScope = rememberCoroutineScope { ioDispatcher }
val dependencies = remember(ioScope) { getDependencies(ioScope, toastState) } val dependencies = remember(ioScope) {
getDependencies(ioScope, toastState)
}
ImageViewerTheme { ImageViewerTheme {
Surface( Surface(
@ -35,5 +46,27 @@ fun getDependencies(ioScope: CoroutineScope, toastState: MutableState<ToastState
toastState.value = ToastState.Shown(text) toastState.value = ToastState.Shown(text)
} }
} }
override val imageStorage: ImageStorage = IosImageStorage(pictures, ioScope)
override val imageStorage: IosImageStorage = IosImageStorage(pictures, ioScope)
override val sharePicture: SharePicture = object : SharePicture {
override fun share(context: PlatformContext, picture: PictureData) {
ioScope.launch {
val data = imageStorage.getNSDataToShare(picture)
withContext(Dispatchers.Main) {
val window = UIApplication.sharedApplication.windows.last() as? UIWindow
val currentViewController = window?.rootViewController
val activityViewController = UIActivityViewController(
activityItems = listOf(UIImage(data = data)),
applicationActivities = null
)
currentViewController?.presentViewController(
viewControllerToPresent = activityViewController,
animated = true,
completion = null,
)
}
}
}
}
} }

41
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/IosShareIcon.kt

@ -0,0 +1,41 @@
package example.imageviewer
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.ImageVector
// TODO Copied from material3, because "material:material-icons-extended" not working on iOS for now
val IosShareIcon: ImageVector =
materialIcon(name = "Filled.IosShare") {
materialPath {
moveTo(16.0f, 5.0f)
lineToRelative(-1.42f, 1.42f)
lineToRelative(-1.59f, -1.59f)
lineTo(12.99f, 16.0f)
horizontalLineToRelative(-1.98f)
lineTo(11.01f, 4.83f)
lineTo(9.42f, 6.42f)
lineTo(8.0f, 5.0f)
lineToRelative(4.0f, -4.0f)
lineToRelative(4.0f, 4.0f)
close()
moveTo(20.0f, 10.0f)
verticalLineToRelative(11.0f)
curveToRelative(0.0f, 1.1f, -0.9f, 2.0f, -2.0f, 2.0f)
lineTo(6.0f, 23.0f)
curveToRelative(-1.11f, 0.0f, -2.0f, -0.9f, -2.0f, -2.0f)
lineTo(4.0f, 10.0f)
curveToRelative(0.0f, -1.11f, 0.89f, -2.0f, 2.0f, -2.0f)
horizontalLineToRelative(3.0f)
verticalLineToRelative(2.0f)
lineTo(6.0f, 10.0f)
verticalLineToRelative(11.0f)
horizontalLineToRelative(12.0f)
lineTo(18.0f, 10.0f)
horizontalLineToRelative(-3.0f)
lineTo(15.0f, 8.0f)
horizontalLineToRelative(3.0f)
curveToRelative(1.1f, 0.0f, 2.0f, 0.89f, 2.0f, 2.0f)
close()
}
}

5
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/main.ios.kt

@ -3,5 +3,8 @@ package example.imageviewer
import androidx.compose.ui.window.ComposeUIViewController import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController import platform.UIKit.UIViewController
fun MainViewController(): UIViewController = ComposeUIViewController { ImageViewerIos() } fun MainViewController(): UIViewController =
ComposeUIViewController {
ImageViewerIos()
}

5
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/platform.ios.kt

@ -3,6 +3,7 @@ package example.imageviewer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
@ -44,3 +45,7 @@ actual fun createUUID(): String =
CFBridgingRelease(CFUUIDCreateString(null, CFUUIDCreate(null))) as String CFBridgingRelease(CFUUIDCreateString(null, CFUUIDCreate(null))) as String
actual val ioDispatcher = Dispatchers.IO actual val ioDispatcher = Dispatchers.IO
actual val isShareFeatureSupported: Boolean = true
actual val shareIcon: ImageVector = IosShareIcon

4
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/storage/FileExtensions.kt

@ -41,6 +41,10 @@ fun NSURL.listFiles(filter: (NSURL, String) -> Boolean) =
?.map { File(this, it) } ?.map { File(this, it) }
?.toTypedArray() ?.toTypedArray()
fun NSURL.delete() {
NSFileManager.defaultManager.removeItemAtURL(this, null)
}
suspend fun NSURL.readData(): NSData { suspend fun NSURL.readData(): NSData {
while (true) { while (true) {
val data = NSData.dataWithContentsOfURL(this) val data = NSData.dataWithContentsOfURL(this)

48
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 { ioScope.launch {
with(image.rawValue) { with(image.rawValue) {
pictureData.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx)) picture.jpgFile.writeJpeg(fitInto(maxStorableImageSizePx))
pictureData.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx)) picture.thumbnailJpgFile.writeJpeg(fitInto(storableThumbnailSizePx))
} }
pictures.add(0, pictureData) pictures.add(0, picture)
pictureData.jsonFile.writeText(pictureData.toJson()) 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) { 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) { 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 { private fun UIImage.fitInto(px: Int): UIImage {

77
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,
)
)
}
}
}

28
experimental/examples/imageviewer/shared/src/iosMain/kotlin/example/imageviewer/view/LocationVisualizer.ios.kt

@ -1,6 +1,7 @@
package example.imageviewer.view package example.imageviewer.view
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.interop.UIKitInteropView import androidx.compose.ui.interop.UIKitInteropView
import example.imageviewer.model.GpsPosition import example.imageviewer.model.GpsPosition
@ -11,26 +12,29 @@ import platform.MapKit.MKPointAnnotation
@Composable @Composable
internal actual fun LocationVisualizer(modifier: Modifier, gps: GpsPosition, title: String) { 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( UIKitInteropView(
modifier = modifier, modifier = modifier,
factory = { factory = {
val mkMapView = MKMapView() mkMapView
val cityAmsterdam = CLLocationCoordinate2DMake(gps.latitude, gps.longitude) },
update = {
mkMapView.setRegion( mkMapView.setRegion(
MKCoordinateRegionMakeWithDistance( MKCoordinateRegionMakeWithDistance(
centerCoordinate = cityAmsterdam, centerCoordinate = location,
10_000.0, 10_000.0 10_000.0, 10_000.0
), ),
animated = false animated = false
) )
mkMapView.addAnnotation( }
MKPointAnnotation(
cityAmsterdam,
title = title,
subtitle = null
)
)
mkMapView
},
) )
} }

Loading…
Cancel
Save