mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
ImageViewer delete, edit and share memories (#2957)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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.ACCESS_COARSE_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>
|
||||
@@ -0,0 +1,6 @@
|
||||
package example.imageviewer
|
||||
|
||||
import androidx.core.content.FileProvider
|
||||
import example.imageviewer.shared.R
|
||||
|
||||
class ImageViewerFileProvider : FileProvider(R.xml.file_paths)
|
||||
@@ -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
|
||||
|
||||
@@ -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 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()
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="my_images" path="share_images/"/>
|
||||
</paths>
|
||||
@@ -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<PictureData> = mutableStateListOf(*resourcePictures)
|
||||
open val externalEvents: Flow<ExternalImageViewerEvent> = 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<Localization> {
|
||||
@@ -86,14 +131,14 @@ internal val LocalImageProvider = staticCompositionLocalOf<ImageProvider> {
|
||||
noLocalProvidedFor("LocalImageProvider")
|
||||
}
|
||||
|
||||
internal val LocalImageStorage = staticCompositionLocalOf<ImageStorage> {
|
||||
noLocalProvidedFor("LocalImageStorage")
|
||||
}
|
||||
|
||||
internal val LocalInternalEvents = staticCompositionLocalOf<Flow<ExternalImageViewerEvent>> {
|
||||
noLocalProvidedFor("LocalInternalEvents")
|
||||
}
|
||||
|
||||
internal val LocalSharePicture = staticCompositionLocalOf<SharePicture> {
|
||||
noLocalProvidedFor("LocalSharePicture")
|
||||
}
|
||||
|
||||
private fun noLocalProvidedFor(name: String): Nothing {
|
||||
error("CompositionLocal $name not present")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<PictureData>) : Page
|
||||
class CameraPage : Page
|
||||
class FullScreenPage(val picture: PictureData) : Page
|
||||
class GalleryPage : Page
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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<PictureData>,
|
||||
@@ -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<PictureData>,
|
||||
pictures: List<PictureData>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,23 +20,31 @@ class DesktopImageStorage(
|
||||
private val largeImages = 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) {
|
||||
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]!!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>(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<ToastState
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,8 @@ package example.imageviewer
|
||||
import androidx.compose.ui.window.ComposeUIViewController
|
||||
import platform.UIKit.UIViewController
|
||||
|
||||
fun MainViewController(): UIViewController = ComposeUIViewController { ImageViewerIos() }
|
||||
fun MainViewController(): UIViewController =
|
||||
ComposeUIViewController {
|
||||
ImageViewerIos()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package example.imageviewer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import kotlinx.cinterop.useContents
|
||||
@@ -44,3 +45,7 @@ actual fun createUUID(): String =
|
||||
CFBridgingRelease(CFUUIDCreateString(null, CFUUIDCreate(null))) as String
|
||||
|
||||
actual val ioDispatcher = Dispatchers.IO
|
||||
|
||||
actual val isShareFeatureSupported: Boolean = true
|
||||
|
||||
actual val shareIcon: ImageVector = IosShareIcon
|
||||
|
||||
@@ -41,6 +41,10 @@ fun NSURL.listFiles(filter: (NSURL, String) -> 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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user