refactor: migrate ShareFragment to Compose

This commit is contained in:
andrekir 2025-01-06 18:44:20 -03:00 committed by Andre K
parent 0635b25e1c
commit 1c863f35f6
4 changed files with 230 additions and 133 deletions

View file

@ -71,6 +71,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
@ -186,6 +187,8 @@ enum class AdminRoute(@StringRes val title: Int) {
sealed interface Route {
@Serializable
data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable
data class Share(val message: String) : Route
@Serializable
data class RadioConfig(val destNum: Int? = null) : Route
@ -448,5 +451,15 @@ fun NavGraph(
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
PaxcounterConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
}
composable<Route.Share> { backStackEntry ->
val message = backStackEntry.toRoute<Route.Share>().message
ShareScreen(
navigateUp = navController::navigateUp,
) {
navController.navigate(Route.Messages(it, message)) {
popUpTo<Route.Share> { inclusive = true }
}
}
}
}
}

View file

@ -21,24 +21,38 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
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.databinding.ShareFragmentBinding
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.components.BaseScaffold
import com.geeksville.mesh.ui.message.navigateToMessages
import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
@ -54,93 +68,114 @@ internal fun FragmentManager.navigateToShareMessage(message: String) {
}
@AndroidEntryPoint
class ShareFragment : ScreenFragment("Messages"), Logging {
class ShareFragment : ScreenFragment("ShareFragment"), Logging {
private val model: UIViewModel by activityViewModels()
private var _binding: ShareFragmentBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
private val contacts get() = model.contactList.value
private var selectedContact = mutableStateOf("")
private fun shareMessage(contact: Contact) {
debug("calling MessagesFragment filter:${contact.contactKey}")
private fun shareMessage(contactKey: String) {
debug("calling MessagesFragment filter:$contactKey")
parentFragmentManager.navigateToMessages(
contact.contactKey,
contactKey,
arguments?.getString("message").toString()
)
}
private fun onClick(contact: Contact) {
if (selectedContact.value == contact.contactKey) {
selectedContact.value = ""
binding.shareButton.isEnabled = false
} else {
selectedContact.value = contact.contactKey
binding.shareButton.isEnabled = true
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ShareFragmentBinding.inflate(inflater, container, false)
return binding.root
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setNavigationOnClickListener {
parentFragmentManager.popBackStack()
}
binding.shareButton.isEnabled = false
binding.shareButton.setOnClickListener {
debug("User clicked shareButton")
val contact = contacts.find { c -> c.contactKey == selectedContact.value }
if (contact != null) {
shareMessage(contact)
}
}
binding.contactListView.setContent {
val contacts by model.contactList.collectAsStateWithLifecycle()
AppTheme {
ShareContactListView(
contacts = contacts,
selectedContact = selectedContact.value,
onClick = ::onClick,
)
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
ShareScreen(
viewModel = model,
navigateUp = parentFragmentManager::popBackStack,
onConfirm = ::shareMessage
)
}
}
}
}
}
@Composable
fun ShareContactListView(
contacts: List<Contact>,
selectedContact: String,
onClick: (Contact) -> Unit,
internal fun ShareScreen(
viewModel: UIViewModel = hiltViewModel(),
navigateUp: () -> Unit,
onConfirm: (String) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(6.dp),
val contactList by viewModel.contactList.collectAsStateWithLifecycle()
BaseScaffold(
title = stringResource(R.string.share_to),
canNavigateBack = true,
navigateUp = navigateUp,
) {
items(contacts, key = { it.contactKey }) { contact ->
val selected = contact.contactKey == selectedContact
ContactItem(
contact = contact,
selected = selected,
onClick = { onClick(contact) },
onLongClick = {},
ShareContent(
contacts = contactList,
onConfirm = onConfirm,
)
}
}
@Composable
private fun ShareContent(
contacts: List<Contact>,
onConfirm: (String) -> Unit = {}
) {
var selectedContact by rememberSaveable { mutableStateOf("") }
Column {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(contacts, key = { it.contactKey }) { contact ->
val selected = contact.contactKey == selectedContact
ContactItem(
contact = contact,
selected = selected,
onClick = { selectedContact = contact.contactKey },
)
}
}
Button(
onClick = {
onConfirm(selectedContact)
},
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
enabled = selectedContact.isNotEmpty(),
) {
Icon(
imageVector = Icons.AutoMirrored.Default.Send,
contentDescription = stringResource(id = R.string.share)
)
}
}
}
@PreviewScreenSizes
@Composable
private fun ShareContentPreview() {
AppTheme {
ShareContent(
contacts = listOf(
Contact(
contactKey = "0^all",
shortName = stringResource(R.string.some_username),
longName = stringResource(R.string.unknown_username),
lastMessageTime = "3 minutes ago",
lastMessageText = stringResource(R.string.sample_message),
unreadCount = 2,
messageCount = 10,
isMuted = true,
),
),
)
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.FabPosition
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.dropUnlessResumed
import com.geeksville.mesh.R
@Composable
fun BaseScaffold(
title: String,
modifier: Modifier = Modifier,
canNavigateBack: Boolean = true,
navigateUp: (() -> Unit)? = null,
actions: @Composable (RowScope.() -> Unit)? = null,
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
contentWindowInsets: WindowInsets = WindowInsets(0, 0, 0, 0),
content: @Composable () -> Unit,
) {
BaseScaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = if (canNavigateBack) {
{
IconButton(onClick = dropUnlessResumed { navigateUp?.invoke() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.navigate_back),
modifier = Modifier
)
}
}
} else {
null
},
actions = { actions?.invoke(this) },
)
},
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
contentWindowInsets = contentWindowInsets,
content = content
)
}
@Composable
fun BaseScaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
contentWindowInsets: WindowInsets = WindowInsets(0, 0, 0, 0),
content: @Composable () -> Unit,
) {
Scaffold(
modifier = modifier.imePadding(),
topBar = topBar,
bottomBar = bottomBar,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
backgroundColor = backgroundColor,
contentColor = contentColor,
contentWindowInsets = contentWindowInsets,
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
content()
}
}
}