mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-07 21:37:02 +09:00
DROID-2966 Chats | Enhancement | Displaying video thumbnail + Request playing video by OS media player (MVP) (#2505)
This commit is contained in:
parent
8b1e379785
commit
7968718ecb
11 changed files with 207 additions and 15 deletions
|
@ -1,7 +1,6 @@
|
|||
package com.anytypeio.anytype.ui.chats
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -14,7 +13,6 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
@ -41,8 +39,7 @@ import com.anytypeio.anytype.core_models.primitives.Space
|
|||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_utils.ext.arg
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
import com.anytypeio.anytype.core_utils.intents.SystemAction
|
||||
import com.anytypeio.anytype.core_utils.intents.SystemAction.*
|
||||
import com.anytypeio.anytype.core_utils.intents.SystemAction.OpenUrl
|
||||
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
|
@ -113,7 +110,7 @@ class ChatFragment : BaseComposeFragment() {
|
|||
modifier = Modifier.weight(1f),
|
||||
vm = vm,
|
||||
onAttachObjectClicked = { showGlobalSearchBottomSheet = true },
|
||||
onMarkupLinkClicked = { proceedWithAction(SystemAction.OpenUrl(it)) },
|
||||
onMarkupLinkClicked = { proceedWithAction(OpenUrl(it)) },
|
||||
onRequestOpenFullScreenImage = { url -> vm.onMediaPreview(url) },
|
||||
onSelectChatReaction = vm::onSelectChatReaction,
|
||||
onViewChatReaction = { msg, emoji ->
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.core_utils.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import java.io.IOException
|
||||
|
||||
fun Context.getJsonDataFromAsset(fileName: String): String? = try {
|
||||
|
@ -11,4 +12,18 @@ fun Context.getJsonDataFromAsset(fileName: String): String? = try {
|
|||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
|
||||
fun getMimeType(context: Context, uri: Uri): String? {
|
||||
return context.contentResolver.getType(uri)
|
||||
}
|
||||
|
||||
fun isImage(uri: Uri, context: Context): Boolean {
|
||||
val mimeType = getMimeType(context, uri)
|
||||
return mimeType?.startsWith("image/") == true
|
||||
}
|
||||
|
||||
fun isVideo(uri: Uri, context: Context): Boolean {
|
||||
val mimeType = getMimeType(context, uri)
|
||||
return mimeType?.startsWith("video/") == true
|
||||
}
|
|
@ -40,6 +40,7 @@ dependencies {
|
|||
implementation libs.composeMaterial
|
||||
|
||||
implementation libs.coilCompose
|
||||
implementation libs.coilVideo
|
||||
annotationProcessor libs.glideCompiler
|
||||
implementation libs.glideCompose
|
||||
|
||||
|
|
|
@ -91,6 +91,13 @@ sealed interface ChatView {
|
|||
val ext: String
|
||||
): Attachment()
|
||||
|
||||
data class Video(
|
||||
val target: Id,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val ext: String
|
||||
): Attachment()
|
||||
|
||||
data class Link(
|
||||
val target: Id,
|
||||
val wrapper: ObjectWrapper.Basic?,
|
||||
|
@ -111,7 +118,8 @@ sealed interface ChatView {
|
|||
|
||||
data class Media(
|
||||
val uri: String,
|
||||
val state: State = State.Idle
|
||||
val state: State = State.Idle,
|
||||
val isVideo: Boolean = false
|
||||
): ChatBoxAttachment()
|
||||
|
||||
data class File(
|
||||
|
@ -127,6 +135,11 @@ sealed interface ChatView {
|
|||
val url: Url
|
||||
) : Existing()
|
||||
|
||||
data class Video(
|
||||
val target: Id,
|
||||
val url: Url
|
||||
) : Existing()
|
||||
|
||||
data class Link(
|
||||
val target: Id,
|
||||
val name: String,
|
||||
|
|
|
@ -301,7 +301,7 @@ class ChatViewModel @Inject constructor(
|
|||
val wrapper = dependencies[attachment.target]
|
||||
ChatView.Message.Attachment.Image(
|
||||
target = attachment.target,
|
||||
url = urlBuilder.medium(path = attachment.target),
|
||||
url = urlBuilder.large(path = attachment.target),
|
||||
name = wrapper?.name.orEmpty(),
|
||||
ext = wrapper?.fileExt.orEmpty()
|
||||
)
|
||||
|
@ -317,6 +317,14 @@ class ChatViewModel @Inject constructor(
|
|||
ext = wrapper.fileExt.orEmpty()
|
||||
)
|
||||
}
|
||||
ObjectType.Layout.VIDEO -> {
|
||||
ChatView.Message.Attachment.Video(
|
||||
target = attachment.target,
|
||||
url = urlBuilder.large(path = attachment.target),
|
||||
name = wrapper.name.orEmpty(),
|
||||
ext = wrapper.fileExt.orEmpty()
|
||||
)
|
||||
}
|
||||
ObjectType.Layout.BOOKMARK -> {
|
||||
ChatView.Message.Attachment.Bookmark(
|
||||
id = wrapper.id,
|
||||
|
@ -525,6 +533,14 @@ class ChatViewModel @Inject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
is ChatView.Message.ChatBoxAttachment.Existing.Video -> {
|
||||
add(
|
||||
Chat.Message.Attachment(
|
||||
target = attachment.target,
|
||||
type = Chat.Message.Attachment.Type.File
|
||||
)
|
||||
)
|
||||
}
|
||||
is ChatView.Message.ChatBoxAttachment.Media -> {
|
||||
chatBoxAttachments.value = currAttachments.toMutableList().apply {
|
||||
set(
|
||||
|
@ -538,13 +554,19 @@ class ChatViewModel @Inject constructor(
|
|||
UploadFile.Params(
|
||||
space = vmParams.space,
|
||||
path = attachment.uri,
|
||||
type = Block.Content.File.Type.IMAGE
|
||||
type = if (attachment.isVideo)
|
||||
Block.Content.File.Type.VIDEO
|
||||
else
|
||||
Block.Content.File.Type.IMAGE
|
||||
)
|
||||
).onSuccess { file ->
|
||||
add(
|
||||
Chat.Message.Attachment(
|
||||
target = file.id,
|
||||
type = Chat.Message.Attachment.Type.Image
|
||||
type = if (attachment.isVideo)
|
||||
Chat.Message.Attachment.Type.File
|
||||
else
|
||||
Chat.Message.Attachment.Type.Image
|
||||
)
|
||||
)
|
||||
chatBoxAttachments.value = currAttachments.toMutableList().apply {
|
||||
|
@ -736,6 +758,14 @@ class ChatViewModel @Inject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
is ChatView.Message.Attachment.Video -> {
|
||||
add(
|
||||
ChatView.Message.ChatBoxAttachment.Existing.Video(
|
||||
target = a.target,
|
||||
url = a.url
|
||||
)
|
||||
)
|
||||
}
|
||||
is ChatView.Message.Attachment.Bookmark -> {
|
||||
add(
|
||||
ChatView.Message.ChatBoxAttachment.Existing.Link(
|
||||
|
@ -839,6 +869,13 @@ class ChatViewModel @Inject constructor(
|
|||
attachment.name
|
||||
}
|
||||
}
|
||||
is ChatView.Message.Attachment.Video -> {
|
||||
if (attachment.ext.isNotEmpty()) {
|
||||
"${attachment.name}.${attachment.ext}"
|
||||
} else {
|
||||
attachment.name
|
||||
}
|
||||
}
|
||||
is ChatView.Message.Attachment.Gallery -> {
|
||||
val first = attachment.images.firstOrNull()
|
||||
if (first != null) {
|
||||
|
@ -893,9 +930,12 @@ class ChatViewModel @Inject constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
is ChatView.Message.Attachment.Gallery -> {
|
||||
is ChatView.Message.Attachment.Video -> {
|
||||
// TODO
|
||||
}
|
||||
is ChatView.Message.Attachment.Gallery -> {
|
||||
// Do nothing.
|
||||
}
|
||||
is ChatView.Message.Attachment.Bookmark -> {
|
||||
commands.emit(ViewModelCommand.Browse(attachment.url))
|
||||
}
|
||||
|
@ -922,11 +962,12 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onChatBoxMediaPicked(uris: List<String>) {
|
||||
fun onChatBoxMediaPicked(uris: List<ChatBoxMediaUri>) {
|
||||
Timber.d("onChatBoxMediaPicked: $uris")
|
||||
chatBoxAttachments.value += uris.map {
|
||||
chatBoxAttachments.value += uris.map { uri ->
|
||||
ChatView.Message.ChatBoxAttachment.Media(
|
||||
uri = it
|
||||
uri = uri.uri,
|
||||
isVideo = uri.isVideo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1193,6 +1234,11 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
data class ChatBoxMediaUri(
|
||||
val uri: String,
|
||||
val isVideo: Boolean = false
|
||||
)
|
||||
|
||||
sealed class ViewModelCommand {
|
||||
data object Exit : ViewModelCommand()
|
||||
data object OpenWidgets : ViewModelCommand()
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.anytypeio.anytype.feature_chats.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -22,11 +25,17 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.rememberAsyncImagePainter
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import coil3.video.videoFrameMillis
|
||||
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
|
||||
import com.anytypeio.anytype.core_ui.views.Relations3
|
||||
|
@ -39,6 +48,7 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
|||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import timber.log.Timber
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class)
|
||||
|
@ -48,6 +58,8 @@ fun BubbleAttachments(
|
|||
onAttachmentLongClicked: (ChatView.Message.Attachment) -> Unit,
|
||||
isUserAuthor: Boolean
|
||||
) {
|
||||
Timber.d("Binding attachments: $attachments")
|
||||
val context = LocalContext.current
|
||||
attachments.forEachIndexed { idx, attachment ->
|
||||
when (attachment) {
|
||||
is ChatView.Message.Attachment.Gallery -> {
|
||||
|
@ -64,6 +76,52 @@ fun BubbleAttachments(
|
|||
index += rowSize
|
||||
}
|
||||
}
|
||||
is ChatView.Message.Attachment.Video -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(292.dp)
|
||||
.background(
|
||||
color = colorResource(R.color.shape_tertiary),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
requestPlayingVideoByOS(attachment, context)
|
||||
},
|
||||
onLongClick = {
|
||||
onAttachmentLongClicked(attachment)
|
||||
}
|
||||
)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.Center)
|
||||
.size(48.dp),
|
||||
color = colorResource(R.color.glyph_active),
|
||||
trackColor = colorResource(R.color.glyph_active).copy(alpha = 0.5f),
|
||||
strokeWidth = 4.dp
|
||||
)
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.size(292.dp)
|
||||
.clip(RoundedCornerShape(12.dp)),
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(attachment.url)
|
||||
.videoFrameMillis(0)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Image(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
painter = painterResource(id = R.drawable.ic_chat_attachment_play),
|
||||
contentDescription = "Play button"
|
||||
)
|
||||
}
|
||||
}
|
||||
is ChatView.Message.Attachment.Image -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
@ -146,6 +204,17 @@ fun BubbleAttachments(
|
|||
}
|
||||
}
|
||||
|
||||
private fun requestPlayingVideoByOS(
|
||||
attachment: ChatView.Message.Attachment.Video,
|
||||
context: Context
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(Uri.parse(attachment.url), "video/*")
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AttachedObject(
|
||||
|
|
|
@ -270,7 +270,7 @@ fun ChatBox(
|
|||
onClick = {
|
||||
showDropdownMenu = false
|
||||
uploadMediaLauncher.launch(
|
||||
PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageAndVideo)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -171,6 +171,35 @@ internal fun ChatBoxAttachments(
|
|||
}
|
||||
}
|
||||
}
|
||||
is ChatView.Message.ChatBoxAttachment.Existing.Video -> {
|
||||
item {
|
||||
Box(modifier = Modifier.padding()) {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(attachment.url),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
top = 12.dp,
|
||||
end = 4.dp
|
||||
)
|
||||
.size(72.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_clear_chatbox_attachment),
|
||||
contentDescription = "Clear attachment icon",
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 6.dp)
|
||||
.noRippleClickable {
|
||||
onClearAttachmentClicked(attachment)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ChatView.Message.ChatBoxAttachment.File -> {
|
||||
item {
|
||||
Box {
|
||||
|
|
|
@ -83,6 +83,7 @@ import com.anytypeio.anytype.core_ui.views.Caption1Medium
|
|||
import com.anytypeio.anytype.core_ui.views.Caption1Regular
|
||||
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
|
||||
import com.anytypeio.anytype.core_utils.common.DefaultFileInfo
|
||||
import com.anytypeio.anytype.core_utils.ext.isVideo
|
||||
import com.anytypeio.anytype.core_utils.ext.parseImagePath
|
||||
import com.anytypeio.anytype.domain.chats.ChatContainer
|
||||
import com.anytypeio.anytype.feature_chats.R
|
||||
|
@ -191,7 +192,17 @@ fun ChatScreenWrapper(
|
|||
onReplyMessage = vm::onReplyMessage,
|
||||
onClearReplyClicked = vm::onClearReplyClicked,
|
||||
onChatBoxMediaPicked = { uris ->
|
||||
vm.onChatBoxMediaPicked(uris.map { it.parseImagePath(context = context) })
|
||||
vm.onChatBoxMediaPicked(
|
||||
uris.map {
|
||||
ChatViewModel.ChatBoxMediaUri(
|
||||
uri = it.parseImagePath(context = context),
|
||||
isVideo = isVideo(
|
||||
uri = it,
|
||||
context = context
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
onChatBoxFilePicked = { uris ->
|
||||
val infos = uris.mapNotNull { uri ->
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="34dp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="34">
|
||||
<path
|
||||
android:pathData="M0,31.58V2.42C0,0.89 1.647,-0.073 2.981,0.677L28.901,15.257C30.26,16.021 30.26,17.979 28.901,18.743L2.981,33.324C1.647,34.073 0,33.11 0,31.58Z"
|
||||
android:fillColor="@color/glyph_white"/>
|
||||
</vector>
|
|
@ -61,6 +61,7 @@ roomVersion = '2.7.0'
|
|||
dataStoreVersion = '1.1.4'
|
||||
amplitudeVersion = '3.35.1'
|
||||
coilComposeVersion = '3.1.0'
|
||||
coilVideoVersion = '3.2.0'
|
||||
sentryVersion = '7.13.0'
|
||||
firebaseBomVersion = "33.13.0"
|
||||
|
||||
|
@ -98,6 +99,7 @@ glide = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersio
|
|||
glideCompiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glideVersion" }
|
||||
glideCompose = { module = "com.github.bumptech.glide:compose", version.ref = "glideComposeVersion" }
|
||||
coilCompose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilComposeVersion" }
|
||||
coilVideo = { module = "io.coil-kt.coil3:coil-video", version.ref = "coilVideoVersion" }
|
||||
coilNetwork = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilComposeVersion" }
|
||||
mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlinVersion" }
|
||||
junit = { module = "junit:junit", version.ref = "junitVersion" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue