Add prev and next buttons to carousel

This commit is contained in:
andrew 2023-07-03 17:19:33 +09:30
parent 1902d4755c
commit db4ff94084
4 changed files with 186 additions and 63 deletions

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -9,25 +10,30 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -40,6 +46,7 @@ import androidx.lifecycle.ViewModel
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage import com.bumptech.glide.integration.compose.GlideImage
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
@ -67,7 +74,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MessageDetailActivity: PassphraseRequiredActionBarActivity() { class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
private var timestamp: Long = 0L private var timestamp: Long = 0L
@ -76,7 +83,6 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
@Inject @Inject
lateinit var storage: Storage lateinit var storage: Storage
companion object { companion object {
// Extras // Extras
const val MESSAGE_TIMESTAMP = "message_timestamp" const val MESSAGE_TIMESTAMP = "message_timestamp"
@ -88,7 +94,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
val viewModel = MessageDetailsViewModel() val viewModel = MessageDetailsViewModel()
class MessageDetailsViewModel: ViewModel() { class MessageDetailsViewModel : ViewModel() {
@Inject @Inject
lateinit var attachmentDb: AttachmentDatabase lateinit var attachmentDb: AttachmentDatabase
@ -103,30 +109,45 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
val duration = slide.takeIf { it.hasAudio() } val duration = slide.takeIf { it.hasAudio() }
?.let { it.asAttachment() as? DatabaseAttachment } ?.let { it.asAttachment() as? DatabaseAttachment }
?.let { attachment -> ?.let { attachment ->
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)
audioExtras.durationMs.takeIf { it > 0 }?.let { ?.let { audioExtras ->
String.format("%01d:%02d", audioExtras.durationMs.takeIf { it > 0 }?.let {
TimeUnit.MILLISECONDS.toMinutes(it), String.format(
TimeUnit.MILLISECONDS.toSeconds(it) % 60) "%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(it),
TimeUnit.MILLISECONDS.toSeconds(it) % 60
)
}
} }
}
} }
val details = slide.run { val details = slide.run {
listOfNotNull( listOfNotNull(
fileName.orNull()?.let { TitledText("File Id:", it) }, fileName.orNull()?.let { TitledText("File Id:", it) },
TitledText("File Type:", asAttachment().contentType), TitledText("File Type:", asAttachment().contentType),
TitledText("File Size:", Util.getPrettyFileSize(fileSize)), TitledText("File Size:", Util.getPrettyFileSize(fileSize)),
if (slide.hasImage()) { TitledText("Resolution:", slide.asAttachment().run { "${width}x$height" } ) } else null, if (slide.hasImage()) {
duration?.let { TitledText("Duration:", it) }, TitledText(
) "Resolution:",
} slide.asAttachment().run { "${width}x$height" })
Attachment(slide, details) } else null,
duration?.let { TitledText("Duration:", it) },
)
}
Attachment(slide, details)
}, },
sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) }, sent = dateSent.let(::Date).toString().let { TitledText("Sent:", it) },
received = dateReceived.let(::Date).toString().let { TitledText("Received:", it) }, received = dateReceived.let(::Date).toString()
.let { TitledText("Received:", it) },
error = error?.let { TitledText("Error:", it) }, error = error?.let { TitledText("Error:", it) },
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } }, senderInfo = individualRecipient.run {
name?.let {
TitledText(
it,
address.serialize()
)
}
},
sender = individualRecipient sender = individualRecipient
) )
} }
@ -141,12 +162,14 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run { messageRecord =
finish() DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run {
return finish()
} return
}
val error = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) val error = DatabaseComponent.get(this).lokiMessageDatabase()
.getErrorMessage(messageRecord!!.getId())
viewModel.setMessageRecord(messageRecord, error) viewModel.setMessageRecord(messageRecord, error)
@ -226,16 +249,29 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
sent?.let { titledText(it) } sent?.let { titledText(it) }
received?.let { titledText(it) } received?.let { titledText(it) }
error?.let { titledText(it, valueStyle = LocalTextStyle.current.copy(color = colorDestructive)) } error?.let {
titledText(
it,
valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
)
}
senderInfo?.let { senderInfo?.let {
titledView("From:") { titledView("From:") {
Row { Row {
sender?.let { sender?.let {
Box(modifier = Modifier Box(
.width(60.dp) modifier = Modifier
.align(Alignment.CenterVertically)) { .width(60.dp)
.align(Alignment.CenterVertically)
) {
AndroidView( AndroidView(
factory = { ProfilePictureView(it).apply { update(sender) } }, factory = {
ProfilePictureView(it).apply {
update(
sender
)
}
},
modifier = Modifier modifier = Modifier
.width(46.dp) .width(46.dp)
.height(46.dp) .height(46.dp)
@ -243,22 +279,38 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
} }
} }
Column { Column {
titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)) titledText(
it,
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
)
} }
} }
} }
} }
} }
} }
Cell { Cell {
Column { Column {
ItemButton("Reply", R.drawable.ic_message_details__reply, onClick = onReply) ItemButton(
"Reply",
R.drawable.ic_message_details__reply,
onClick = onReply
)
Divider() Divider()
if (error != null) { if (error != null) {
ItemButton("Resend", R.drawable.ic_message_details__refresh, onClick = onResend) ItemButton(
"Resend",
R.drawable.ic_message_details__refresh,
onClick = onResend
)
Divider() Divider()
} }
ItemButton("Delete", R.drawable.ic_message_details__trash, colors = destructiveButtonColors(), onClick = onDelete) ItemButton(
"Delete",
R.drawable.ic_message_details__trash,
colors = destructiveButtonColors(),
onClick = onDelete
)
} }
} }
} }
@ -277,26 +329,27 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
@OptIn( @OptIn(
ExperimentalFoundationApi::class, ExperimentalFoundationApi::class,
ExperimentalGlideComposeApi::class, ExperimentalGlideComposeApi::class,
ExperimentalLayoutApi::class,
) )
@Composable @Composable
fun ImageAttachments(attachments: List<Attachment>) { fun ImageAttachments(attachments: List<Attachment>) {
val imageAttachments = attachments.filter { it.slide.hasImage() } val imageAttachments = attachments.filter { it.slide.hasImage() }
val pagerState = rememberPagerState { val pagerState = rememberPagerState { imageAttachments.size }
imageAttachments.size
}
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
CellNoMargin { Row {
Box { if (imageAttachments.size >= 2) PrevButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically))
HorizontalPager(state = pagerState) { i -> else Spacer(modifier = Modifier.width(32.dp))
imageAttachments[i].slide.apply { Box(modifier = Modifier.weight(1f)) {
GlideImage( CellNoMargin {
contentScale = ContentScale.Crop, HorizontalPager(state = pagerState) { i ->
modifier = Modifier.aspectRatio(1f), imageAttachments[i].slide.apply {
model = uri, GlideImage(
contentDescription = fileName.orNull() ?: "image" contentScale = ContentScale.Crop,
) modifier = Modifier.aspectRatio(1f),
model = uri,
contentDescription = fileName.orNull() ?: "image"
)
}
} }
} }
if (imageAttachments.size >= 2) { if (imageAttachments.size >= 2) {
@ -307,15 +360,61 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
) )
} }
} }
if (imageAttachments.size >= 2) NextButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically))
else Spacer(modifier = Modifier.width(32.dp))
} }
attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let {
CellWithPaddingAndMargin { FileDetails(attachments, pagerState)
FlowRow( }
verticalArrangement = Arrangement.spacedBy(16.dp), }
maxItemsInEachRow = 2
) { @OptIn(ExperimentalFoundationApi::class)
it.forEach { titledText(it, Modifier.weight(1f)) } @Composable
} fun PrevButton(pagerState: PagerState, modifier: Modifier = Modifier) {
CarouselButton(pagerState, modifier = modifier, enabled = pagerState.canScrollBackward, id = R.drawable.ic_prev, delta = -1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NextButton(pagerState: PagerState, modifier: Modifier = Modifier) {
CarouselButton(pagerState, modifier = modifier, enabled = pagerState.canScrollForward, id = R.drawable.ic_next, delta = 1)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CarouselButton(
pagerState: PagerState,
modifier: Modifier = Modifier,
enabled: Boolean,
@DrawableRes id: Int,
delta: Int
) {
val animationScope = rememberCoroutineScope()
pagerState.apply {
IconButton(
modifier = Modifier
.width(40.dp)
.then(modifier),
enabled = enabled,
onClick = { animationScope.launch { animateScrollToPage(currentPage + delta) } }) {
Icon(
painter = painterResource(id = id),
contentDescription = "",
)
}
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
@Composable
fun FileDetails(attachments: List<Attachment>, pagerState: PagerState) {
attachments[pagerState.currentPage].fileDetails.takeIf { it.isNotEmpty() }?.let {
CellWithPaddingAndMargin {
FlowRow(
verticalArrangement = Arrangement.spacedBy(16.dp),
maxItemsInEachRow = 2
) {
it.forEach { titledText(it, Modifier.weight(1f)) }
} }
} }
} }
@ -323,11 +422,19 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
@Composable @Composable
fun Divider() { fun Divider() {
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 1.dp, color = LocalExtraColors.current.divider) Divider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 1.dp,
color = LocalExtraColors.current.divider
)
} }
@Composable @Composable
fun titledText(titledText: TitledText, modifier: Modifier = Modifier, valueStyle: TextStyle = LocalTextStyle.current) { fun titledText(
titledText: TitledText,
modifier: Modifier = Modifier,
valueStyle: TextStyle = LocalTextStyle.current
) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
Title(titledText.title) Title(titledText.title)
Text(titledText.value, style = valueStyle) Text(titledText.value, style = valueStyle)

View File

@ -57,11 +57,11 @@ fun ItemButton(
@Composable @Composable
fun Cell(content: @Composable () -> Unit) { fun Cell(content: @Composable () -> Unit) {
CellWithPaddingAndMargin(0.dp) { content() } CellWithPaddingAndMargin(padding = 0.dp) { content() }
} }
@Composable @Composable
fun CellNoMargin(content: @Composable () -> Unit) { fun CellNoMargin(content: @Composable () -> Unit) {
CellWithPaddingAndMargin(0.dp, 0.dp) { content() } CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() }
} }
@Composable @Composable

View File

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="17dp"
android:viewportHeight="17" android:viewportWidth="13"
android:width="13dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M13,16.004l-13,-0l-0,-16l13,-0z"/>
<path android:fillColor="#ffffff" android:pathData="M0.646,1.736L10.112,7.933L0.444,14.268C0.323,14.343 0.222,14.438 0.144,14.547C0.067,14.657 0.015,14.779 -0.007,14.906C-0.029,15.033 -0.022,15.163 0.014,15.287C0.05,15.412 0.115,15.529 0.203,15.632C0.292,15.734 0.404,15.82 0.532,15.885C0.66,15.95 0.801,15.991 0.948,16.008C1.095,16.024 1.244,16.015 1.386,15.981C1.529,15.946 1.662,15.887 1.778,15.808L12.353,8.88C12.466,8.805 12.562,8.711 12.635,8.605C12.687,8.563 12.734,8.518 12.778,8.47C12.955,8.266 13.031,8.009 12.99,7.756C12.949,7.503 12.794,7.274 12.559,7.12L1.984,0.193C1.868,0.117 1.736,0.061 1.595,0.029C1.454,-0.003 1.307,-0.011 1.163,0.006C1.018,0.024 0.88,0.066 0.754,0.13C0.628,0.194 0.519,0.279 0.431,0.381C0.343,0.482 0.278,0.597 0.241,0.72C0.204,0.843 0.195,0.971 0.215,1.097C0.235,1.223 0.284,1.344 0.358,1.454C0.432,1.563 0.53,1.659 0.646,1.736Z"/>
</group>
</vector>

View File

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="17dp"
android:viewportHeight="17" android:viewportWidth="12"
android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0.004h12v16h-12z"/>
<path android:fillColor="#ffffff" android:pathData="M11.403,14.272L2.666,8.075L11.59,1.74C11.701,1.665 11.795,1.57 11.867,1.46C11.938,1.351 11.986,1.229 12.006,1.102C12.027,0.975 12.02,0.845 11.987,0.721C11.954,0.596 11.894,0.479 11.812,0.376C11.73,0.274 11.627,0.187 11.509,0.123C11.391,0.058 11.26,0.016 11.125,0C10.989,-0.016 10.852,-0.007 10.72,0.027C10.589,0.062 10.466,0.12 10.359,0.2L0.597,7.127C0.493,7.203 0.405,7.297 0.337,7.403C0.289,7.444 0.245,7.49 0.205,7.538C0.042,7.742 -0.029,7.999 0.009,8.252C0.047,8.505 0.19,8.734 0.407,8.887L10.168,15.815C10.275,15.891 10.398,15.947 10.528,15.979C10.658,16.011 10.793,16.019 10.927,16.001C11.06,15.984 11.188,15.942 11.304,15.878C11.42,15.814 11.521,15.728 11.602,15.627C11.684,15.526 11.743,15.411 11.777,15.288C11.811,15.165 11.82,15.037 11.801,14.911C11.783,14.785 11.738,14.664 11.67,14.554C11.601,14.445 11.511,14.349 11.403,14.272Z"/>
</group>
</vector>