From 475e9fc22c85c432c51ccbc00c00d081af11fb46 Mon Sep 17 00:00:00 2001 From: Andre K Date: Tue, 19 Nov 2024 11:59:28 -0300 Subject: [PATCH] refactor: migrate `QuickChat` to Compose (#1419) --- .../database/QuickChatActionRepository.kt | 29 +- .../mesh/database/dao/QuickChatActionDao.kt | 15 +- .../mesh/database/entity/QuickChatAction.kt | 8 +- .../java/com/geeksville/mesh/model/UIState.kt | 50 +- .../mesh/ui/QuickChatSettingsFragment.kt | 564 +++++++++++------- .../drawable/ic_baseline_fast_forward_24.xml | 5 - .../main/res/layout/dialog_add_quick_chat.xml | 66 -- 7 files changed, 375 insertions(+), 362 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_baseline_fast_forward_24.xml delete mode 100644 app/src/main/res/layout/dialog_add_quick_chat.xml diff --git a/app/src/main/java/com/geeksville/mesh/database/QuickChatActionRepository.kt b/app/src/main/java/com/geeksville/mesh/database/QuickChatActionRepository.kt index 0bef1c756..51cc750bf 100644 --- a/app/src/main/java/com/geeksville/mesh/database/QuickChatActionRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/database/QuickChatActionRepository.kt @@ -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) { +class QuickChatActionRepository @Inject constructor( + private val quickChatDaoLazy: dagger.Lazy, + private val dispatchers: CoroutineDispatchers, +) { private val quickChatActionDao by lazy { quickChatDaoLazy.get() } - suspend fun getAllActions(): Flow> = 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/QuickChatActionDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/QuickChatActionDao.kt index 8af931323..237696494 100644 --- a/app/src/main/java/com/geeksville/mesh/database/dao/QuickChatActionDao.kt +++ b/app/src/main/java/com/geeksville/mesh/database/dao/QuickChatActionDao.kt @@ -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> - @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) - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/QuickChatAction.kt b/app/src/main/java/com/geeksville/mesh/database/entity/QuickChatAction.kt index 5f88ae770..f4bb26619 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/QuickChatAction.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/QuickChatAction.kt @@ -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 { diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 900724ddd..95fe93ab8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -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 get() = _channels - private val _quickChatActions = MutableStateFlow>(emptyList()) - val quickChatActions: StateFlow> = _quickChatActions + val quickChatActions get() = quickChatActionRepository.getAllActions() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) private val _focusedNode = MutableStateFlow(null) val focusedNode: StateFlow = _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) { - viewModelScope.launch(Dispatchers.Main) { + viewModelScope.launch(Dispatchers.IO) { for (position in actions.indices) { quickChatActionRepository.setItemPosition(actions[position].uuid, position) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt index e7ca39afd..7557f2c8b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/QuickChatSettingsFragment.kt @@ -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(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 = {} + ) + } +} diff --git a/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml b/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml deleted file mode 100644 index ac50e268d..000000000 --- a/app/src/main/res/drawable/ic_baseline_fast_forward_24.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/layout/dialog_add_quick_chat.xml b/app/src/main/res/layout/dialog_add_quick_chat.xml deleted file mode 100644 index da559bc80..000000000 --- a/app/src/main/res/layout/dialog_add_quick_chat.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file