mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(sharing): Refactor QR/NFC scanning with ML Kit and CameraX (#4471)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
3971c0a9f4
commit
96551761c8
37 changed files with 1455 additions and 464 deletions
|
|
@ -14,6 +14,8 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:Suppress("detekt:ALL")
|
||||
|
||||
package org.meshtastic.feature.node.list
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
|
|
@ -57,16 +59,19 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_favorite
|
||||
import org.meshtastic.core.strings.channel_invalid
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.mute_always
|
||||
import org.meshtastic.core.strings.node_count_template
|
||||
|
|
@ -79,7 +84,9 @@ import org.meshtastic.core.ui.component.AddContactFAB
|
|||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.smartScrollToTop
|
||||
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.node.component.NodeActionDialogs
|
||||
import org.meshtastic.feature.node.component.NodeFilterTextField
|
||||
import org.meshtastic.feature.node.component.NodeItem
|
||||
|
|
@ -90,10 +97,13 @@ import org.meshtastic.proto.SharedContact
|
|||
@Composable
|
||||
fun NodeListScreen(
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
onNavigateToChannels: () -> Unit = {},
|
||||
viewModel: NodeListViewModel = hiltViewModel(),
|
||||
scrollToTopEvents: Flow<ScrollToTopEvent>? = null,
|
||||
activeNodeId: Int? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val state by viewModel.nodesUiState.collectAsStateWithLifecycle()
|
||||
|
||||
val nodes by viewModel.nodeList.collectAsStateWithLifecycle()
|
||||
|
|
@ -119,6 +129,10 @@ fun NodeListScreen(
|
|||
val isScrollInProgress by remember {
|
||||
derivedStateOf { listState.isScrollInProgress && (listState.canScrollForward || listState.canScrollBackward) }
|
||||
}
|
||||
|
||||
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelSet() }) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
|
|
@ -142,7 +156,10 @@ fun NodeListScreen(
|
|||
visible = !isScrollInProgress && connectionState == ConnectionState.Connected && shareCapable,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onSharedContactRequested = { contact -> viewModel.setSharedContactRequested(contact) },
|
||||
onResult = { uri ->
|
||||
viewModel.handleScannedUri(uri) { scope.launch { context.showToast(Res.string.channel_invalid) } }
|
||||
},
|
||||
onDismissSharedContact = { viewModel.setSharedContactRequested(null) },
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@
|
|||
*/
|
||||
package org.meshtastic.feature.node.list
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -32,9 +35,12 @@ import org.meshtastic.core.data.repository.NodeRepository
|
|||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.model.util.toChannelSet
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.component.toSharedContact
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
|
@ -45,7 +51,7 @@ class NodeListViewModel
|
|||
constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val nodeRepository: NodeRepository,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
val nodeActions: NodeActions,
|
||||
val nodeFilterPreferences: NodeFilterPreferences,
|
||||
|
|
@ -62,6 +68,9 @@ constructor(
|
|||
private val _sharedContactRequested: MutableStateFlow<SharedContact?> = MutableStateFlow(null)
|
||||
val sharedContactRequested = _sharedContactRequested.asStateFlow()
|
||||
|
||||
private val _requestChannelSet = MutableStateFlow<ChannelSet?>(null)
|
||||
val requestChannelSet = _requestChannelSet.asStateFlow()
|
||||
|
||||
private val nodeSortOption = nodeFilterPreferences.nodeSortOption
|
||||
|
||||
private val _nodeFilterText = savedStateHandle.getStateFlow(KEY_FILTER_TEXT, "")
|
||||
|
|
@ -155,6 +164,38 @@ constructor(
|
|||
_sharedContactRequested.value = sharedContact
|
||||
}
|
||||
|
||||
fun handleScannedUri(uri: Uri, onInvalid: () -> Unit) {
|
||||
if (uri.path?.contains("/v/") == true) {
|
||||
runCatching { _sharedContactRequested.value = uri.toSharedContact() }
|
||||
.onFailure { ex ->
|
||||
Logger.e(ex) { "Shared contact error" }
|
||||
onInvalid()
|
||||
}
|
||||
} else {
|
||||
runCatching { _requestChannelSet.value = uri.toChannelSet() }
|
||||
.onFailure { ex ->
|
||||
Logger.e(ex) { "Channel url error" }
|
||||
onInvalid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearRequestChannelSet() {
|
||||
_requestChannelSet.value = null
|
||||
}
|
||||
|
||||
fun setChannels(channelSet: ChannelSet) = viewModelScope.launch {
|
||||
radioConfigRepository.replaceAllSettings(channelSet.settings)
|
||||
val newLoraConfig = channelSet.lora_config
|
||||
if (newLoraConfig != null) {
|
||||
try {
|
||||
serviceRepository.meshService?.setConfig(Config(lora = newLoraConfig).encode())
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Set config error" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun favoriteNode(node: Node) = viewModelScope.launch { nodeActions.favoriteNode(node) }
|
||||
|
||||
fun ignoreNode(node: Node) = viewModelScope.launch { nodeActions.ignoreNode(node) }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue