mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate QuickChat to Compose (#1419)
This commit is contained in:
parent
4855576248
commit
475e9fc22c
7 changed files with 375 additions and 362 deletions
|
|
@ -1,38 +1,35 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class QuickChatActionRepository @Inject constructor(private val quickChatDaoLazy: dagger.Lazy<QuickChatActionDao>) {
|
||||
class QuickChatActionRepository @Inject constructor(
|
||||
private val quickChatDaoLazy: dagger.Lazy<QuickChatActionDao>,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val quickChatActionDao by lazy {
|
||||
quickChatDaoLazy.get()
|
||||
}
|
||||
|
||||
suspend fun getAllActions(): Flow<List<QuickChatAction>> = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.getAll()
|
||||
fun getAllActions() = quickChatActionDao.getAll().flowOn(dispatchers.io)
|
||||
|
||||
suspend fun upsert(action: QuickChatAction) = withContext(dispatchers.io) {
|
||||
quickChatActionDao.upsert(action)
|
||||
}
|
||||
|
||||
suspend fun insert(action: QuickChatAction) = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.insert(action)
|
||||
}
|
||||
|
||||
suspend fun deleteAll() = withContext(Dispatchers.IO) {
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) {
|
||||
quickChatActionDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun delete(action: QuickChatAction) = withContext(Dispatchers.IO) {
|
||||
suspend fun delete(action: QuickChatAction) = withContext(dispatchers.io) {
|
||||
quickChatActionDao.delete(action)
|
||||
}
|
||||
|
||||
suspend fun update(action: QuickChatAction) = withContext(Dispatchers.IO) {
|
||||
quickChatActionDao.update(action)
|
||||
}
|
||||
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) = withContext(Dispatchers.IO) {
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) = withContext(dispatchers.io) {
|
||||
quickChatActionDao.updateActionPosition(uuid, newPos)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.geeksville.mesh.database.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -10,8 +13,8 @@ interface QuickChatActionDao {
|
|||
@Query("Select * from quick_chat order by position asc")
|
||||
fun getAll(): Flow<List<QuickChatAction>>
|
||||
|
||||
@Insert
|
||||
fun insert(action: QuickChatAction)
|
||||
@Upsert
|
||||
fun upsert(action: QuickChatAction)
|
||||
|
||||
@Query("Delete from quick_chat")
|
||||
fun deleteAll()
|
||||
|
|
@ -25,13 +28,9 @@ interface QuickChatActionDao {
|
|||
decrementPositionsAfter(action.position)
|
||||
}
|
||||
|
||||
@Update
|
||||
fun update(action: QuickChatAction)
|
||||
|
||||
@Query("Update quick_chat set position=:position WHERE uuid=:uuid")
|
||||
fun updateActionPosition(uuid: Long, position: Int)
|
||||
|
||||
@Query("Update quick_chat set position=position-1 where position>=:position")
|
||||
fun decrementPositionsAfter(position: Int)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import androidx.room.PrimaryKey
|
|||
|
||||
@Entity(tableName = "quick_chat")
|
||||
data class QuickChatAction(
|
||||
@PrimaryKey(autoGenerate = true) val uuid: Long,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "message") val message: String,
|
||||
@ColumnInfo(name = "mode") val mode: Mode,
|
||||
@PrimaryKey(autoGenerate = true) val uuid: Long = 0L,
|
||||
@ColumnInfo(name = "name") val name: String = "",
|
||||
@ColumnInfo(name = "message") val message: String = "",
|
||||
@ColumnInfo(name = "mode") val mode: Mode = Mode.Instant,
|
||||
@ColumnInfo(name = "position") val position: Int
|
||||
) {
|
||||
enum class Mode {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
|
|
@ -186,8 +186,8 @@ class UIViewModel @Inject constructor(
|
|||
private val _channels = MutableStateFlow(channelSet {})
|
||||
val channels: StateFlow<AppOnlyProtos.ChannelSet> get() = _channels
|
||||
|
||||
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
|
||||
val quickChatActions: StateFlow<List<QuickChatAction>> = _quickChatActions
|
||||
val quickChatActions get() = quickChatActionRepository.getAllActions()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
private val _focusedNode = MutableStateFlow<NodeEntity?>(null)
|
||||
val focusedNode: StateFlow<NodeEntity?> = _focusedNode
|
||||
|
|
@ -230,7 +230,7 @@ class UIViewModel @Inject constructor(
|
|||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = Eagerly,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = NodesUiState.Empty,
|
||||
)
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ class UIViewModel @Inject constructor(
|
|||
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = Eagerly,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -270,11 +270,6 @@ class UIViewModel @Inject constructor(
|
|||
radioConfigRepository.moduleConfigFlow.onEach { config ->
|
||||
_moduleConfig.value = config
|
||||
}.launchIn(viewModelScope)
|
||||
viewModelScope.launch {
|
||||
quickChatActionRepository.getAllActions().collect { actions ->
|
||||
_quickChatActions.value = actions
|
||||
}
|
||||
}
|
||||
radioConfigRepository.channelSetFlow.onEach { channelSet ->
|
||||
_channels.value = channelSet
|
||||
}.launchIn(viewModelScope)
|
||||
|
|
@ -327,7 +322,7 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = Eagerly,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -699,39 +694,16 @@ class UIViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun addQuickChatAction(name: String, value: String, mode: QuickChatAction.Mode) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val action = QuickChatAction(0, name, value, mode, _quickChatActions.value.size)
|
||||
quickChatActionRepository.insert(action)
|
||||
}
|
||||
fun addQuickChatAction(action: QuickChatAction) = viewModelScope.launch(Dispatchers.IO) {
|
||||
quickChatActionRepository.upsert(action)
|
||||
}
|
||||
|
||||
fun deleteQuickChatAction(action: QuickChatAction) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
quickChatActionRepository.delete(action)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuickChatAction(
|
||||
action: QuickChatAction,
|
||||
name: String?,
|
||||
message: String?,
|
||||
mode: QuickChatAction.Mode?
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val newAction = QuickChatAction(
|
||||
action.uuid,
|
||||
name ?: action.name,
|
||||
message ?: action.message,
|
||||
mode ?: action.mode,
|
||||
action.position
|
||||
)
|
||||
quickChatActionRepository.update(newAction)
|
||||
}
|
||||
fun deleteQuickChatAction(action: QuickChatAction) = viewModelScope.launch(Dispatchers.IO) {
|
||||
quickChatActionRepository.delete(action)
|
||||
}
|
||||
|
||||
fun updateActionPositions(actions: List<QuickChatAction>) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
for (position in actions.indices) {
|
||||
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,236 +1,356 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FloatingActionButton
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.FastForward
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.focus.onFocusEvent
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.databinding.QuickChatSettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.components.dragContainer
|
||||
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
|
||||
import com.geeksville.mesh.ui.components.rememberDragDropState
|
||||
import com.geeksville.mesh.ui.theme.AppTheme
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging {
|
||||
private var _binding: QuickChatSettingsFragmentBinding? = null
|
||||
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val model: UIViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = QuickChatSettingsFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.quickChatSettingsToolbar.setNavigationOnClickListener {
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
binding.quickChatSettingsCreateButton.setOnClickListener {
|
||||
val builder = createEditDialog(requireContext(), R.string.quick_chat_new)
|
||||
|
||||
builder.builder.setPositiveButton(R.string.add) { _, _ ->
|
||||
|
||||
val name = builder.nameInput.text.toString().trim()
|
||||
val message = builder.messageInput.text.toString()
|
||||
if (builder.isNotEmpty()) {
|
||||
model.addQuickChatAction(
|
||||
name, message,
|
||||
if (builder.modeSwitch.isChecked) QuickChatAction.Mode.Instant else QuickChatAction.Mode.Append
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = builder.builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
binding.quickChatSettingsView.setContent {
|
||||
val actions by model.quickChatActions.collectAsStateWithLifecycle()
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
|
||||
val list = actions.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
|
||||
model.updateActionPositions(list)
|
||||
}
|
||||
|
||||
AppTheme {
|
||||
LazyColumn(
|
||||
modifier = Modifier.dragContainer(
|
||||
dragDropState = dragDropState,
|
||||
haptics = LocalHapticFeedback.current,
|
||||
),
|
||||
state = listState,
|
||||
// contentPadding = PaddingValues(16.dp),
|
||||
) {
|
||||
dragDropItemsIndexed(
|
||||
items = actions,
|
||||
dragDropState = dragDropState,
|
||||
key = { _, item -> item.uuid },
|
||||
) { _, action, isDragging ->
|
||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 4.dp)
|
||||
QuickChatItem(
|
||||
elevation = elevation,
|
||||
action = action,
|
||||
onEditClick = ::onEditAction,
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
||||
setContent {
|
||||
AppTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(id = R.string.quick_chat)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { parentFragmentManager.popBackStack() }) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
stringResource(id = R.string.navigate_back),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
QuickChatScreen(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
data class DialogBuilder(
|
||||
val builder: MaterialAlertDialogBuilder,
|
||||
val nameInput: EditText,
|
||||
val messageInput: EditText,
|
||||
val modeSwitch: SwitchMaterial,
|
||||
val instantImage: ImageView
|
||||
) {
|
||||
fun isNotEmpty(): Boolean = nameInput.text.isNotEmpty() and messageInput.text.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun getMessageName(message: String): String {
|
||||
return if (message.length <= 3) {
|
||||
message.uppercase()
|
||||
} else {
|
||||
buildString {
|
||||
append(message.first().uppercase())
|
||||
append(message[message.length / 2].uppercase())
|
||||
append(message.last().uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEditDialog(context: Context, @StringRes title: Int): DialogBuilder {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(title)
|
||||
|
||||
val layout = LayoutInflater.from(context).inflate(R.layout.dialog_add_quick_chat, null)
|
||||
|
||||
val nameInput: EditText = layout.findViewById(R.id.addQuickChatName)
|
||||
val messageInput: EditText = layout.findViewById(R.id.addQuickChatMessage)
|
||||
val modeSwitch: SwitchMaterial = layout.findViewById(R.id.addQuickChatMode)
|
||||
val instantImage: ImageView = layout.findViewById(R.id.addQuickChatInsant)
|
||||
instantImage.visibility = if (modeSwitch.isChecked) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
// don't change action name on edits
|
||||
var nameHasChanged = title == R.string.quick_chat_edit
|
||||
|
||||
modeSwitch.setOnCheckedChangeListener { _, _ ->
|
||||
if (modeSwitch.isChecked) {
|
||||
modeSwitch.setText(R.string.quick_chat_instant)
|
||||
instantImage.visibility = View.VISIBLE
|
||||
} else {
|
||||
modeSwitch.setText(R.string.quick_chat_append)
|
||||
instantImage.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
messageInput.addTextChangedListener { text ->
|
||||
if (!nameHasChanged) {
|
||||
nameInput.setText(getMessageName(text.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
nameInput.addTextChangedListener {
|
||||
if (nameInput.isFocused) nameHasChanged = true
|
||||
}
|
||||
|
||||
builder.setView(layout)
|
||||
|
||||
return DialogBuilder(builder, nameInput, messageInput, modeSwitch, instantImage)
|
||||
}
|
||||
|
||||
private fun onEditAction(action: QuickChatAction) {
|
||||
val builder = createEditDialog(requireContext(), R.string.quick_chat_edit)
|
||||
builder.nameInput.setText(action.name)
|
||||
builder.messageInput.setText(action.message)
|
||||
val isInstant = action.mode == QuickChatAction.Mode.Instant
|
||||
builder.modeSwitch.isChecked = isInstant
|
||||
builder.instantImage.visibility = if (isInstant) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
builder.builder.setNegativeButton(R.string.delete) { _, _ ->
|
||||
model.deleteQuickChatAction(action)
|
||||
}
|
||||
builder.builder.setPositiveButton(R.string.save) { _, _ ->
|
||||
if (builder.isNotEmpty()) {
|
||||
model.updateQuickChatAction(
|
||||
action,
|
||||
builder.nameInput.text.toString(),
|
||||
builder.messageInput.text.toString(),
|
||||
if (builder.modeSwitch.isChecked) {
|
||||
QuickChatAction.Mode.Instant
|
||||
} else {
|
||||
QuickChatAction.Mode.Append
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
val dialog = builder.builder.create()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun QuickChatItem(
|
||||
internal fun QuickChatScreen(
|
||||
viewModel: UIViewModel = hiltViewModel(),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val actions by viewModel.quickChatActions.collectAsStateWithLifecycle()
|
||||
var showActionDialog by remember { mutableStateOf<QuickChatAction?>(null) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
|
||||
val list = actions.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
|
||||
viewModel.updateActionPositions(list)
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
if (showActionDialog != null) {
|
||||
val action = showActionDialog ?: return
|
||||
EditQuickChatDialog(
|
||||
action = action,
|
||||
onSave = viewModel::addQuickChatAction,
|
||||
onDelete = viewModel::deleteQuickChatAction,
|
||||
) { showActionDialog = null }
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
showActionDialog = QuickChatAction(position = actions.size)
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = R.string.add),
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.dragContainer(
|
||||
dragDropState = dragDropState,
|
||||
haptics = LocalHapticFeedback.current,
|
||||
),
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
) {
|
||||
dragDropItemsIndexed(
|
||||
items = actions,
|
||||
dragDropState = dragDropState,
|
||||
key = { _, item -> item.uuid },
|
||||
) { _, action, isDragging ->
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (isDragging) 8.dp else 4.dp,
|
||||
label = "DragAndDropElevationAnimation",
|
||||
)
|
||||
QuickChatItem(
|
||||
elevation = elevation,
|
||||
action = action,
|
||||
onEdit = { showActionDialog = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun getMessageName(message: String): String = if (message.length <= 3) {
|
||||
message.uppercase()
|
||||
} else {
|
||||
buildString {
|
||||
append(message.first().uppercase())
|
||||
append(message[message.length / 2].uppercase())
|
||||
append(message.last().uppercase())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun EditQuickChatDialog(
|
||||
action: QuickChatAction,
|
||||
onSave: (QuickChatAction) -> Unit,
|
||||
onDelete: (QuickChatAction) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var actionInput by remember { mutableStateOf(action) }
|
||||
val newQuickChat = action.uuid == 0L
|
||||
val isInstant = actionInput.mode == QuickChatAction.Mode.Instant
|
||||
val title = if (newQuickChat) R.string.quick_chat_new else R.string.quick_chat_edit
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.h6.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextFieldWithCounter(
|
||||
label = stringResource(R.string.name),
|
||||
value = actionInput.name,
|
||||
maxSize = 5,
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { actionInput = actionInput.copy(name = it.uppercase()) }
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextFieldWithCounter(
|
||||
label = stringResource(id = R.string.message),
|
||||
value = actionInput.message,
|
||||
maxSize = 235,
|
||||
getSize = { it.toByteArray().size + 1 },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
actionInput = actionInput.copy(message = it)
|
||||
if (newQuickChat) {
|
||||
actionInput = actionInput.copy(name = getMessageName(it))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val (text, icon) = if (isInstant) {
|
||||
R.string.quick_chat_instant to Icons.Default.FastForward
|
||||
} else {
|
||||
R.string.quick_chat_append to Icons.Default.Add
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (isInstant) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(id = text),
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(text),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
Switch(
|
||||
checked = isInstant,
|
||||
onCheckedChange = { checked ->
|
||||
actionInput = actionInput.copy(
|
||||
mode = when (checked) {
|
||||
true -> QuickChatAction.Mode.Instant
|
||||
false -> QuickChatAction.Mode.Append
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
TextButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onDismiss,
|
||||
) { Text(stringResource(R.string.cancel)) }
|
||||
|
||||
if (!newQuickChat) {
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
onDelete(actionInput)
|
||||
onDismiss()
|
||||
},
|
||||
) { Text(text = stringResource(R.string.delete)) }
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
onSave(actionInput)
|
||||
onDismiss()
|
||||
},
|
||||
enabled = actionInput.name.isNotEmpty() && actionInput.message.isNotEmpty(),
|
||||
) { Text(text = stringResource(R.string.save)) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OutlinedTextFieldWithCounter(
|
||||
label: String,
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
singleLine: Boolean = false,
|
||||
maxSize: Int,
|
||||
getSize: (String) -> Int = { it.length },
|
||||
onValueChange: (String) -> Unit = {},
|
||||
) = Column(modifier) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = {
|
||||
if (getSize(it) <= maxSize) {
|
||||
onValueChange(it)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.onFocusEvent { isFocused = it.isFocused },
|
||||
label = { Text(text = label) },
|
||||
singleLine = singleLine,
|
||||
)
|
||||
if (isFocused) {
|
||||
Text(
|
||||
text = "${getSize(value)}/$maxSize",
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 4.dp, end = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun QuickChatItem(
|
||||
action: QuickChatAction,
|
||||
modifier: Modifier = Modifier,
|
||||
onEditClick: (QuickChatAction) -> Unit = {},
|
||||
onEdit: (QuickChatAction) -> Unit = {},
|
||||
elevation: Dp = 4.dp,
|
||||
) {
|
||||
Card(
|
||||
|
|
@ -240,56 +360,37 @@ internal fun QuickChatItem(
|
|||
elevation = elevation,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
Surface {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val showInstantIcon = action.mode == QuickChatAction.Mode.Instant
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_fast_forward_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
tint = if (showInstantIcon) LocalContentColor.current else Color.Transparent,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = action.name,
|
||||
fontSize = 20.sp,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = action.message,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { onEditClick(action) },
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
ListItem(
|
||||
icon = {
|
||||
if (action.mode == QuickChatAction.Mode.Instant) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_edit_24),
|
||||
contentDescription = null
|
||||
imageVector = Icons.Default.FastForward,
|
||||
contentDescription = stringResource(id = R.string.quick_chat_instant),
|
||||
)
|
||||
}
|
||||
},
|
||||
text = { Text(text = action.name) },
|
||||
secondaryText = { Text(text = action.message) },
|
||||
trailing = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onEdit(action) },
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_edit_24),
|
||||
contentDescription = stringResource(id = R.string.quick_chat_edit),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_drag_handle_24),
|
||||
contentDescription = stringResource(id = R.string.quick_chat),
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_drag_handle_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -299,12 +400,27 @@ private fun QuickChatItemPreview() {
|
|||
AppTheme {
|
||||
QuickChatItem(
|
||||
action = QuickChatAction(
|
||||
uuid = 0L,
|
||||
name = "TST",
|
||||
message = "Test",
|
||||
mode = QuickChatAction.Mode.Instant,
|
||||
position = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
private fun EditQuickChatDialogPreview() {
|
||||
AppTheme {
|
||||
EditQuickChatDialog(
|
||||
action = QuickChatAction(
|
||||
name = "TST",
|
||||
message = "Test",
|
||||
position = 0,
|
||||
),
|
||||
onSave = {},
|
||||
onDelete = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue