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.os.Bundle
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
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.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
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.GlideImage
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.utilities.Util
@ -67,7 +74,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
private var timestamp: Long = 0L
@ -76,7 +83,6 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
@Inject
lateinit var storage: Storage
companion object {
// Extras
const val MESSAGE_TIMESTAMP = "message_timestamp"
@ -88,7 +94,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
val viewModel = MessageDetailsViewModel()
class MessageDetailsViewModel: ViewModel() {
class MessageDetailsViewModel : ViewModel() {
@Inject
lateinit var attachmentDb: AttachmentDatabase
@ -103,30 +109,45 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
val duration = slide.takeIf { it.hasAudio() }
?.let { it.asAttachment() as? DatabaseAttachment }
?.let { attachment ->
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras ->
audioExtras.durationMs.takeIf { it > 0 }?.let {
String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(it),
TimeUnit.MILLISECONDS.toSeconds(it) % 60)
attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)
?.let { audioExtras ->
audioExtras.durationMs.takeIf { it > 0 }?.let {
String.format(
"%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(it),
TimeUnit.MILLISECONDS.toSeconds(it) % 60
)
}
}
}
}
val details = slide.run {
listOfNotNull(
fileName.orNull()?.let { TitledText("File Id:", it) },
TitledText("File Type:", asAttachment().contentType),
TitledText("File Size:", Util.getPrettyFileSize(fileSize)),
if (slide.hasImage()) { TitledText("Resolution:", slide.asAttachment().run { "${width}x$height" } ) } else null,
duration?.let { TitledText("Duration:", it) },
)
}
Attachment(slide, details)
val details = slide.run {
listOfNotNull(
fileName.orNull()?.let { TitledText("File Id:", it) },
TitledText("File Type:", asAttachment().contentType),
TitledText("File Size:", Util.getPrettyFileSize(fileSize)),
if (slide.hasImage()) {
TitledText(
"Resolution:",
slide.asAttachment().run { "${width}x$height" })
} else null,
duration?.let { TitledText("Duration:", it) },
)
}
Attachment(slide, details)
},
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) },
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
senderInfo = individualRecipient.run {
name?.let {
TitledText(
it,
address.serialize()
)
}
},
sender = individualRecipient
)
}
@ -141,12 +162,14 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run {
finish()
return
}
messageRecord =
DatabaseComponent.get(this).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: run {
finish()
return
}
val error = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
val error = DatabaseComponent.get(this).lokiMessageDatabase()
.getErrorMessage(messageRecord!!.getId())
viewModel.setMessageRecord(messageRecord, error)
@ -226,16 +249,29 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
sent?.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 {
titledView("From:") {
Row {
sender?.let {
Box(modifier = Modifier
.width(60.dp)
.align(Alignment.CenterVertically)) {
Box(
modifier = Modifier
.width(60.dp)
.align(Alignment.CenterVertically)
) {
AndroidView(
factory = { ProfilePictureView(it).apply { update(sender) } },
factory = {
ProfilePictureView(it).apply {
update(
sender
)
}
},
modifier = Modifier
.width(46.dp)
.height(46.dp)
@ -243,22 +279,38 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
}
}
Column {
titledText(it, valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace))
titledText(
it,
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
)
}
}
}
}
}
}
}
Cell {
Column {
ItemButton("Reply", R.drawable.ic_message_details__reply, onClick = onReply)
ItemButton(
"Reply",
R.drawable.ic_message_details__reply,
onClick = onReply
)
Divider()
if (error != null) {
ItemButton("Resend", R.drawable.ic_message_details__refresh, onClick = onResend)
ItemButton(
"Resend",
R.drawable.ic_message_details__refresh,
onClick = onResend
)
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(
ExperimentalFoundationApi::class,
ExperimentalGlideComposeApi::class,
ExperimentalLayoutApi::class,
)
@Composable
fun ImageAttachments(attachments: List<Attachment>) {
val imageAttachments = attachments.filter { it.slide.hasImage() }
val pagerState = rememberPagerState {
imageAttachments.size
}
val pagerState = rememberPagerState { imageAttachments.size }
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
CellNoMargin {
Box {
HorizontalPager(state = pagerState) { i ->
imageAttachments[i].slide.apply {
GlideImage(
contentScale = ContentScale.Crop,
modifier = Modifier.aspectRatio(1f),
model = uri,
contentDescription = fileName.orNull() ?: "image"
)
Row {
if (imageAttachments.size >= 2) PrevButton(pagerState, modifier = Modifier.align(Alignment.CenterVertically))
else Spacer(modifier = Modifier.width(32.dp))
Box(modifier = Modifier.weight(1f)) {
CellNoMargin {
HorizontalPager(state = pagerState) { i ->
imageAttachments[i].slide.apply {
GlideImage(
contentScale = ContentScale.Crop,
modifier = Modifier.aspectRatio(1f),
model = uri,
contentDescription = fileName.orNull() ?: "image"
)
}
}
}
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 {
FlowRow(
verticalArrangement = Arrangement.spacedBy(16.dp),
maxItemsInEachRow = 2
) {
it.forEach { titledText(it, Modifier.weight(1f)) }
}
FileDetails(attachments, pagerState)
}
}
@OptIn(ExperimentalFoundationApi::class)
@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
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
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)) {
Title(titledText.title)
Text(titledText.value, style = valueStyle)

View File

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