some improvements, new feature

This commit is contained in:
2025-08-27 05:17:40 +03:00
parent 4677e484d9
commit 8cb3ed8784
8 changed files with 114 additions and 64 deletions
@@ -1,5 +1,7 @@
package dev.meloda.fast.common.extensions package dev.meloda.fast.common.extensions
import android.os.Build
import android.os.Bundle
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -11,6 +13,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlin.reflect.KClass
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -103,7 +106,7 @@ fun Any.asInt(): Int {
} }
fun Any.asLong(): Long { fun Any.asLong(): Long {
return when(this) { return when (this) {
is Number -> this.toLong() is Number -> this.toLong()
else -> throw IllegalArgumentException("Object is not numeric") else -> throw IllegalArgumentException("Object is not numeric")
@@ -117,3 +120,19 @@ fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
else -> emptyList() else -> emptyList()
} }
} }
fun <T> Bundle.getParcelableCompat(key: String, clazz: Class<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, clazz)
} else {
getParcelable(key)
}
}
fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, clazz.java)
} else {
getParcelable(key)
}
}
@@ -103,6 +103,13 @@ object AppSettings {
) )
set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value) set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value)
var showManualRefreshOptions: Boolean
get() = get(
SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS,
SettingsKeys.DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS
)
set(value) = put(SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS, value)
var enableHaptic: Boolean var enableHaptic: Boolean
get() = get( get() = get(
SettingsKeys.KEY_ENABLE_HAPTIC, SettingsKeys.KEY_ENABLE_HAPTIC,
@@ -13,8 +13,8 @@ object SettingsKeys {
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button" const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button"
const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false
const val KEY_SHOW_RECORD_VOICE_BUTTON = "show_record_voice_button" const val KEY_SHOW_MANUAL_REFRESH_OPTIONS = "show_manual_refresh_options"
const val DEFAULT_VALUE_SHOW_RECORD_VOICE_BUTTON = false const val DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS = false
const val KEY_APPEARANCE = "appearance" const val KEY_APPEARANCE = "appearance"
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
@@ -63,7 +63,9 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.conversations.model.ConversationsScreenState
import dev.meloda.fast.conversations.navigation.ConversationsGraph import dev.meloda.fast.conversations.navigation.ConversationsGraph
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.FullScreenContainedLoader
import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.components.VkErrorView
@@ -78,7 +80,6 @@ import dev.meloda.fast.ui.util.emptyImmutableList
import dev.meloda.fast.ui.util.isScrollingUp import dev.meloda.fast.ui.util.isScrollingUp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import dev.meloda.fast.ui.R
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@@ -215,11 +216,35 @@ fun ConversationsScreen(
} }
} }
IconButton(onClick = { dropDownMenuExpanded = true }) { val dropDownItems = mutableListOf<@Composable () -> Unit>()
Icon(
imageVector = Icons.Rounded.MoreVert, if (AppSettings.General.showManualRefreshOptions) {
contentDescription = null dropDownItems += {
) DropdownMenuItem(
onClick = {
onRefreshDropdownItemClicked()
dropDownMenuExpanded = false
},
text = {
Text(text = stringResource(id = R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
}
}
if (dropDownItems.isNotEmpty()) {
IconButton(onClick = { dropDownMenuExpanded = true }) {
Icon(
imageVector = Icons.Rounded.MoreVert,
contentDescription = null
)
}
} }
DropdownMenu( DropdownMenu(
@@ -228,21 +253,7 @@ fun ConversationsScreen(
onDismissRequest = { dropDownMenuExpanded = false }, onDismissRequest = { dropDownMenuExpanded = false },
offset = DpOffset(x = (-4).dp, y = (-60).dp) offset = DpOffset(x = (-4).dp, y = (-60).dp)
) { ) {
DropdownMenuItem( dropDownItems.forEach { it.invoke() }
onClick = {
onRefreshDropdownItemClicked()
dropDownMenuExpanded = false
},
text = {
Text(text = stringResource(id = R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
@@ -31,6 +31,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -69,7 +70,7 @@ fun FriendsRoute(
val currentTheme = LocalThemeConfig.current val currentTheme = LocalThemeConfig.current
val hazeState = LocalHazeState.current val hazeState = LocalHazeState.current
var canScrollBackward by remember { var canScrollBackward by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
} }
@@ -115,35 +116,30 @@ fun FriendsRoute(
derivedStateOf { pagerState.currentPage } derivedStateOf { pagerState.currentPage }
} }
var orderType: String by remember { mutableStateOf("hints") } var orderType: String by rememberSaveable { mutableStateOf("hints") }
var showOrderDialog by rememberSaveable { mutableStateOf(false) }
var showOrderDialog by remember { mutableStateOf(false) }
val orderPriority = stringResource(R.string.friends_order_priority) val orderPriority = stringResource(R.string.friends_order_priority)
val orderName = stringResource(R.string.friends_order_name) val orderName = stringResource(R.string.friends_order_name)
val orderRandom = stringResource(R.string.friends_order_random) val orderRandom = stringResource(R.string.friends_order_random)
val orderMobile = stringResource(R.string.friends_order_mobile)
val orderSmart = stringResource(R.string.friends_order_smart)
val orderTitleItems = remember { val orderTitleItems = remember {
ImmutableList.of( ImmutableList.of(
orderPriority, orderPriority,
orderName, orderName,
orderRandom, orderRandom,
orderMobile,
orderSmart
) )
} }
val orderItems = remember { val orderItems = remember {
listOf("hints", "name", "random", "mobile", "smart") listOf("hints", "name", "random")
}
var selectedIndex by remember {
mutableIntStateOf(0)
} }
if (showOrderDialog) { if (showOrderDialog) {
var selectedIndex by remember {
mutableIntStateOf(orderItems.indexOf(orderType))
}
MaterialDialog( MaterialDialog(
onDismissRequest = { showOrderDialog = false }, onDismissRequest = { showOrderDialog = false },
confirmText = stringResource(R.string.ok), confirmText = stringResource(R.string.ok),
@@ -25,6 +25,7 @@ import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import com.conena.nanokt.collections.indexOfFirstOrNull import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.getParcelableCompat
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.orDots import dev.meloda.fast.common.extensions.orDots
import dev.meloda.fast.common.extensions.removeIfCompat import dev.meloda.fast.common.extensions.removeIfCompat
@@ -57,6 +58,7 @@ import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkPhotoDomain import dev.meloda.fast.model.api.domain.VkPhotoDomain
import dev.meloda.fast.network.VkErrorCode import dev.meloda.fast.network.VkErrorCode
import dev.meloda.fast.ui.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -67,7 +69,6 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.math.abs import kotlin.math.abs
import kotlin.random.Random import kotlin.random.Random
import dev.meloda.fast.ui.R
interface MessagesHistoryViewModel { interface MessagesHistoryViewModel {
@@ -267,7 +268,7 @@ class MessagesHistoryViewModelImpl(
override fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) { override fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) {
when (dialog) { when (dialog) {
is MessageDialog.MessageOptions -> { is MessageDialog.MessageOptions -> {
when (val option = bundle.getParcelable<MessageOption>("option")) { when (val option = bundle.getParcelableCompat("option", MessageOption::class)) {
null -> Unit null -> Unit
MessageOption.Retry -> { MessageOption.Retry -> {
@@ -51,11 +51,10 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.getImage import dev.meloda.fast.ui.util.getImage
import dev.meloda.fast.ui.R
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@Composable @Composable
fun MessagesHistoryTopBar( fun MessagesHistoryTopBar(
@@ -221,13 +220,37 @@ fun MessagesHistoryTopBar(
) )
} }
} else { } else {
IconButton( val dropDownItems = mutableListOf<@Composable () -> Unit>()
onClick = { dropDownMenuExpanded = true }
) { if (AppSettings.General.showManualRefreshOptions) {
Icon( dropDownItems += {
imageVector = Icons.Outlined.MoreVert, DropdownMenuItem(
contentDescription = "Options" onClick = {
) onRefresh()
dropDownMenuExpanded = false
},
text = {
Text(text = stringResource(R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
}
}
if (dropDownItems.isNotEmpty()) {
IconButton(
onClick = { dropDownMenuExpanded = true }
) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = "Options"
)
}
} }
DropdownMenu( DropdownMenu(
@@ -238,21 +261,7 @@ fun MessagesHistoryTopBar(
}, },
offset = DpOffset(x = (-4).dp, y = (-60).dp) offset = DpOffset(x = (-4).dp, y = (-60).dp)
) { ) {
DropdownMenuItem( dropDownItems.forEach { it.invoke() }
onClick = {
onRefresh()
dropDownMenuExpanded = false
},
text = {
Text(text = stringResource(R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
} }
} }
} }
@@ -30,6 +30,7 @@ import dev.meloda.fast.settings.model.SettingsDialog
import dev.meloda.fast.settings.model.SettingsItem import dev.meloda.fast.settings.model.SettingsItem
import dev.meloda.fast.settings.model.SettingsScreenState import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.TextProvider import dev.meloda.fast.settings.model.TextProvider
import dev.meloda.fast.ui.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@@ -37,7 +38,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import dev.meloda.fast.ui.R
class SettingsViewModel( class SettingsViewModel(
private val loadUserByIdUseCase: LoadUserByIdUseCase, private val loadUserByIdUseCase: LoadUserByIdUseCase,
@@ -351,6 +351,12 @@ class SettingsViewModel(
text = UiText.Resource(R.string.settings_general_show_attachment_button_summary), text = UiText.Resource(R.string.settings_general_show_attachment_button_summary),
defaultValue = SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON defaultValue = SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON
) )
val generalShowManualRefreshOptions = SettingsItem.Switch(
key = SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS,
defaultValue = SettingsKeys.DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS,
title = UiText.Simple("Refresh options"),
text = UiText.Simple("Show manual refresh options in some screens")
)
val generalEnableHaptic = SettingsItem.Switch( val generalEnableHaptic = SettingsItem.Switch(
key = SettingsKeys.KEY_ENABLE_HAPTIC, key = SettingsKeys.KEY_ENABLE_HAPTIC,
defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC, defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC,
@@ -538,6 +544,7 @@ class SettingsViewModel(
generalUseContactNames, generalUseContactNames,
generalShowEmojiButton, generalShowEmojiButton,
generalShowAttachmentButton, generalShowAttachmentButton,
generalShowManualRefreshOptions,
generalEnableHaptic generalEnableHaptic
) )
val appearanceList = listOf( val appearanceList = listOf(