forked from melod1n/fast-messenger
reworked chat materials screen and some fixes
This commit is contained in:
@@ -1,75 +1,78 @@
|
||||
package dev.meloda.fast.model.api.data
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VkVideoData(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val width: Int?,
|
||||
val height: Int?,
|
||||
val duration: Int,
|
||||
val date: Int,
|
||||
val comments: Int?,
|
||||
val description: String?,
|
||||
val player: String?,
|
||||
val added: Int?,
|
||||
val type: String,
|
||||
val views: Int,
|
||||
val access_key: String?,
|
||||
val owner_id: Int,
|
||||
val is_favorite: Boolean?,
|
||||
val image: List<Image>?,
|
||||
val first_frame: List<FirstFrame>?,
|
||||
val files: File?
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "width") val width: Int?,
|
||||
@Json(name = "height") val height: Int?,
|
||||
@Json(name = "duration") val duration: Int,
|
||||
@Json(name = "date") val date: Int,
|
||||
@Json(name = "comments") val comments: Int?,
|
||||
@Json(name = "description") val description: String?,
|
||||
@Json(name = "player") val player: String?,
|
||||
@Json(name = "added") val added: Int?,
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "views") val views: Int,
|
||||
@Json(name = "access_key") val accessKey: String?,
|
||||
@Json(name = "owner_id") val ownerId: Int,
|
||||
@Json(name = "is_favorite") val isFavorite: Boolean?,
|
||||
@Json(name = "image") val image: List<Image>?,
|
||||
@Json(name = "first_frame") val firstFrame: List<FirstFrame>?,
|
||||
@Json(name = "files") val files: File?
|
||||
) : VkAttachmentData {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Image(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
val with_padding: Int?
|
||||
@Json(name = "width") val width: Int,
|
||||
@Json(name = "height") val height: Int,
|
||||
@Json(name = "url") val url: String,
|
||||
@Json(name = "with_padding") val withPadding: Int?
|
||||
) {
|
||||
|
||||
fun asVideoImage() = VkVideoDomain.VideoImage(
|
||||
width = width,
|
||||
height = height,
|
||||
url = url,
|
||||
withPadding = with_padding == 1
|
||||
withPadding = withPadding == 1
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FirstFrame(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val url: String
|
||||
@Json(name = "height") val height: Int,
|
||||
@Json(name = "width") val width: Int,
|
||||
@Json(name = "url") val url: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class File(
|
||||
val mp4_240: String?,
|
||||
val mp4_360: String?,
|
||||
val mp4_480: String?,
|
||||
val mp4_720: String?,
|
||||
val mp4_1080: String?,
|
||||
val mp4_1440: String?,
|
||||
val hls: String?,
|
||||
val dash_uni: String?,
|
||||
val dash_sep: String?,
|
||||
val hls_ondemand: String?,
|
||||
val dash_ondemand: String?,
|
||||
val failover_host: String?
|
||||
@Json(name = "mp4_240") val mp4240: String?,
|
||||
@Json(name = "mp4_360") val mp4360: String?,
|
||||
@Json(name = "mp4_480") val mp4480: String?,
|
||||
@Json(name = "mp4_720") val mp4720: String?,
|
||||
@Json(name = "mp4_1080") val mp41080: String?,
|
||||
@Json(name = "mp4_1440") val mp41440: String?,
|
||||
@Json(name = "hls") val hls: String?,
|
||||
@Json(name = "dash_uni") val dashUni: String?,
|
||||
@Json(name = "dash_sep") val dashSep: String?,
|
||||
@Json(name = "hls_ondemand") val hlsOnDemand: String?,
|
||||
@Json(name = "dash_ondemand") val dashOnDemand: String?,
|
||||
@Json(name = "failover_host") val failOverHost: String?
|
||||
)
|
||||
|
||||
fun toDomain() = VkVideoDomain(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
ownerId = ownerId,
|
||||
images = image.orEmpty().map { it.asVideoImage() },
|
||||
firstFrames = first_frame,
|
||||
accessKey = access_key,
|
||||
title = title
|
||||
firstFrames = firstFrame,
|
||||
accessKey = accessKey,
|
||||
title = title,
|
||||
views = views,
|
||||
duration = duration
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ data class VkVideoDomain(
|
||||
val firstFrames: List<VkVideoData.FirstFrame>?,
|
||||
val accessKey: String?,
|
||||
val title: String,
|
||||
val views: Int,
|
||||
val duration: Int
|
||||
) : VkAttachment {
|
||||
|
||||
override val type: AttachmentType = AttachmentType.VIDEO
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9 13V5C9 3.9 9.9 3 11 3H20C21.1 3 22 3.9 22 5V11H18.57L17.29 9.26C17.23 9.17 17.11 9.17 17.05 9.26L15.06 12C15 12.06 14.88 12.07 14.82 12L13.39 10.25C13.33 10.18 13.22 10.18 13.16 10.25L11.05 12.91C10.97 13 11.04 13.15 11.16 13.15H17.5V15H11C9.89 15 9 14.11 9 13M6 22V21H4V22H2V2H4V3H6V2H8.39C7.54 2.74 7 3.8 7 5V13C7 15.21 8.79 17 11 17H15.7C14.67 17.83 14 19.08 14 20.5C14 21.03 14.11 21.53 14.28 22H6M4 7H6V5H4V7M4 11H6V9H4V11M4 15H6V13H4V15M6 19V17H4V19H6M23 13V15H21V20.5C21 21.88 19.88 23 18.5 23S16 21.88 16 20.5 17.12 18 18.5 18C18.86 18 19.19 18.07 19.5 18.21V13H23Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
|
||||
|
||||
</vector>
|
||||
@@ -218,4 +218,15 @@
|
||||
<string name="title_create_chat">Создать чат</string>
|
||||
<string name="action_create">Создать</string>
|
||||
<string name="create_chat_title">Название</string>
|
||||
<string name="chat_materials_title">Вложения чата</string>
|
||||
<string name="chat_materials_action_title">Вложения</string>
|
||||
<string name="friends_order_priority">Приоритет</string>
|
||||
<string name="friends_order_name">Имя</string>
|
||||
<string name="friends_order_random">Случайно</string>
|
||||
<string name="friends_order_by_title">Упорядочить по</string>
|
||||
<string name="chat_attachment_photos">Фото</string>
|
||||
<string name="chat_attachment_videos">Видео</string>
|
||||
<string name="chat_attachment_music">Музыка</string>
|
||||
<string name="chat_attachment_files">Файлы</string>
|
||||
<string name="chat_attachment_links">Ссылки</string>
|
||||
</resources>
|
||||
|
||||
@@ -283,4 +283,17 @@
|
||||
<string name="title_create_chat">Create chat</string>
|
||||
<string name="action_create">Create</string>
|
||||
<string name="create_chat_title">Title</string>
|
||||
<string name="chat_materials_title">Chat materials</string>
|
||||
<string name="chat_materials_action_title">Materials</string>
|
||||
<string name="friends_order_priority">Priority</string>
|
||||
<string name="friends_order_name">Name</string>
|
||||
<string name="friends_order_random">Random</string>
|
||||
<string name="friends_order_mobile" translatable="false">Mobile</string>
|
||||
<string name="friends_order_smart" translatable="false">Smart</string>
|
||||
<string name="friends_order_by_title">Order by</string>
|
||||
<string name="chat_attachment_photos">Photos</string>
|
||||
<string name="chat_attachment_videos">Videos</string>
|
||||
<string name="chat_attachment_music">Music</string>
|
||||
<string name="chat_attachment_files">Files</string>
|
||||
<string name="chat_attachment_links">Links</string>
|
||||
</resources>
|
||||
|
||||
+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()
|
||||
}
|
||||
|
||||
+161
-226
@@ -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
|
||||
}
|
||||
)
|
||||
PrimaryTabRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
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(
|
||||
tabItems.forEachIndexed { index, item ->
|
||||
Tab(
|
||||
selected = index == selectedTabIndex,
|
||||
onClick = {
|
||||
onRefreshDropdownItemClicked()
|
||||
dropDownMenuExpanded = false
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
PhotoMaterialsScreen(
|
||||
modifier = Modifier,
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
padding = padding,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onSessionExpiredLogOutButtonClicked = { },
|
||||
setCanScrollBackward = { canScrollBackward = it },
|
||||
canPaginate = canPaginate,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
)
|
||||
}
|
||||
repeat(3) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 = {}
|
||||
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
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
|
||||
|
||||
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