yayan 1 rok pred
rodič
commit
17efb43f07

+ 6 - 3
app/build.gradle

@@ -4,6 +4,8 @@ plugins {
     id 'com.google.dagger.hilt.android'
     id 'kotlin-kapt'
     id 'com.google.devtools.ksp'
+    id 'kotlin-parcelize'
+    id 'org.jetbrains.kotlin.plugin.serialization'
 }
 
 android {
@@ -78,7 +80,7 @@ dependencies {
     debugImplementation("androidx.compose.ui:ui-test-manifest")
 
     implementation 'androidx.navigation:navigation-compose:2.7.7'
-    implementation 'io.coil-kt:coil-compose:2.2.2'
+    implementation 'io.coil-kt:coil-compose:2.6.0'
 
     implementation platform('androidx.compose:compose-bom:2024.02.01')
     implementation "androidx.compose.ui:ui"
@@ -88,8 +90,9 @@ dependencies {
     implementation "androidx.compose.runtime:runtime"
     implementation "androidx.compose.runtime:runtime-livedata"
 
-    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
-    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
+    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0'
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
 
     implementation "com.google.dagger:hilt-android:2.49"
     kapt "com.google.dagger:hilt-android-compiler:2.48"

+ 7 - 0
app/src/main/AndroidManifest.xml

@@ -4,6 +4,13 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission
+        android:name="android.permission.READ_EXTERNAL_STORAGE"
+        android:maxSdkVersion="32" />
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+
 
     <uses-feature android:name="android.hardware.camera.any" />
 

+ 193 - 2
app/src/main/java/io/nexilis/alpha/ui/components/Attachments.kt

@@ -1,13 +1,26 @@
+import android.Manifest
+import android.content.Context
+import android.net.Uri
+import android.os.Bundle
+import android.os.Parcelable
+import android.provider.OpenableColumns
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.AttachFile
 import androidx.compose.material.icons.filled.Camera
@@ -20,27 +33,51 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
 import androidx.compose.material3.TooltipDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Popup
+import androidx.core.net.toUri
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import coil.compose.AsyncImage
+import coil.request.ImageRequest
+import io.nexilis.alpha.R
 import io.nexilis.service.core.BlueSky
 import io.nexilis.service.core.DarkLimeGreen
 import io.nexilis.service.core.Orange
 import io.nexilis.service.core.Purple
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
-fun Attachments(modifier: Modifier) {
+fun Attachments(modifier: Modifier, onAttachment: (List<Uri>) -> Unit) {
     val keyboardController = LocalSoftwareKeyboardController.current
     val positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider()
     var isOpenMenu by remember {
@@ -62,9 +99,36 @@ fun Attachments(modifier: Modifier) {
             Box(modifier = modifier) {
                 Row(Modifier.align(Alignment.Center)) {
                     Column {
+                        val context = LocalContext.current
+                        val launcher =
+                            rememberLauncherForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
+                                onAttachment(it)
+                                isOpenMenu = false
+                            }
+                        val permissionLauncher = rememberLauncherForActivityResult(
+                            ActivityResultContracts.RequestPermission()
+                        ) {
+                            if (it) {
+                                Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT)
+                                    .show()
+                                launcher.launch(
+                                    arrayOf(
+                                        "doc/*",
+                                        "docx/*",
+                                        "application/*",
+                                        "image/*"
+                                    )
+                                )
+                            } else {
+                                Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT)
+                                    .show()
+                            }
+                        }
                         AttachmentButton(
                             color = MaterialTheme.colorScheme.primary,
-                            onClick = {},
+                            onClick = {
+                                permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
+                            },
                             text = {
                                 Text(text = "File", style = MaterialTheme.typography.bodySmall)
                             }) {
@@ -160,4 +224,131 @@ fun AttachmentButton(
         Spacer(modifier = Modifier.size(6.dp))
         text()
     }
+}
+
+fun Context.getAttachmentItem(uri: Uri): AttachmentItem? {
+    val item = contentResolver.query(uri, null, null, null, null)
+        ?.use { cursor ->
+            val nameIndex =
+                cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+            val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
+            cursor.moveToFirst()
+            AttachmentItem(
+                cursor.getString(nameIndex),
+                cursor.getLong(sizeIndex),
+                uri
+            )
+        }
+    return item
+}
+
+object UriSerializable : KSerializer<Uri> {
+    override val descriptor: SerialDescriptor
+        get() = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
+
+    override fun deserialize(decoder: Decoder): Uri {
+        return decoder.decodeString().toUri()
+    }
+
+    override fun serialize(encoder: Encoder, value: Uri) {
+        encoder.encodeString(value.toString())
+    }
+
+}
+
+@Serializable
+@Parcelize
+data class AttachmentItem(
+    val name: String,
+    val size: Long,
+    @Serializable(with = UriSerializable::class)
+    val uri: Uri
+) : Parcelable
+
+@Serializable
+@Parcelize
+data class AttachmentItemList(
+    val list: List<AttachmentItem?>
+) : Parcelable
+
+val AttachmentItemListType = object : NavType<AttachmentItemList>(isNullableAllowed = false) {
+    override fun put(bundle: Bundle, key: String, value: AttachmentItemList) {
+        bundle.putParcelable(key, value)
+    }
+
+    override fun get(bundle: Bundle, key: String): AttachmentItemList? {
+        return bundle.getParcelable<AttachmentItemList>(key)
+    }
+
+    override fun parseValue(value: String): AttachmentItemList {
+        return Json.decodeFromString<AttachmentItemList>(value)
+    }
+}
+
+@Composable
+fun AttachmentCaption(navController: NavHostController, attachments: AttachmentItemList) {
+    var textInput by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+        mutableStateOf(TextFieldValue("", TextRange(0, 7)))
+    }
+    val context = LocalContext.current
+    Column(modifier = Modifier.fillMaxSize()) {
+        attachments.list[0]?.let {
+            val mimeType = context.contentResolver.getType(it.uri)
+            if ((mimeType != null) && mimeType.startsWith("image")) {
+                AsyncImage(
+                    model = ImageRequest.Builder(LocalContext.current)
+                        .data(it.uri)
+                        .addHeader("Cookie", "PHPSESSID=123;MOBILE=123")
+                        .crossfade(true)
+                        .build(),
+                    placeholder = painterResource(R.drawable.ic_placeholder),
+                    contentDescription = "",
+                    contentScale = ContentScale.Fit,
+                    modifier = Modifier
+                        .weight(1f)
+                )
+            }
+        }
+        TextField(
+            modifier = Modifier
+                .fillMaxWidth()
+                .graphicsLayer {
+                    shape = CircleShape
+                    clip = true
+                },
+            value = textInput,
+            onValueChange = { textInput = it },
+            label = null,
+            placeholder = {
+                Text(text = "Add a caption...", color = MaterialTheme.colorScheme.onSurface)
+            },
+            trailingIcon = {
+                Row {
+                    IconButton(onClick = {
+                        if (textInput.text.trim().isEmpty()) {
+                            return@IconButton
+                        }
+                        textInput = TextFieldValue("")
+                    }) {
+                        Icon(
+                            imageVector = Icons.AutoMirrored.Filled.Send,
+                            contentDescription = "",
+                            tint = MaterialTheme.colorScheme.primary
+                        )
+                    }
+                }
+            },
+            colors = TextFieldDefaults.colors(
+                focusedContainerColor = MaterialTheme.colorScheme.surface,
+                unfocusedContainerColor = MaterialTheme.colorScheme.surface,
+                disabledContainerColor = MaterialTheme.colorScheme.surface,
+                focusedIndicatorColor = Color.Transparent,
+                unfocusedIndicatorColor = Color.Transparent,
+                disabledIndicatorColor = Color.Transparent,
+                errorIndicatorColor = Color.Transparent,
+            ),
+            keyboardOptions = KeyboardOptions(KeyboardCapitalization.Sentences),
+            maxLines = 3
+        )
+    }
 }

+ 25 - 8
app/src/main/java/io/nexilis/alpha/ui/main/Chat.kt

@@ -1,6 +1,8 @@
 package io.nexilis.alpha.ui.main
 
 import Attachments
+import AttachmentItemList
+import android.net.Uri
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyColumn
@@ -17,7 +19,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -25,16 +26,21 @@ import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.hilt.navigation.compose.hiltViewModel
 import androidx.navigation.NavHostController
+import getAttachmentItem
 import io.nexilis.alpha.ui.components.ContentChat
 import io.nexilis.alpha.ui.components.LeftBubbleChat
 import io.nexilis.alpha.ui.components.RightBubbleChat
+import io.nexilis.alpha.ui.screen.Screen
 import io.nexilis.service.data.entities.Buddy
 import io.nexilis.service.data.entities.Message
 import io.nexilis.service.data.viewmodels.MessageViewModel
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
 
 @Composable
 fun Chat(
     navController: NavHostController,
+    contentPadding: PaddingValues = PaddingValues(0.dp),
     pin: String,
     me: Buddy
 ) {
@@ -48,11 +54,8 @@ fun Chat(
     Column(
         modifier = Modifier
             .fillMaxSize()
-            .onSizeChanged {
-
-            }
             .background(color = MaterialTheme.colorScheme.surfaceContainer)
-            .padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
+            .padding(contentPadding)
     ) {
         LazyColumn(
             modifier = Modifier.weight(1.0f),
@@ -96,8 +99,7 @@ fun Chat(
                 .graphicsLayer {
                     shape = CircleShape
                     clip = true
-                }
-                .imePadding(),
+                },
             value = textInput,
             onValueChange = { textInput = it },
             label = null,
@@ -114,7 +116,22 @@ fun Chat(
                             shape = RoundedCornerShape(16.dp)
                             clip = true
                         }
-                        .background(MaterialTheme.colorScheme.surface)
+                        .background(MaterialTheme.colorScheme.surface),
+                    onAttachment = { list ->
+                        val items = list.map {
+                            context.getAttachmentItem(it)
+                        }
+                        if (items.size == 1) {
+                            val params = AttachmentItemList(items)
+                            val data = Uri.encode(Json.encodeToString(params))
+                            navController.navigate(Screen.AttachmentCaption.route + "/${data}") {
+                                launchSingleTop = true
+                                restoreState = true
+                            }
+                        } else if (items.size > 1) {
+
+                        }
+                    }
                 )
             },
             trailingIcon = {

+ 15 - 0
app/src/main/java/io/nexilis/alpha/ui/main/Main.kt

@@ -1,5 +1,8 @@
 package io.nexilis.alpha.ui.main
 
+import AttachmentCaption
+import AttachmentItemList
+import AttachmentItemListType
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
@@ -111,6 +114,18 @@ fun Main(navController: NavHostController, me: Buddy) {
                         }
                     }
                 }
+                composable(
+                    route = Screen.AttachmentCaption.route + "/{data}",
+                    arguments = listOf(navArgument("data") { type = AttachmentItemListType })
+                ) {
+                    it.arguments?.getParcelable<AttachmentItemList>("data")
+                        ?.let { data ->
+                            AttachmentCaption(
+                                navController = navMainController,
+                                attachments = data
+                            )
+                        }
+                }
             }
         }
     }

+ 5 - 10
app/src/main/java/io/nexilis/alpha/ui/main/Sign.kt

@@ -59,8 +59,7 @@ fun SignUp(
     ) {
         OutlinedTextField(
             modifier = Modifier
-                .fillMaxWidth()
-                .imePadding(),
+                .fillMaxWidth(),
             value = textUsername,
             onValueChange = { textUsername = it },
             label = {
@@ -70,8 +69,7 @@ fun SignUp(
         )
         OutlinedTextField(
             modifier = Modifier
-                .fillMaxWidth()
-                .imePadding(),
+                .fillMaxWidth(),
             value = textPassword,
             onValueChange = { textPassword = it },
             label = {
@@ -93,8 +91,7 @@ fun SignUp(
         )
         OutlinedTextField(
             modifier = Modifier
-                .fillMaxWidth()
-                .imePadding(),
+                .fillMaxWidth(),
             value = textConfirmPassword,
             onValueChange = { textConfirmPassword = it },
             label = {
@@ -153,8 +150,7 @@ fun SignIn(navController: NavHostController, completion: (Boolean) -> Unit) {
     ) {
         OutlinedTextField(
             modifier = Modifier
-                .fillMaxWidth()
-                .imePadding(),
+                .fillMaxWidth(),
             value = textUsername,
             onValueChange = { textUsername = it },
             label = {
@@ -164,8 +160,7 @@ fun SignIn(navController: NavHostController, completion: (Boolean) -> Unit) {
         )
         OutlinedTextField(
             modifier = Modifier
-                .fillMaxWidth()
-                .imePadding(),
+                .fillMaxWidth(),
             value = textPassword,
             onValueChange = { textPassword = it },
             label = {

+ 3 - 0
app/src/main/java/io/nexilis/alpha/ui/screen/Screen.kt

@@ -2,6 +2,7 @@ package io.nexilis.alpha.ui.screen
 
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.Chat
+import androidx.compose.material.icons.filled.AttachFile
 import androidx.compose.material.icons.filled.Email
 import androidx.compose.material.icons.filled.Home
 import androidx.compose.material.icons.filled.Person
@@ -20,6 +21,7 @@ sealed class Screen(
     object SignIn : Screen("sign_in", Icons.Filled.Email, "Sign In")
     object SignUp : Screen("sign_up", Icons.Filled.Email, "Sign Up")
     object Friend : Screen("friend", Icons.Filled.Person, "Friend")
+    object AttachmentCaption : Screen("attachment", Icons.Filled.AttachFile, "Attachments")
 }
 
 val barItems = listOf(
@@ -37,4 +39,5 @@ val screenItems = listOf(
     Screen.SignIn,
     Screen.SignUp,
     Screen.Friend,
+    Screen.AttachmentCaption
 )

+ 1 - 9
app/src/main/res/values/colors.xml

@@ -1,10 +1,2 @@
 <?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <color name="purple_200">#FFBB86FC</color>
-    <color name="purple_500">#FF6200EE</color>
-    <color name="purple_700">#FF3700B3</color>
-    <color name="teal_200">#FF03DAC5</color>
-    <color name="teal_700">#FF018786</color>
-    <color name="black">#FF000000</color>
-    <color name="white">#FFFFFFFF</color>
-</resources>
+<resources></resources>

+ 0 - 1
app/src/main/res/values/strings.xml

@@ -1,4 +1,3 @@
 <resources>
     <string name="app_name">Alpha</string>
-    <string name="text_fox">The quick brown fox jumps over the lazy dog</string>
 </resources>

+ 2 - 1
build.gradle

@@ -4,8 +4,9 @@ buildscript {
 plugins {
     id 'com.android.application' version '8.2.2' apply false
     id 'com.android.library' version '8.2.2' apply false
-    id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
     id 'com.google.dagger.hilt.android' version '2.48' apply false
     id 'com.google.devtools.ksp' version '1.9.10-1.0.13' apply false
     id 'com.google.gms.google-services' version '4.3.14' apply false
+    id 'org.jetbrains.kotlin.android' version '1.9.10' apply false
+    id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.10' apply false
 }

+ 3 - 3
cpaas-lite/build.gradle

@@ -51,8 +51,8 @@ dependencies {
     implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
     implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
 
-    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1"
-    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0'
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
 
     implementation "com.google.dagger:hilt-android:2.49"
     kapt "com.google.dagger:hilt-android-compiler:2.48"
@@ -63,7 +63,7 @@ dependencies {
     implementation(platform("com.google.firebase:firebase-bom:32.7.2"))
     implementation "com.google.firebase:firebase-messaging-ktx"
 
-    implementation "io.coil-kt:coil:2.5.0"
+    implementation 'io.coil-kt:coil:2.6.0'
 
 }