forked from melod1n/fast-messenger
reworked chat materials screen and some fixes
This commit is contained in:
+7
-7
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.MaterialType
|
||||
import dev.meloda.fast.chatmaterials.navigation.ChatMaterials
|
||||
import dev.meloda.fast.chatmaterials.util.asPresentation
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
@@ -23,7 +24,7 @@ interface ChatMaterialsViewModel {
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
fun onMetPaginationCondition()
|
||||
fun onPaginationConditionsMet()
|
||||
|
||||
fun onRefresh()
|
||||
|
||||
@@ -33,6 +34,7 @@ interface ChatMaterialsViewModel {
|
||||
}
|
||||
|
||||
class ChatMaterialsViewModelImpl(
|
||||
private val materialType: MaterialType,
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel(), ChatMaterialsViewModel {
|
||||
@@ -57,7 +59,7 @@ class ChatMaterialsViewModelImpl(
|
||||
loadChatMaterials()
|
||||
}
|
||||
|
||||
override fun onMetPaginationCondition() {
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.materials.size }
|
||||
loadChatMaterials()
|
||||
}
|
||||
@@ -75,14 +77,12 @@ class ChatMaterialsViewModelImpl(
|
||||
loadChatMaterials(0)
|
||||
}
|
||||
|
||||
private fun loadChatMaterials(
|
||||
offset: Int = currentOffset.value
|
||||
) {
|
||||
private fun loadChatMaterials(offset: Int = currentOffset.value) {
|
||||
messagesUseCase.getHistoryAttachments(
|
||||
peerId = screenState.value.peerId,
|
||||
count = LOAD_COUNT,
|
||||
offset = offset,
|
||||
attachmentTypes = listOf(screenState.value.attachmentType),
|
||||
attachmentTypes = listOf(materialType.toString()),
|
||||
conversationMessageId = screenState.value.conversationMessageId
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
@@ -126,6 +126,6 @@ class ChatMaterialsViewModelImpl(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 100
|
||||
const val LOAD_COUNT = 30
|
||||
}
|
||||
}
|
||||
|
||||
+38
-2
@@ -1,9 +1,45 @@
|
||||
package dev.meloda.fast.chatmaterials.di
|
||||
|
||||
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import dev.meloda.fast.chatmaterials.model.MaterialType
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val chatMaterialsModule = module {
|
||||
viewModelOf(::ChatMaterialsViewModelImpl)
|
||||
viewModel(named(MaterialType.PHOTO)) {
|
||||
ChatMaterialsViewModelImpl(
|
||||
materialType = MaterialType.PHOTO,
|
||||
messagesUseCase = get(),
|
||||
savedStateHandle = get()
|
||||
)
|
||||
}
|
||||
viewModel(named(MaterialType.AUDIO)) {
|
||||
ChatMaterialsViewModelImpl(
|
||||
materialType = MaterialType.AUDIO,
|
||||
messagesUseCase = get(),
|
||||
savedStateHandle = get()
|
||||
)
|
||||
}
|
||||
viewModel(named(MaterialType.VIDEO)) {
|
||||
ChatMaterialsViewModelImpl(
|
||||
materialType = MaterialType.VIDEO,
|
||||
messagesUseCase = get(),
|
||||
savedStateHandle = get()
|
||||
)
|
||||
}
|
||||
viewModel(named(MaterialType.FILE)) {
|
||||
ChatMaterialsViewModelImpl(
|
||||
materialType = MaterialType.FILE,
|
||||
messagesUseCase = get(),
|
||||
savedStateHandle = get()
|
||||
)
|
||||
}
|
||||
viewModel(named(MaterialType.LINK)) {
|
||||
ChatMaterialsViewModelImpl(
|
||||
materialType = MaterialType.LINK,
|
||||
messagesUseCase = get(),
|
||||
savedStateHandle = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package dev.meloda.fast.chatmaterials.model
|
||||
|
||||
enum class MaterialType {
|
||||
PHOTO, VIDEO, AUDIO, FILE, LINK;
|
||||
|
||||
override fun toString(): String = when (this) {
|
||||
PHOTO -> "photo"
|
||||
VIDEO -> "video"
|
||||
AUDIO -> "audio"
|
||||
FILE -> "doc"
|
||||
LINK -> "link"
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -7,7 +7,10 @@ sealed class UiChatMaterial {
|
||||
) : UiChatMaterial()
|
||||
|
||||
data class Video(
|
||||
val previewUrl: String
|
||||
val previewUrl: String?,
|
||||
val title: String,
|
||||
val views: Int,
|
||||
val duration: String
|
||||
) : UiChatMaterial()
|
||||
|
||||
data class Audio(
|
||||
@@ -18,11 +21,16 @@ sealed class UiChatMaterial {
|
||||
) : UiChatMaterial()
|
||||
|
||||
data class File(
|
||||
val title: String
|
||||
val previewUrl: String?,
|
||||
val title: String,
|
||||
val size: String,
|
||||
val extension: String
|
||||
) : UiChatMaterial()
|
||||
|
||||
data class Link(
|
||||
val title: String,
|
||||
val previewUrl: String?
|
||||
val previewUrl: String?,
|
||||
val title: String?,
|
||||
val url: String,
|
||||
val urlFirstChar: String
|
||||
) : UiChatMaterial()
|
||||
}
|
||||
|
||||
+171
-236
@@ -1,93 +1,69 @@
|
||||
package dev.meloda.fast.chatmaterials.presentation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
|
||||
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.chatmaterials.model.MaterialType
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.AudioMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.FileMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.LinkMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.PhotoMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.presentation.materials.VideoMaterialsScreen
|
||||
import dev.meloda.fast.ui.model.TabItem
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun ChatMaterialsRoute(
|
||||
onBack: () -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
ChatMaterialsScreen(
|
||||
screenState = screenState,
|
||||
onBack = onBack,
|
||||
onTypeChanged = viewModel::onTypeChanged,
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
)
|
||||
}
|
||||
@@ -99,55 +75,36 @@ fun ChatMaterialsRoute(
|
||||
)
|
||||
@Composable
|
||||
fun ChatMaterialsScreen(
|
||||
screenState: ChatMaterialsScreenState = ChatMaterialsScreenState.EMPTY,
|
||||
onBack: () -> Unit = {},
|
||||
onTypeChanged: (String) -> Unit = {},
|
||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {}
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
|
||||
val attachments = screenState.materials
|
||||
|
||||
var moreClearBlur by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val hazeState = remember { HazeState() }
|
||||
val hazeStyle = if (moreClearBlur) HazeMaterials.ultraThin() else HazeMaterials.regular()
|
||||
|
||||
var dropDownMenuExpanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var checkedTypeIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
LaunchedEffect(checkedTypeIndex) {
|
||||
onTypeChanged(
|
||||
when (checkedTypeIndex) {
|
||||
0 -> "photo"
|
||||
1 -> "video"
|
||||
2 -> "audio"
|
||||
3 -> "doc"
|
||||
4 -> "link"
|
||||
else -> ""
|
||||
}
|
||||
val titles = remember {
|
||||
listOf(
|
||||
UiR.string.chat_attachment_photos,
|
||||
UiR.string.chat_attachment_videos,
|
||||
UiR.string.chat_attachment_music,
|
||||
UiR.string.chat_attachment_files,
|
||||
UiR.string.chat_attachment_links,
|
||||
)
|
||||
}
|
||||
|
||||
val titles = listOf("Photos", "Videos", "Audios")//, "Files", "Links")
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
val canScrollBackward = when (checkedTypeIndex) {
|
||||
in 0..1 -> gridState.canScrollBackward
|
||||
else -> listState.canScrollBackward
|
||||
val tabItems = remember {
|
||||
titles.map { resId ->
|
||||
TabItem(
|
||||
titleResId = resId,
|
||||
unselectedIconResId = null,
|
||||
selectedIconResId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("ChatMaterialsScreen", "ChatMaterialsScreen: canScrollBackward: $canScrollBackward")
|
||||
var canScrollBackward by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
val topBarContainerColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
|
||||
@@ -160,10 +117,8 @@ fun ChatMaterialsScreen(
|
||||
|
||||
val topBarContainerColor by animateColorAsState(
|
||||
targetValue =
|
||||
if (currentTheme.enableBlur || !canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
if (currentTheme.enableBlur || !canScrollBackward) MaterialTheme.colorScheme.surface
|
||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
@@ -171,7 +126,13 @@ fun ChatMaterialsScreen(
|
||||
)
|
||||
)
|
||||
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
val pagerState = rememberPagerState(
|
||||
pageCount = tabItems::size
|
||||
)
|
||||
|
||||
val selectedTabIndex by remember {
|
||||
derivedStateOf { pagerState.currentPage }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -181,11 +142,9 @@ fun ChatMaterialsScreen(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = hazeStyle
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
|
||||
.fillMaxWidth()
|
||||
@@ -193,7 +152,7 @@ fun ChatMaterialsScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Chat Materials",
|
||||
text = stringResource(UiR.string.chat_materials_title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
@@ -210,163 +169,139 @@ fun ChatMaterialsScreen(
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
dropDownMenuExpanded = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = "Options button"
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||
expanded = dropDownMenuExpanded,
|
||||
onDismissRequest = {
|
||||
dropDownMenuExpanded = false
|
||||
},
|
||||
offset = DpOffset(x = (-4).dp, y = (-60).dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onRefreshDropdownItemClicked()
|
||||
dropDownMenuExpanded = false
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.action_refresh))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Refresh,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (currentTheme.enableBlur) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = if (moreClearBlur) "Default blur" else "Clearer blur")
|
||||
},
|
||||
onClick = {
|
||||
moreClearBlur = !moreClearBlur
|
||||
dropDownMenuExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
titles.forEachIndexed { index, title ->
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
RadioButton(
|
||||
selected = checkedTypeIndex == index,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = title)
|
||||
},
|
||||
onClick = {
|
||||
checkedTypeIndex = index
|
||||
dropDownMenuExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
PrimaryTabRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
item.titleResId?.let { resId ->
|
||||
Text(text = stringResource(id = resId))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
if (checkedTypeIndex in listOf(0, 1)) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
state = gridState,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) { index ->
|
||||
when (index) {
|
||||
0 -> {
|
||||
val viewModel: ChatMaterialsViewModel =
|
||||
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.PHOTO))
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
) {
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
}
|
||||
items(attachments) { item ->
|
||||
ChatMaterialItem(
|
||||
item = item,
|
||||
onClick = {
|
||||
if (item is UiChatMaterial.Photo) {
|
||||
onPhotoClicked(item.previewUrl)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
PhotoMaterialsScreen(
|
||||
modifier = Modifier,
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
padding = padding,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onSessionExpiredLogOutButtonClicked = { },
|
||||
setCanScrollBackward = { canScrollBackward = it },
|
||||
canPaginate = canPaginate,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
items(attachments) { item ->
|
||||
ChatMaterialItem(
|
||||
item = item,
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
1 -> {
|
||||
val viewModel: ChatMaterialsViewModel =
|
||||
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.VIDEO))
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
VideoMaterialsScreen(
|
||||
modifier = Modifier,
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
padding = padding,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onSessionExpiredLogOutButtonClicked = { },
|
||||
setCanScrollBackward = { canScrollBackward = it },
|
||||
canPaginate = canPaginate,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
|
||||
)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val viewModel: ChatMaterialsViewModel =
|
||||
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.AUDIO))
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
AudioMaterialsScreen(
|
||||
modifier = Modifier,
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
padding = padding,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onSessionExpiredLogOutButtonClicked = { },
|
||||
setCanScrollBackward = { canScrollBackward = it },
|
||||
canPaginate = canPaginate,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
|
||||
)
|
||||
}
|
||||
|
||||
3 -> {
|
||||
val viewModel: ChatMaterialsViewModel =
|
||||
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.FILE))
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
FileMaterialsScreen(
|
||||
modifier = Modifier,
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
padding = padding,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onSessionExpiredLogOutButtonClicked = { },
|
||||
setCanScrollBackward = { canScrollBackward = it },
|
||||
canPaginate = canPaginate,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
|
||||
)
|
||||
}
|
||||
|
||||
4 -> {
|
||||
val viewModel: ChatMaterialsViewModel =
|
||||
koinViewModel<ChatMaterialsViewModelImpl>(named(MaterialType.LINK))
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
LinkMaterialsScreen(
|
||||
modifier = Modifier,
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
padding = padding,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onSessionExpiredLogOutButtonClicked = { },
|
||||
setCanScrollBackward = { canScrollBackward = it },
|
||||
canPaginate = canPaginate,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet
|
||||
)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package dev.meloda.fast.chatmaterials.presentation.materials
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AudioMaterialsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
canPaginate: Boolean,
|
||||
screenState: ChatMaterialsScreenState,
|
||||
baseError: BaseError?,
|
||||
padding: PaddingValues,
|
||||
onRefresh: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.canScrollBackward }
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
val paginationConditionMet by remember(canPaginate, listState) {
|
||||
derivedStateOf {
|
||||
canPaginate &&
|
||||
(listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
|
||||
?: -9) >= (listState.layoutInfo.totalItemsCount - 6)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(paginationConditionMet) {
|
||||
if (paginationConditionMet && !screenState.isPaginating) {
|
||||
onPaginationConditionsMet()
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(UiR.string.session_expired),
|
||||
buttonText = stringResource(UiR.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(UiR.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
items(screenState.materials) { item ->
|
||||
item as UiChatMaterial.Audio
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.size(42.dp)
|
||||
.padding(4.dp),
|
||||
painter = painterResource(UiR.drawable.round_play_arrow_24),
|
||||
contentDescription = null,
|
||||
tint = contentColorFor(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||
Text(
|
||||
text = item.artist,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(text = item.duration)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
if (screenState.isPaginationExhausted) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
listState.scrollToItem(14)
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
colors = IconButtonDefaults.filledIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.KeyboardArrowUp,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.materials.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(R.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package dev.meloda.fast.chatmaterials.presentation.materials
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.imageLoader
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FileMaterialsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
canPaginate: Boolean,
|
||||
screenState: ChatMaterialsScreenState,
|
||||
baseError: BaseError?,
|
||||
padding: PaddingValues,
|
||||
onRefresh: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.canScrollBackward }
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(R.string.session_expired),
|
||||
buttonText = stringResource(R.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(R.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
items(screenState.materials) { item ->
|
||||
item as UiChatMaterial.File
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
var errorLoading by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (item.previewUrl != null && !errorLoading) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.size(width = 64.dp, height = 48.dp),
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = item.previewUrl,
|
||||
imageLoader = LocalContext.current.imageLoader,
|
||||
onState = {
|
||||
errorLoading = it is AsyncImagePainter.State.Error
|
||||
}
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(
|
||||
MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
)
|
||||
.size(width = 64.dp, height = 48.dp)
|
||||
.padding(4.dp),
|
||||
text = item.extension.uppercase(),
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 40.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||
Text(
|
||||
text = item.size,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.materials.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(R.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package dev.meloda.fast.chatmaterials.presentation.materials
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.imageLoader
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LinkMaterialsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
canPaginate: Boolean,
|
||||
screenState: ChatMaterialsScreenState,
|
||||
baseError: BaseError?,
|
||||
padding: PaddingValues,
|
||||
onRefresh: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.canScrollBackward }
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(R.string.session_expired),
|
||||
buttonText = stringResource(R.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(R.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
items(screenState.materials) { item ->
|
||||
item as UiChatMaterial.Link
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 72.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
var errorLoading by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (item.previewUrl != null && !errorLoading) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.size(
|
||||
width = 86.dp,
|
||||
height = 64.dp
|
||||
),
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = item.previewUrl,
|
||||
imageLoader = LocalContext.current.imageLoader,
|
||||
onState = {
|
||||
errorLoading = it is AsyncImagePainter.State.Error
|
||||
}
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(
|
||||
MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(3.dp)
|
||||
)
|
||||
.size(
|
||||
width = 86.dp,
|
||||
height = 64.dp
|
||||
)
|
||||
.padding(4.dp),
|
||||
text = item.urlFirstChar,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 56.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (item.title != null) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
}
|
||||
|
||||
LocalContentAlpha(
|
||||
alpha = if (item.title != null) ContentAlpha.medium
|
||||
else ContentAlpha.high
|
||||
) {
|
||||
Text(
|
||||
text = item.url,
|
||||
style = if (item.title != null) {
|
||||
MaterialTheme.typography.bodyMedium
|
||||
} else {
|
||||
MaterialTheme.typography.bodyLarge
|
||||
},
|
||||
maxLines = if (item.title != null) 1 else 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.materials.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(R.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package dev.meloda.fast.chatmaterials.presentation.materials
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PhotoMaterialsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
canPaginate: Boolean,
|
||||
screenState: ChatMaterialsScreenState,
|
||||
baseError: BaseError?,
|
||||
padding: PaddingValues,
|
||||
onRefresh: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPhotoClicked: (String) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val gridState = rememberLazyGridState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
LaunchedEffect(gridState) {
|
||||
snapshotFlow { gridState.canScrollBackward }
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(R.string.session_expired),
|
||||
buttonText = stringResource(R.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(R.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
state = gridState,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
}
|
||||
items(items = screenState.materials) { item ->
|
||||
item as UiChatMaterial.Photo
|
||||
AsyncImage(
|
||||
model = item.previewUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clickable(
|
||||
onClick = {
|
||||
onPhotoClicked(item.previewUrl)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.materials.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(R.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package dev.meloda.fast.chatmaterials.presentation.materials
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.imageLoader
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VideoMaterialsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
canPaginate: Boolean,
|
||||
screenState: ChatMaterialsScreenState,
|
||||
baseError: BaseError?,
|
||||
padding: PaddingValues,
|
||||
onRefresh: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
setCanScrollBackward: (Boolean) -> Unit,
|
||||
onPaginationConditionsMet: () -> Unit
|
||||
) {
|
||||
val hazeState = LocalHazeState.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val listState = rememberLazyListState()
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.canScrollBackward }
|
||||
.collect(setCanScrollBackward)
|
||||
}
|
||||
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(R.string.session_expired),
|
||||
buttonText = stringResource(R.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(R.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
PullToRefreshBox(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
|
||||
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr)),
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
onRefresh = onRefresh,
|
||||
indicator = {
|
||||
PullToRefreshDefaults.Indicator(
|
||||
state = pullToRefreshState,
|
||||
isRefreshing = screenState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = padding.calculateTopPadding()),
|
||||
)
|
||||
}
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.fillMaxSize()
|
||||
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
|
||||
}
|
||||
items(screenState.materials) { item ->
|
||||
item as UiChatMaterial.Video
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 72.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.33f)
|
||||
.height(64.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = item.previewUrl,
|
||||
imageLoader = LocalContext.current.imageLoader
|
||||
),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(
|
||||
end = 4.dp,
|
||||
bottom = 4.dp
|
||||
)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)
|
||||
)
|
||||
.padding(
|
||||
vertical = 1.dp,
|
||||
horizontal = 4.dp
|
||||
),
|
||||
text = item.duration,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.background
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
LocalContentAlpha(alpha = ContentAlpha.medium) {
|
||||
Text(
|
||||
text = "${item.views} views",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
|
||||
if (screenState.materials.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(R.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
-9
@@ -1,6 +1,7 @@
|
||||
package dev.meloda.fast.chatmaterials.util
|
||||
|
||||
import dev.meloda.fast.chatmaterials.model.UiChatMaterial
|
||||
import dev.meloda.fast.common.util.AndroidUtils
|
||||
import dev.meloda.fast.model.api.data.AttachmentType
|
||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
import dev.meloda.fast.model.api.domain.VkAudioDomain
|
||||
@@ -8,7 +9,6 @@ import dev.meloda.fast.model.api.domain.VkFileDomain
|
||||
import dev.meloda.fast.model.api.domain.VkLinkDomain
|
||||
import dev.meloda.fast.model.api.domain.VkPhotoDomain
|
||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
@@ -22,36 +22,110 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
|
||||
|
||||
AttachmentType.VIDEO -> {
|
||||
val attachment = this.attachment as VkVideoDomain
|
||||
|
||||
val duration = attachment.duration
|
||||
|
||||
val days = duration / (24 * 3600)
|
||||
val hours = (duration % (24 * 3600)) / 3600
|
||||
val minutes = (duration % 3600) / 60
|
||||
val seconds = duration % 60
|
||||
|
||||
val args = mutableListOf<Int>()
|
||||
if (days > 0) args.add(days)
|
||||
if (hours > 0) args.add(hours)
|
||||
args.add(minutes)
|
||||
args.add(seconds)
|
||||
|
||||
val builder = StringBuilder()
|
||||
if (days > 0) builder.append("%02d:")
|
||||
if (hours > 0) builder.append("%02d:")
|
||||
builder.append("%02d:%02d")
|
||||
|
||||
val formattedDuration =
|
||||
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
|
||||
|
||||
UiChatMaterial.Video(
|
||||
previewUrl = attachment.images.firstOrNull()?.url.orEmpty()
|
||||
previewUrl = attachment.images.maxByOrNull(VkVideoDomain.VideoImage::width)?.url.orEmpty(),
|
||||
title = attachment.title,
|
||||
views = attachment.views,
|
||||
duration = formattedDuration
|
||||
)
|
||||
}
|
||||
|
||||
AttachmentType.AUDIO -> {
|
||||
val attachment = this.attachment as VkAudioDomain
|
||||
|
||||
val duration = attachment.duration
|
||||
|
||||
val days = duration / (24 * 3600)
|
||||
val hours = (duration % (24 * 3600)) / 3600
|
||||
val minutes = (duration % 3600) / 60
|
||||
val seconds = duration % 60
|
||||
|
||||
val args = mutableListOf<Int>()
|
||||
if (days > 0) args.add(days)
|
||||
if (hours > 0) args.add(hours)
|
||||
args.add(minutes)
|
||||
args.add(seconds)
|
||||
|
||||
val builder = StringBuilder()
|
||||
if (days > 0) builder.append("%02d:")
|
||||
if (hours > 0) builder.append("%02d:")
|
||||
builder.append("%d:%02d")
|
||||
|
||||
val formattedDuration =
|
||||
builder.toString().format(Locale.getDefault(), *args.toTypedArray())
|
||||
|
||||
UiChatMaterial.Audio(
|
||||
previewUrl = null,
|
||||
title = attachment.title,
|
||||
artist = attachment.artist,
|
||||
duration = SimpleDateFormat(
|
||||
"mm:ss",
|
||||
Locale.getDefault()
|
||||
).format(attachment.duration)
|
||||
duration = formattedDuration
|
||||
)
|
||||
}
|
||||
|
||||
AttachmentType.FILE -> {
|
||||
val attachment = this.attachment as VkFileDomain
|
||||
|
||||
val previewUrl: String? = when (val preview = attachment.preview) {
|
||||
null -> null
|
||||
|
||||
else -> {
|
||||
when {
|
||||
preview.photo != null -> {
|
||||
val size = preview.photo?.sizes?.maxByOrNull { it.width }
|
||||
size?.src
|
||||
}
|
||||
|
||||
preview.video != null -> {
|
||||
val size = preview.video?.src
|
||||
size
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UiChatMaterial.File(
|
||||
title = attachment.title
|
||||
title = attachment.title,
|
||||
previewUrl = previewUrl,
|
||||
size = AndroidUtils.bytesToHumanReadableSize(attachment.size.toDouble()),
|
||||
extension = attachment.ext.take(4)
|
||||
)
|
||||
}
|
||||
|
||||
AttachmentType.LINK -> {
|
||||
val attachment = this.attachment as VkLinkDomain
|
||||
|
||||
UiChatMaterial.Link(
|
||||
title = attachment.title ?: attachment.url,
|
||||
previewUrl = attachment.photo?.getMaxSize()?.url
|
||||
title = attachment.title,
|
||||
previewUrl = attachment.photo?.getMaxSize()?.url,
|
||||
url = attachment.url,
|
||||
urlFirstChar = attachment.url.replaceFirst("http://", "")
|
||||
.replaceFirst("https://", "")
|
||||
.take(1)
|
||||
.uppercase()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ data class ConversationsScreenState(
|
||||
val isPaginationExhausted: Boolean,
|
||||
val profileImageUrl: String?,
|
||||
val scrollIndex: Int,
|
||||
val scrollOffset: Int
|
||||
val scrollOffset: Int,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
-1
@@ -137,7 +137,6 @@ fun ConversationsScreen(
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
|
||||
val maxLines by remember(currentTheme) {
|
||||
|
||||
+16
-21
@@ -1,6 +1,7 @@
|
||||
package dev.meloda.fast.conversations.presentation
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -79,8 +80,6 @@ fun CreateChatRoute(
|
||||
onChatCreated: (Int) -> Unit,
|
||||
viewModel: CreateChatViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
@@ -148,20 +147,24 @@ fun CreateChatScreen(
|
||||
|
||||
val hazeState = LocalHazeState.current
|
||||
|
||||
val toolbarColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!listState.canScrollBackward) 1f else 0f,
|
||||
val topBarContainerColorAlpha by animateFloatAsState(
|
||||
targetValue = if (!currentTheme.enableBlur || !listState.canScrollBackward) 1f else 0f,
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
val toolbarContainerColor by animateColorAsState(
|
||||
val topBarContainerColor by animateColorAsState(
|
||||
targetValue =
|
||||
if (currentTheme.enableBlur || !listState.canScrollBackward)
|
||||
MaterialTheme.colorScheme.surface
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
if (currentTheme.enableBlur || !listState.canScrollBackward) MaterialTheme.colorScheme.surface
|
||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||
label = "toolbarColorAlpha",
|
||||
animationSpec = tween(durationMillis = 50)
|
||||
animationSpec = tween(
|
||||
durationMillis = 200,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
@@ -171,11 +174,7 @@ fun CreateChatScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
toolbarContainerColor.copy(
|
||||
alpha = if (currentTheme.enableBlur) toolbarColorAlpha else 1f
|
||||
)
|
||||
)
|
||||
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeEffect(
|
||||
@@ -205,11 +204,7 @@ fun CreateChatScreen(
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = toolbarContainerColor.copy(
|
||||
alpha = 0f
|
||||
)
|
||||
),
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
|
||||
+32
-36
@@ -24,14 +24,13 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -50,7 +49,7 @@ import dev.meloda.fast.ui.model.TabItem
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||
@@ -60,10 +59,7 @@ fun FriendsRoute(
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
) {
|
||||
var selectedTabIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val hazeState = LocalHazeState.current
|
||||
|
||||
@@ -107,20 +103,36 @@ fun FriendsRoute(
|
||||
)
|
||||
}
|
||||
|
||||
val pagerState = rememberPagerState(pageCount = tabItems::size)
|
||||
|
||||
val selectedTabIndex by remember {
|
||||
derivedStateOf { pagerState.currentPage }
|
||||
}
|
||||
|
||||
var orderType: String by remember { mutableStateOf("hints") }
|
||||
|
||||
var showOrderDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val orderItems = remember {
|
||||
mapOf(
|
||||
"hints" to "Priority",
|
||||
"name" to "Name",
|
||||
"random" to "Random",
|
||||
"mobile" to "Mobile",
|
||||
"smart" to "Smart"
|
||||
val orderPriority = stringResource(UiR.string.friends_order_priority)
|
||||
val orderName = stringResource(UiR.string.friends_order_name)
|
||||
val orderRandom = stringResource(UiR.string.friends_order_random)
|
||||
val orderMobile = stringResource(UiR.string.friends_order_mobile)
|
||||
val orderSmart = stringResource(UiR.string.friends_order_smart)
|
||||
|
||||
val orderTitleItems = remember {
|
||||
ImmutableList.of(
|
||||
orderPriority,
|
||||
orderName,
|
||||
orderRandom,
|
||||
orderMobile,
|
||||
orderSmart
|
||||
)
|
||||
}
|
||||
|
||||
val orderItems = remember {
|
||||
listOf("hints", "name", "random", "mobile", "smart")
|
||||
}
|
||||
|
||||
var selectedIndex by remember {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
@@ -130,17 +142,16 @@ fun FriendsRoute(
|
||||
onDismissRequest = { showOrderDialog = false },
|
||||
confirmText = stringResource(R.string.ok),
|
||||
confirmAction = {
|
||||
orderType =
|
||||
orderItems.keys.toCollection(mutableListOf())[selectedIndex]
|
||||
orderType = orderItems[selectedIndex]
|
||||
},
|
||||
cancelText = stringResource(R.string.cancel),
|
||||
selectionType = SelectionType.Single,
|
||||
items = ImmutableList.copyOf(orderItems.values),
|
||||
items = orderTitleItems,
|
||||
preSelectedItems = ImmutableList.of(selectedIndex),
|
||||
onItemClick = {
|
||||
selectedIndex = it
|
||||
},
|
||||
title = "Order type",
|
||||
title = stringResource(UiR.string.friends_order_by_title),
|
||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||
)
|
||||
}
|
||||
@@ -199,8 +210,8 @@ fun FriendsRoute(
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onClick = {
|
||||
if (selectedTabIndex != index) {
|
||||
selectedTabIndex = index
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
@@ -214,21 +225,6 @@ fun FriendsRoute(
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = selectedTabIndex
|
||||
) {
|
||||
tabItems.size
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedTabIndex) {
|
||||
pagerState.animateScrollToPage(selectedTabIndex)
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.currentPage }
|
||||
.collect { selectedTabIndex = it }
|
||||
}
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
+12
-4
@@ -229,8 +229,7 @@ fun MessagesHistoryScreen(
|
||||
.fillMaxWidth(),
|
||||
title = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (selectedMessages.isEmpty()) {
|
||||
@@ -332,6 +331,9 @@ fun MessagesHistoryScreen(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (screenState.isLoading) {
|
||||
return@TopAppBar
|
||||
}
|
||||
IconButton(
|
||||
onClick = { dropDownMenuExpanded = true }
|
||||
) {
|
||||
@@ -362,7 +364,13 @@ fun MessagesHistoryScreen(
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(text = "Materials")
|
||||
Text(text = stringResource(UiR.string.chat_materials_action_title))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(UiR.drawable.ic_multimedia),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
@@ -371,7 +379,7 @@ fun MessagesHistoryScreen(
|
||||
dropDownMenuExpanded = false
|
||||
},
|
||||
text = {
|
||||
Text(text = "Refresh")
|
||||
Text(text = stringResource(UiR.string.action_refresh))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
|
||||
Reference in New Issue
Block a user