mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(ui): compose resources, domain layer (#4628)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
96adc70401
commit
2676a51647
322 changed files with 3031 additions and 2790 deletions
|
|
@ -16,7 +16,7 @@ graph TB
|
|||
:feature:firmware -.-> :core:prefs
|
||||
:feature:firmware -.-> :core:proto
|
||||
:feature:firmware -.-> :core:service
|
||||
:feature:firmware -.-> :core:strings
|
||||
:feature:firmware -.-> :core:resources
|
||||
:feature:firmware -.-> :core:ui
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ dependencies {
|
|||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.accompanist.permissions)
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ import kotlinx.coroutines.runBlocking
|
|||
import no.nordicsemi.android.dfu.DfuBaseService
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.model.BuildConfig
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.firmware_update_channel_description
|
||||
import org.meshtastic.core.strings.firmware_update_channel_name
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.firmware_update_channel_description
|
||||
import org.meshtastic.core.resources.firmware_update_channel_name
|
||||
|
||||
class FirmwareDfuService : DfuBaseService() {
|
||||
override fun onCreate() {
|
||||
|
|
|
|||
|
|
@ -91,50 +91,51 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseType
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.back
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.chirpy
|
||||
import org.meshtastic.core.strings.dont_show_again_for_device
|
||||
import org.meshtastic.core.strings.firmware_update_almost_there
|
||||
import org.meshtastic.core.strings.firmware_update_alpha
|
||||
import org.meshtastic.core.strings.firmware_update_checking
|
||||
import org.meshtastic.core.strings.firmware_update_currently_installed
|
||||
import org.meshtastic.core.strings.firmware_update_device
|
||||
import org.meshtastic.core.strings.firmware_update_disclaimer_chirpy_says
|
||||
import org.meshtastic.core.strings.firmware_update_disclaimer_text
|
||||
import org.meshtastic.core.strings.firmware_update_disclaimer_title
|
||||
import org.meshtastic.core.strings.firmware_update_disconnect_warning
|
||||
import org.meshtastic.core.strings.firmware_update_do_not_close
|
||||
import org.meshtastic.core.strings.firmware_update_done
|
||||
import org.meshtastic.core.strings.firmware_update_error
|
||||
import org.meshtastic.core.strings.firmware_update_hang_tight
|
||||
import org.meshtastic.core.strings.firmware_update_keep_device_close
|
||||
import org.meshtastic.core.strings.firmware_update_latest
|
||||
import org.meshtastic.core.strings.firmware_update_local_file
|
||||
import org.meshtastic.core.strings.firmware_update_method_detail
|
||||
import org.meshtastic.core.strings.firmware_update_rak4631_bootloader_hint
|
||||
import org.meshtastic.core.strings.firmware_update_release_notes
|
||||
import org.meshtastic.core.strings.firmware_update_retry
|
||||
import org.meshtastic.core.strings.firmware_update_save_dfu_file
|
||||
import org.meshtastic.core.strings.firmware_update_select_file
|
||||
import org.meshtastic.core.strings.firmware_update_source_local
|
||||
import org.meshtastic.core.strings.firmware_update_stable
|
||||
import org.meshtastic.core.strings.firmware_update_success
|
||||
import org.meshtastic.core.strings.firmware_update_taking_a_while
|
||||
import org.meshtastic.core.strings.firmware_update_target
|
||||
import org.meshtastic.core.strings.firmware_update_title
|
||||
import org.meshtastic.core.strings.firmware_update_unknown_release
|
||||
import org.meshtastic.core.strings.firmware_update_usb_bootloader_warning
|
||||
import org.meshtastic.core.strings.firmware_update_usb_instruction_text
|
||||
import org.meshtastic.core.strings.firmware_update_usb_instruction_title
|
||||
import org.meshtastic.core.strings.firmware_update_verification_failed
|
||||
import org.meshtastic.core.strings.firmware_update_verifying
|
||||
import org.meshtastic.core.strings.firmware_update_waiting_reconnect
|
||||
import org.meshtastic.core.strings.i_know_what_i_m_doing
|
||||
import org.meshtastic.core.strings.learn_more
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.back
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.chirpy
|
||||
import org.meshtastic.core.resources.dont_show_again_for_device
|
||||
import org.meshtastic.core.resources.firmware_update_almost_there
|
||||
import org.meshtastic.core.resources.firmware_update_alpha
|
||||
import org.meshtastic.core.resources.firmware_update_checking
|
||||
import org.meshtastic.core.resources.firmware_update_currently_installed
|
||||
import org.meshtastic.core.resources.firmware_update_device
|
||||
import org.meshtastic.core.resources.firmware_update_disclaimer_chirpy_says
|
||||
import org.meshtastic.core.resources.firmware_update_disclaimer_text
|
||||
import org.meshtastic.core.resources.firmware_update_disclaimer_title
|
||||
import org.meshtastic.core.resources.firmware_update_disconnect_warning
|
||||
import org.meshtastic.core.resources.firmware_update_do_not_close
|
||||
import org.meshtastic.core.resources.firmware_update_done
|
||||
import org.meshtastic.core.resources.firmware_update_error
|
||||
import org.meshtastic.core.resources.firmware_update_hang_tight
|
||||
import org.meshtastic.core.resources.firmware_update_keep_device_close
|
||||
import org.meshtastic.core.resources.firmware_update_latest
|
||||
import org.meshtastic.core.resources.firmware_update_local_file
|
||||
import org.meshtastic.core.resources.firmware_update_method_detail
|
||||
import org.meshtastic.core.resources.firmware_update_rak4631_bootloader_hint
|
||||
import org.meshtastic.core.resources.firmware_update_release_notes
|
||||
import org.meshtastic.core.resources.firmware_update_retry
|
||||
import org.meshtastic.core.resources.firmware_update_save_dfu_file
|
||||
import org.meshtastic.core.resources.firmware_update_select_file
|
||||
import org.meshtastic.core.resources.firmware_update_source_local
|
||||
import org.meshtastic.core.resources.firmware_update_stable
|
||||
import org.meshtastic.core.resources.firmware_update_success
|
||||
import org.meshtastic.core.resources.firmware_update_taking_a_while
|
||||
import org.meshtastic.core.resources.firmware_update_target
|
||||
import org.meshtastic.core.resources.firmware_update_title
|
||||
import org.meshtastic.core.resources.firmware_update_unknown_release
|
||||
import org.meshtastic.core.resources.firmware_update_usb_bootloader_warning
|
||||
import org.meshtastic.core.resources.firmware_update_usb_instruction_text
|
||||
import org.meshtastic.core.resources.firmware_update_usb_instruction_title
|
||||
import org.meshtastic.core.resources.firmware_update_verification_failed
|
||||
import org.meshtastic.core.resources.firmware_update_verifying
|
||||
import org.meshtastic.core.resources.firmware_update_waiting_reconnect
|
||||
import org.meshtastic.core.resources.i_know_what_i_m_doing
|
||||
import org.meshtastic.core.resources.img_chirpy
|
||||
import org.meshtastic.core.resources.learn_more
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.save
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.icon.Bluetooth
|
||||
import org.meshtastic.core.ui.icon.CheckCircle
|
||||
|
|
@ -336,7 +337,9 @@ private fun FirmwareUpdateContent(
|
|||
is FirmwareUpdateState.Verifying -> VerifyingState()
|
||||
is FirmwareUpdateState.VerificationFailed ->
|
||||
VerificationFailedState(onRetry = actions.onStartUpdate, onIgnore = actions.onDone)
|
||||
|
||||
is FirmwareUpdateState.Error -> ErrorState(error = state.error, onRetry = actions.onRetry)
|
||||
|
||||
is FirmwareUpdateState.Success -> SuccessState(onDone = actions.onDone)
|
||||
is FirmwareUpdateState.AwaitingFileSave -> AwaitingFileSaveState(state, actions.onSaveFile)
|
||||
}
|
||||
|
|
@ -493,7 +496,7 @@ private fun ChirpyCard() {
|
|||
AsyncImage(
|
||||
model =
|
||||
ImageRequest.Builder(LocalContext.current)
|
||||
.data(org.meshtastic.core.ui.R.drawable.chirpy)
|
||||
.data(Res.drawable.img_chirpy)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentScale = ContentScale.Fit,
|
||||
|
|
|
|||
|
|
@ -49,30 +49,30 @@ import org.meshtastic.core.prefs.radio.RadioPrefs
|
|||
import org.meshtastic.core.prefs.radio.isBle
|
||||
import org.meshtastic.core.prefs.radio.isSerial
|
||||
import org.meshtastic.core.prefs.radio.isTcp
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.firmware_update_battery_low
|
||||
import org.meshtastic.core.resources.firmware_update_copying
|
||||
import org.meshtastic.core.resources.firmware_update_dfu_aborted
|
||||
import org.meshtastic.core.resources.firmware_update_dfu_error
|
||||
import org.meshtastic.core.resources.firmware_update_disconnecting
|
||||
import org.meshtastic.core.resources.firmware_update_enabling_dfu
|
||||
import org.meshtastic.core.resources.firmware_update_extracting
|
||||
import org.meshtastic.core.resources.firmware_update_failed
|
||||
import org.meshtastic.core.resources.firmware_update_flashing
|
||||
import org.meshtastic.core.resources.firmware_update_local_failed
|
||||
import org.meshtastic.core.resources.firmware_update_method_ble
|
||||
import org.meshtastic.core.resources.firmware_update_method_usb
|
||||
import org.meshtastic.core.resources.firmware_update_method_wifi
|
||||
import org.meshtastic.core.resources.firmware_update_no_device
|
||||
import org.meshtastic.core.resources.firmware_update_node_info_missing
|
||||
import org.meshtastic.core.resources.firmware_update_starting_dfu
|
||||
import org.meshtastic.core.resources.firmware_update_unknown_error
|
||||
import org.meshtastic.core.resources.firmware_update_unknown_hardware
|
||||
import org.meshtastic.core.resources.firmware_update_updating
|
||||
import org.meshtastic.core.resources.firmware_update_validating
|
||||
import org.meshtastic.core.resources.unknown
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.firmware_update_battery_low
|
||||
import org.meshtastic.core.strings.firmware_update_copying
|
||||
import org.meshtastic.core.strings.firmware_update_dfu_aborted
|
||||
import org.meshtastic.core.strings.firmware_update_dfu_error
|
||||
import org.meshtastic.core.strings.firmware_update_disconnecting
|
||||
import org.meshtastic.core.strings.firmware_update_enabling_dfu
|
||||
import org.meshtastic.core.strings.firmware_update_extracting
|
||||
import org.meshtastic.core.strings.firmware_update_failed
|
||||
import org.meshtastic.core.strings.firmware_update_flashing
|
||||
import org.meshtastic.core.strings.firmware_update_local_failed
|
||||
import org.meshtastic.core.strings.firmware_update_method_ble
|
||||
import org.meshtastic.core.strings.firmware_update_method_usb
|
||||
import org.meshtastic.core.strings.firmware_update_method_wifi
|
||||
import org.meshtastic.core.strings.firmware_update_no_device
|
||||
import org.meshtastic.core.strings.firmware_update_node_info_missing
|
||||
import org.meshtastic.core.strings.firmware_update_starting_dfu
|
||||
import org.meshtastic.core.strings.firmware_update_unknown_error
|
||||
import org.meshtastic.core.strings.firmware_update_unknown_hardware
|
||||
import org.meshtastic.core.strings.firmware_update_updating
|
||||
import org.meshtastic.core.strings.firmware_update_validating
|
||||
import org.meshtastic.core.strings.unknown
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ import no.nordicsemi.android.dfu.DfuServiceListenerHelper
|
|||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.firmware_update_downloading_percent
|
||||
import org.meshtastic.core.resources.firmware_update_nordic_failed
|
||||
import org.meshtastic.core.resources.firmware_update_not_found_in_release
|
||||
import org.meshtastic.core.resources.firmware_update_starting_service
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.firmware_update_downloading_percent
|
||||
import org.meshtastic.core.strings.firmware_update_nordic_failed
|
||||
import org.meshtastic.core.strings.firmware_update_not_found_in_release
|
||||
import org.meshtastic.core.strings.firmware_update_starting_service
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ import kotlinx.coroutines.delay
|
|||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.firmware_update_downloading_percent
|
||||
import org.meshtastic.core.resources.firmware_update_rebooting
|
||||
import org.meshtastic.core.resources.firmware_update_retrieval_failed
|
||||
import org.meshtastic.core.resources.firmware_update_usb_failed
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.firmware_update_downloading_percent
|
||||
import org.meshtastic.core.strings.firmware_update_rebooting
|
||||
import org.meshtastic.core.strings.firmware_update_retrieval_failed
|
||||
import org.meshtastic.core.strings.firmware_update_usb_failed
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -29,18 +29,18 @@ import org.jetbrains.compose.resources.getString
|
|||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.firmware_update_connecting_attempt
|
||||
import org.meshtastic.core.resources.firmware_update_downloading_percent
|
||||
import org.meshtastic.core.resources.firmware_update_erasing
|
||||
import org.meshtastic.core.resources.firmware_update_hash_rejected
|
||||
import org.meshtastic.core.resources.firmware_update_loading
|
||||
import org.meshtastic.core.resources.firmware_update_ota_failed
|
||||
import org.meshtastic.core.resources.firmware_update_retrieval_failed
|
||||
import org.meshtastic.core.resources.firmware_update_starting_ota
|
||||
import org.meshtastic.core.resources.firmware_update_uploading
|
||||
import org.meshtastic.core.resources.firmware_update_waiting_reboot
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.firmware_update_connecting_attempt
|
||||
import org.meshtastic.core.strings.firmware_update_downloading_percent
|
||||
import org.meshtastic.core.strings.firmware_update_erasing
|
||||
import org.meshtastic.core.strings.firmware_update_hash_rejected
|
||||
import org.meshtastic.core.strings.firmware_update_loading
|
||||
import org.meshtastic.core.strings.firmware_update_ota_failed
|
||||
import org.meshtastic.core.strings.firmware_update_retrieval_failed
|
||||
import org.meshtastic.core.strings.firmware_update_starting_ota
|
||||
import org.meshtastic.core.strings.firmware_update_uploading
|
||||
import org.meshtastic.core.strings.firmware_update_waiting_reboot
|
||||
import org.meshtastic.feature.firmware.FirmwareRetriever
|
||||
import org.meshtastic.feature.firmware.FirmwareUpdateHandler
|
||||
import org.meshtastic.feature.firmware.FirmwareUpdateState
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Dedicated screens for explaining and requesting specific permissions:
|
|||
```mermaid
|
||||
graph TB
|
||||
:feature:intro[intro]:::android-feature
|
||||
:feature:intro -.-> :core:strings
|
||||
:feature:intro -.-> :core:resources
|
||||
:feature:intro -.-> :core:ui
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ plugins {
|
|||
configure<LibraryExtension> { namespace = "org.meshtastic.feature.intro" }
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.analytics_notice
|
||||
import org.meshtastic.core.strings.analytics_platforms
|
||||
import org.meshtastic.core.strings.datadog_link
|
||||
import org.meshtastic.core.strings.firebase_link
|
||||
import org.meshtastic.core.strings.for_more_information_see_our_privacy_policy
|
||||
import org.meshtastic.core.strings.privacy_url
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.analytics_notice
|
||||
import org.meshtastic.core.resources.analytics_platforms
|
||||
import org.meshtastic.core.resources.datadog_link
|
||||
import org.meshtastic.core.resources.firebase_link
|
||||
import org.meshtastic.core.resources.for_more_information_see_our_privacy_policy
|
||||
import org.meshtastic.core.resources.privacy_url
|
||||
import org.meshtastic.core.ui.component.AutoLinkText
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ import kotlinx.serialization.Serializable
|
|||
import no.nordicsemi.android.common.permissions.ble.RequireBluetooth
|
||||
import no.nordicsemi.android.common.permissions.ble.RequireLocation
|
||||
import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.permission_denied
|
||||
import org.meshtastic.core.strings.permission_granted
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.permission_denied
|
||||
import org.meshtastic.core.resources.permission_granted
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,16 +25,16 @@ import androidx.compose.material.icons.outlined.SettingsInputAntenna
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.bluetooth_feature_config
|
||||
import org.meshtastic.core.strings.bluetooth_feature_config_description
|
||||
import org.meshtastic.core.strings.bluetooth_feature_discovery
|
||||
import org.meshtastic.core.strings.bluetooth_feature_discovery_description
|
||||
import org.meshtastic.core.strings.bluetooth_permission
|
||||
import org.meshtastic.core.strings.configure_bluetooth_permissions
|
||||
import org.meshtastic.core.strings.next
|
||||
import org.meshtastic.core.strings.permission_missing_31
|
||||
import org.meshtastic.core.strings.settings
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bluetooth_feature_config
|
||||
import org.meshtastic.core.resources.bluetooth_feature_config_description
|
||||
import org.meshtastic.core.resources.bluetooth_feature_discovery
|
||||
import org.meshtastic.core.resources.bluetooth_feature_discovery_description
|
||||
import org.meshtastic.core.resources.bluetooth_permission
|
||||
import org.meshtastic.core.resources.configure_bluetooth_permissions
|
||||
import org.meshtastic.core.resources.next
|
||||
import org.meshtastic.core.resources.permission_missing_31
|
||||
import org.meshtastic.core.resources.settings
|
||||
|
||||
/**
|
||||
* Screen for configuring Bluetooth permissions during the app introduction. It explains why Bluetooth permissions are
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.intro
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -34,11 +33,11 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.configure_critical_alerts
|
||||
import org.meshtastic.core.strings.critical_alerts
|
||||
import org.meshtastic.core.strings.critical_alerts_dnd_request_text
|
||||
import org.meshtastic.core.strings.skip
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.configure_critical_alerts
|
||||
import org.meshtastic.core.resources.critical_alerts
|
||||
import org.meshtastic.core.resources.critical_alerts_dnd_request_text
|
||||
import org.meshtastic.core.resources.skip
|
||||
|
||||
/**
|
||||
* Screen for explaining and guiding the user to configure critical alert settings. This screen is part of the app
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.intro
|
||||
|
||||
import android.content.Intent
|
||||
|
|
@ -26,20 +25,20 @@ import androidx.compose.material.icons.outlined.Router
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.configure_location_permissions
|
||||
import org.meshtastic.core.strings.distance_filters
|
||||
import org.meshtastic.core.strings.distance_filters_description
|
||||
import org.meshtastic.core.strings.distance_measurements
|
||||
import org.meshtastic.core.strings.distance_measurements_description
|
||||
import org.meshtastic.core.strings.mesh_map_location
|
||||
import org.meshtastic.core.strings.mesh_map_location_description
|
||||
import org.meshtastic.core.strings.next
|
||||
import org.meshtastic.core.strings.phone_location
|
||||
import org.meshtastic.core.strings.phone_location_description
|
||||
import org.meshtastic.core.strings.settings
|
||||
import org.meshtastic.core.strings.share_location
|
||||
import org.meshtastic.core.strings.share_location_description
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.configure_location_permissions
|
||||
import org.meshtastic.core.resources.distance_filters
|
||||
import org.meshtastic.core.resources.distance_filters_description
|
||||
import org.meshtastic.core.resources.distance_measurements
|
||||
import org.meshtastic.core.resources.distance_measurements_description
|
||||
import org.meshtastic.core.resources.mesh_map_location
|
||||
import org.meshtastic.core.resources.mesh_map_location_description
|
||||
import org.meshtastic.core.resources.next
|
||||
import org.meshtastic.core.resources.phone_location
|
||||
import org.meshtastic.core.resources.phone_location_description
|
||||
import org.meshtastic.core.resources.settings
|
||||
import org.meshtastic.core.resources.share_location
|
||||
import org.meshtastic.core.resources.share_location_description
|
||||
|
||||
/**
|
||||
* Screen for configuring location permissions during the app introduction. It explains why location permissions are
|
||||
|
|
|
|||
|
|
@ -26,18 +26,18 @@ import androidx.compose.material.icons.outlined.SpeakerPhone
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.app_notifications
|
||||
import org.meshtastic.core.strings.configure_notification_permissions
|
||||
import org.meshtastic.core.strings.incoming_messages
|
||||
import org.meshtastic.core.strings.low_battery
|
||||
import org.meshtastic.core.strings.new_nodes
|
||||
import org.meshtastic.core.strings.next
|
||||
import org.meshtastic.core.strings.notification_permissions_description
|
||||
import org.meshtastic.core.strings.notifications_for_channel_and_direct_messages
|
||||
import org.meshtastic.core.strings.notifications_for_low_battery_alerts
|
||||
import org.meshtastic.core.strings.notifications_for_newly_discovered_nodes
|
||||
import org.meshtastic.core.strings.settings
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.app_notifications
|
||||
import org.meshtastic.core.resources.configure_notification_permissions
|
||||
import org.meshtastic.core.resources.incoming_messages
|
||||
import org.meshtastic.core.resources.low_battery
|
||||
import org.meshtastic.core.resources.new_nodes
|
||||
import org.meshtastic.core.resources.next
|
||||
import org.meshtastic.core.resources.notification_permissions_description
|
||||
import org.meshtastic.core.resources.notifications_for_channel_and_direct_messages
|
||||
import org.meshtastic.core.resources.notifications_for_low_battery_alerts
|
||||
import org.meshtastic.core.resources.notifications_for_newly_discovered_nodes
|
||||
import org.meshtastic.core.resources.settings
|
||||
|
||||
/**
|
||||
* Screen for configuring notification permissions during the app introduction. It explains why notification permissions
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.intro
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
|
|
@ -43,8 +42,8 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.skip
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.skip
|
||||
|
||||
/**
|
||||
* A generic layout for screens within the app introduction flow. It typically presents a headline, a descriptive text
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.intro
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -40,16 +39,16 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.communicate_off_the_grid
|
||||
import org.meshtastic.core.strings.create_your_own_networks
|
||||
import org.meshtastic.core.strings.easily_set_up_private_mesh_networks
|
||||
import org.meshtastic.core.strings.get_started
|
||||
import org.meshtastic.core.strings.intro_welcome
|
||||
import org.meshtastic.core.strings.meshtastic
|
||||
import org.meshtastic.core.strings.share_your_location_in_real_time
|
||||
import org.meshtastic.core.strings.stay_connected_anywhere
|
||||
import org.meshtastic.core.strings.track_and_share_locations
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.communicate_off_the_grid
|
||||
import org.meshtastic.core.resources.create_your_own_networks
|
||||
import org.meshtastic.core.resources.easily_set_up_private_mesh_networks
|
||||
import org.meshtastic.core.resources.get_started
|
||||
import org.meshtastic.core.resources.intro_welcome
|
||||
import org.meshtastic.core.resources.meshtastic
|
||||
import org.meshtastic.core.resources.share_your_location_in_real_time
|
||||
import org.meshtastic.core.resources.stay_connected_anywhere
|
||||
import org.meshtastic.core.resources.track_and_share_locations
|
||||
|
||||
/**
|
||||
* The initial welcome screen for the app introduction flow. It displays a brief overview of the app's key features.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ graph TB
|
|||
:feature:map -.-> :core:prefs
|
||||
:feature:map -.-> :core:proto
|
||||
:feature:map -.-> :core:service
|
||||
:feature:map -.-> :core:strings
|
||||
:feature:map -.-> :core:resources
|
||||
:feature:map -.-> :core:ui
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ dependencies {
|
|||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.accompanist.permissions)
|
||||
|
|
|
|||
|
|
@ -89,38 +89,38 @@ import org.meshtastic.core.common.util.nowMillis
|
|||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.calculating
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.clear
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.strings.delete_for_everyone
|
||||
import org.meshtastic.core.strings.delete_for_me
|
||||
import org.meshtastic.core.strings.expires
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.location_disabled
|
||||
import org.meshtastic.core.strings.map_cache_info
|
||||
import org.meshtastic.core.strings.map_cache_manager
|
||||
import org.meshtastic.core.strings.map_cache_size
|
||||
import org.meshtastic.core.strings.map_cache_tiles
|
||||
import org.meshtastic.core.strings.map_clear_tiles
|
||||
import org.meshtastic.core.strings.map_download_complete
|
||||
import org.meshtastic.core.strings.map_download_errors
|
||||
import org.meshtastic.core.strings.map_download_region
|
||||
import org.meshtastic.core.strings.map_filter
|
||||
import org.meshtastic.core.strings.map_node_popup_details
|
||||
import org.meshtastic.core.strings.map_offline_manager
|
||||
import org.meshtastic.core.strings.map_purge_fail
|
||||
import org.meshtastic.core.strings.map_purge_success
|
||||
import org.meshtastic.core.strings.map_style_selection
|
||||
import org.meshtastic.core.strings.map_subDescription
|
||||
import org.meshtastic.core.strings.map_tile_source
|
||||
import org.meshtastic.core.strings.only_favorites
|
||||
import org.meshtastic.core.strings.show_precision_circle
|
||||
import org.meshtastic.core.strings.show_waypoints
|
||||
import org.meshtastic.core.strings.toggle_my_position
|
||||
import org.meshtastic.core.strings.waypoint_delete
|
||||
import org.meshtastic.core.strings.you
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.calculating
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.resources.delete_for_everyone
|
||||
import org.meshtastic.core.resources.delete_for_me
|
||||
import org.meshtastic.core.resources.expires
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.location_disabled
|
||||
import org.meshtastic.core.resources.map_cache_info
|
||||
import org.meshtastic.core.resources.map_cache_manager
|
||||
import org.meshtastic.core.resources.map_cache_size
|
||||
import org.meshtastic.core.resources.map_cache_tiles
|
||||
import org.meshtastic.core.resources.map_clear_tiles
|
||||
import org.meshtastic.core.resources.map_download_complete
|
||||
import org.meshtastic.core.resources.map_download_errors
|
||||
import org.meshtastic.core.resources.map_download_region
|
||||
import org.meshtastic.core.resources.map_filter
|
||||
import org.meshtastic.core.resources.map_node_popup_details
|
||||
import org.meshtastic.core.resources.map_offline_manager
|
||||
import org.meshtastic.core.resources.map_purge_fail
|
||||
import org.meshtastic.core.resources.map_purge_success
|
||||
import org.meshtastic.core.resources.map_style_selection
|
||||
import org.meshtastic.core.resources.map_subDescription
|
||||
import org.meshtastic.core.resources.map_tile_source
|
||||
import org.meshtastic.core.resources.only_favorites
|
||||
import org.meshtastic.core.resources.show_precision_circle
|
||||
import org.meshtastic.core.resources.show_waypoints
|
||||
import org.meshtastic.core.resources.toggle_my_position
|
||||
import org.meshtastic.core.resources.waypoint_delete
|
||||
import org.meshtastic.core.resources.you
|
||||
import org.meshtastic.core.ui.component.BasicListItem
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
|
|
@ -281,18 +281,18 @@ fun MapView(
|
|||
scope.launch { context.showToast(Res.string.location_disabled) }
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d { "user clicked MyLocationNewOverlay ${myLocationOverlay == null}" }
|
||||
if (myLocationOverlay == null) {
|
||||
myLocationOverlay =
|
||||
MyLocationNewOverlay(this).apply {
|
||||
enableMyLocation()
|
||||
enableFollowLocation()
|
||||
getBitmapFromVectorDrawable(context, org.meshtastic.core.ui.R.drawable.ic_map_location_dot_24)
|
||||
?.let {
|
||||
setPersonIcon(it)
|
||||
setPersonAnchor(0.5f, 0.5f)
|
||||
}
|
||||
getBitmapFromVectorDrawable(context, org.meshtastic.core.ui.R.drawable.ic_map_navigation_24)?.let {
|
||||
getBitmapFromVectorDrawable(context, R.drawable.ic_map_location_dot)?.let {
|
||||
setPersonIcon(it)
|
||||
setPersonAnchor(0.5f, 0.5f)
|
||||
}
|
||||
getBitmapFromVectorDrawable(context, R.drawable.ic_map_navigation)?.let {
|
||||
setDirectionIcon(it)
|
||||
setDirectionAnchor(0.5f, 0.5f)
|
||||
}
|
||||
|
|
@ -388,9 +388,7 @@ fun MapView(
|
|||
val traceroutePolylines = remember { mutableStateListOf<Polyline>() }
|
||||
var hasCenteredTraceroute by remember(tracerouteOverlay) { mutableStateOf(false) }
|
||||
|
||||
val markerIcon = remember {
|
||||
AppCompatResources.getDrawable(context, org.meshtastic.core.ui.R.drawable.ic_baseline_location_on_24)
|
||||
}
|
||||
val markerIcon = remember { AppCompatResources.getDrawable(context, R.drawable.ic_location_on) }
|
||||
|
||||
fun MapView.onNodesChanged(nodes: Collection<Node>): List<MarkerWithLabel> {
|
||||
val nodesWithPosition = nodes.filter { it.validPosition != null }
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import androidx.compose.ui.unit.Density
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.meshtastic.core.ui.R
|
||||
import org.meshtastic.proto.Position
|
||||
import org.osmdroid.util.GeoPoint
|
||||
import org.osmdroid.views.MapView
|
||||
|
|
@ -125,7 +124,7 @@ fun MapView.addPolyline(density: Density, geoPoints: List<GeoPoint>, onClick: ()
|
|||
}
|
||||
|
||||
fun MapView.addPositionMarkers(positions: List<Position>, onClick: () -> Unit): List<Marker> {
|
||||
val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation_24)
|
||||
val navIcon = ContextCompat.getDrawable(context, R.drawable.ic_map_navigation)
|
||||
val markers =
|
||||
positions.map {
|
||||
Marker(this).apply {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
|
|
@ -36,11 +35,11 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.map_select_download_region
|
||||
import org.meshtastic.core.strings.map_start_download
|
||||
import org.meshtastic.core.strings.map_tile_download_estimate
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.map_select_download_region
|
||||
import org.meshtastic.core.resources.map_start_download
|
||||
import org.meshtastic.core.resources.map_tile_download_estimate
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.map_download_region
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.map_download_region
|
||||
|
||||
@Composable
|
||||
fun DownloadButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
|
|
|
|||
|
|
@ -70,18 +70,18 @@ import org.meshtastic.core.common.util.nowInstant
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.common.util.systemTimeZone
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.date
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.description
|
||||
import org.meshtastic.core.strings.expires
|
||||
import org.meshtastic.core.strings.locked
|
||||
import org.meshtastic.core.strings.name
|
||||
import org.meshtastic.core.strings.send
|
||||
import org.meshtastic.core.strings.time
|
||||
import org.meshtastic.core.strings.waypoint_edit
|
||||
import org.meshtastic.core.strings.waypoint_new
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.date
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.description
|
||||
import org.meshtastic.core.resources.expires
|
||||
import org.meshtastic.core.resources.locked
|
||||
import org.meshtastic.core.resources.name
|
||||
import org.meshtastic.core.resources.send
|
||||
import org.meshtastic.core.resources.time
|
||||
import org.meshtastic.core.resources.waypoint_edit
|
||||
import org.meshtastic.core.resources.waypoint_new
|
||||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -29,8 +28,8 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.map_style_selection
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.map_style_selection
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
14
feature/map/src/fdroid/res/drawable/ic_location_on.xml
Normal file
14
feature/map/src/fdroid/res/drawable/ic_location_on.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="30dp"
|
||||
android:height="30dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#3388ff"
|
||||
android:strokeWidth="1.25"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:pathData="M12,9m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
|
||||
</vector>
|
||||
11
feature/map/src/fdroid/res/drawable/ic_map_location_dot.xml
Normal file
11
feature/map/src/fdroid/res/drawable/ic_map_location_dot.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#3388ff"
|
||||
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"
|
||||
android:strokeWidth="2.0"
|
||||
android:strokeColor="@android:color/white" />
|
||||
</vector>
|
||||
11
feature/map/src/fdroid/res/drawable/ic_map_navigation.xml
Normal file
11
feature/map/src/fdroid/res/drawable/ic_map_navigation.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#3388ff"
|
||||
android:pathData="M12,2L4.5,20.29l0.71,0.71L12,18l6.79,3 0.71,-0.71z"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="@android:color/white" />
|
||||
</vector>
|
||||
|
|
@ -97,16 +97,16 @@ import org.meshtastic.core.model.util.metersIn
|
|||
import org.meshtastic.core.model.util.mpsToKmph
|
||||
import org.meshtastic.core.model.util.mpsToMph
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alt
|
||||
import org.meshtastic.core.strings.heading
|
||||
import org.meshtastic.core.strings.latitude
|
||||
import org.meshtastic.core.strings.longitude
|
||||
import org.meshtastic.core.strings.position
|
||||
import org.meshtastic.core.strings.sats
|
||||
import org.meshtastic.core.strings.speed
|
||||
import org.meshtastic.core.strings.timestamp
|
||||
import org.meshtastic.core.strings.track_point
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.alt
|
||||
import org.meshtastic.core.resources.heading
|
||||
import org.meshtastic.core.resources.latitude
|
||||
import org.meshtastic.core.resources.longitude
|
||||
import org.meshtastic.core.resources.position
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.resources.speed
|
||||
import org.meshtastic.core.resources.timestamp
|
||||
import org.meshtastic.core.resources.track_point
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -31,9 +30,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.nodes_at_this_location
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.nodes_at_this_location
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.feature.map.model.NodeClusterItem
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
|
@ -39,14 +38,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_layer
|
||||
import org.meshtastic.core.strings.hide_layer
|
||||
import org.meshtastic.core.strings.manage_map_layers
|
||||
import org.meshtastic.core.strings.map_layer_formats
|
||||
import org.meshtastic.core.strings.no_map_layers_loaded
|
||||
import org.meshtastic.core.strings.remove_layer
|
||||
import org.meshtastic.core.strings.show_layer
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_layer
|
||||
import org.meshtastic.core.resources.hide_layer
|
||||
import org.meshtastic.core.resources.manage_map_layers
|
||||
import org.meshtastic.core.resources.map_layer_formats
|
||||
import org.meshtastic.core.resources.no_map_layers_loaded
|
||||
import org.meshtastic.core.resources.remove_layer
|
||||
import org.meshtastic.core.resources.show_layer
|
||||
import org.meshtastic.feature.map.MapLayerItem
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
|
|||
|
|
@ -49,21 +49,21 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.data.model.CustomTileProviderConfig
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_custom_tile_source
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.delete_custom_tile_source
|
||||
import org.meshtastic.core.strings.edit_custom_tile_source
|
||||
import org.meshtastic.core.strings.manage_custom_tile_sources
|
||||
import org.meshtastic.core.strings.name
|
||||
import org.meshtastic.core.strings.name_cannot_be_empty
|
||||
import org.meshtastic.core.strings.no_custom_tile_sources_found
|
||||
import org.meshtastic.core.strings.provider_name_exists
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.strings.url_cannot_be_empty
|
||||
import org.meshtastic.core.strings.url_must_contain_placeholders
|
||||
import org.meshtastic.core.strings.url_template
|
||||
import org.meshtastic.core.strings.url_template_hint
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_custom_tile_source
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.delete_custom_tile_source
|
||||
import org.meshtastic.core.resources.edit_custom_tile_source
|
||||
import org.meshtastic.core.resources.manage_custom_tile_sources
|
||||
import org.meshtastic.core.resources.name
|
||||
import org.meshtastic.core.resources.name_cannot_be_empty
|
||||
import org.meshtastic.core.resources.no_custom_tile_sources_found
|
||||
import org.meshtastic.core.resources.provider_name_exists
|
||||
import org.meshtastic.core.resources.save
|
||||
import org.meshtastic.core.resources.url_cannot_be_empty
|
||||
import org.meshtastic.core.resources.url_must_contain_placeholders
|
||||
import org.meshtastic.core.resources.url_template
|
||||
import org.meshtastic.core.resources.url_template_hint
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.map.MapViewModel
|
||||
|
|
|
|||
|
|
@ -71,18 +71,18 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowInstant
|
||||
import org.meshtastic.core.common.util.systemTimeZone
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.date
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.description
|
||||
import org.meshtastic.core.strings.expires
|
||||
import org.meshtastic.core.strings.locked
|
||||
import org.meshtastic.core.strings.name
|
||||
import org.meshtastic.core.strings.send
|
||||
import org.meshtastic.core.strings.time
|
||||
import org.meshtastic.core.strings.waypoint_edit
|
||||
import org.meshtastic.core.strings.waypoint_new
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.date
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.description
|
||||
import org.meshtastic.core.resources.expires
|
||||
import org.meshtastic.core.resources.locked
|
||||
import org.meshtastic.core.resources.name
|
||||
import org.meshtastic.core.resources.send
|
||||
import org.meshtastic.core.resources.time
|
||||
import org.meshtastic.core.resources.waypoint_edit
|
||||
import org.meshtastic.core.resources.waypoint_new
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.manage_map_layers
|
||||
import org.meshtastic.core.strings.map_filter
|
||||
import org.meshtastic.core.strings.map_tile_source
|
||||
import org.meshtastic.core.strings.orient_north
|
||||
import org.meshtastic.core.strings.toggle_my_position
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.manage_map_layers
|
||||
import org.meshtastic.core.resources.map_filter
|
||||
import org.meshtastic.core.resources.map_tile_source
|
||||
import org.meshtastic.core.resources.orient_north
|
||||
import org.meshtastic.core.resources.toggle_my_position
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.feature.map.MapViewModel
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -40,11 +39,11 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.last_heard_filter_label
|
||||
import org.meshtastic.core.strings.only_favorites
|
||||
import org.meshtastic.core.strings.show_precision_circle
|
||||
import org.meshtastic.core.strings.show_waypoints
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.last_heard_filter_label
|
||||
import org.meshtastic.core.resources.only_favorites
|
||||
import org.meshtastic.core.resources.show_precision_circle
|
||||
import org.meshtastic.core.resources.show_waypoints
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
import org.meshtastic.feature.map.MapViewModel
|
||||
import kotlin.math.roundToInt
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -29,13 +28,13 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.maps.android.compose.MapType
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.manage_custom_tile_sources
|
||||
import org.meshtastic.core.strings.map_type_hybrid
|
||||
import org.meshtastic.core.strings.map_type_normal
|
||||
import org.meshtastic.core.strings.map_type_satellite
|
||||
import org.meshtastic.core.strings.map_type_terrain
|
||||
import org.meshtastic.core.strings.selected_map_type
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.manage_custom_tile_sources
|
||||
import org.meshtastic.core.resources.map_type_hybrid
|
||||
import org.meshtastic.core.resources.map_type_normal
|
||||
import org.meshtastic.core.resources.map_type_satellite
|
||||
import org.meshtastic.core.resources.map_type_terrain
|
||||
import org.meshtastic.core.resources.selected_map_type
|
||||
import org.meshtastic.feature.map.MapViewModel
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import com.google.android.gms.maps.model.LatLng
|
|||
import com.google.maps.android.compose.Marker
|
||||
import com.google.maps.android.compose.rememberUpdatedMarkerState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.locked
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.locked
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.map.BaseMapViewModel
|
||||
import org.meshtastic.proto.Waypoint
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.any
|
||||
import org.meshtastic.core.resources.eight_hours
|
||||
import org.meshtastic.core.resources.one_day
|
||||
import org.meshtastic.core.resources.one_hour
|
||||
import org.meshtastic.core.resources.two_days
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.any
|
||||
import org.meshtastic.core.strings.eight_hours
|
||||
import org.meshtastic.core.strings.one_day
|
||||
import org.meshtastic.core.strings.one_hour
|
||||
import org.meshtastic.core.strings.two_days
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.proto.Position
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 org.meshtastic.feature.map
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -26,8 +25,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.map
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.map
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ graph TB
|
|||
:feature:messaging -.-> :core:prefs
|
||||
:feature:messaging -.-> :core:proto
|
||||
:feature:messaging -.-> :core:service
|
||||
:feature:messaging -.-> :core:strings
|
||||
:feature:messaging -.-> :core:resources
|
||||
:feature:messaging -.-> :core:ui
|
||||
|
||||
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ dependencies {
|
|||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
|
|
@ -64,4 +64,6 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.test.ext.junit)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ import androidx.compose.ui.unit.dp
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.strings.relays
|
||||
import org.meshtastic.core.strings.resend
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.resources.relays
|
||||
import org.meshtastic.core.resources.resend
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
|
|
|
|||
|
|
@ -105,32 +105,32 @@ import org.meshtastic.core.database.model.Message
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.getChannel
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alert_bell_text
|
||||
import org.meshtastic.core.strings.cancel_reply
|
||||
import org.meshtastic.core.strings.clear_selection
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.delete_messages
|
||||
import org.meshtastic.core.strings.delete_messages_title
|
||||
import org.meshtastic.core.strings.filter_disable_for_contact
|
||||
import org.meshtastic.core.strings.filter_enable_for_contact
|
||||
import org.meshtastic.core.strings.filter_hide_count
|
||||
import org.meshtastic.core.strings.filter_show_count
|
||||
import org.meshtastic.core.strings.message_input_label
|
||||
import org.meshtastic.core.strings.navigate_back
|
||||
import org.meshtastic.core.strings.overflow_menu
|
||||
import org.meshtastic.core.strings.quick_chat
|
||||
import org.meshtastic.core.strings.quick_chat_hide
|
||||
import org.meshtastic.core.strings.quick_chat_show
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.strings.replying_to
|
||||
import org.meshtastic.core.strings.scroll_to_bottom
|
||||
import org.meshtastic.core.strings.select_all
|
||||
import org.meshtastic.core.strings.send
|
||||
import org.meshtastic.core.strings.type_a_message
|
||||
import org.meshtastic.core.strings.unknown
|
||||
import org.meshtastic.core.strings.unknown_channel
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.alert_bell_text
|
||||
import org.meshtastic.core.resources.cancel_reply
|
||||
import org.meshtastic.core.resources.clear_selection
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.delete_messages
|
||||
import org.meshtastic.core.resources.delete_messages_title
|
||||
import org.meshtastic.core.resources.filter_disable_for_contact
|
||||
import org.meshtastic.core.resources.filter_enable_for_contact
|
||||
import org.meshtastic.core.resources.filter_hide_count
|
||||
import org.meshtastic.core.resources.filter_show_count
|
||||
import org.meshtastic.core.resources.message_input_label
|
||||
import org.meshtastic.core.resources.navigate_back
|
||||
import org.meshtastic.core.resources.overflow_menu
|
||||
import org.meshtastic.core.resources.quick_chat
|
||||
import org.meshtastic.core.resources.quick_chat_hide
|
||||
import org.meshtastic.core.resources.quick_chat_show
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.resources.replying_to
|
||||
import org.meshtastic.core.resources.scroll_to_bottom
|
||||
import org.meshtastic.core.resources.select_all
|
||||
import org.meshtastic.core.resources.send
|
||||
import org.meshtastic.core.resources.type_a_message
|
||||
import org.meshtastic.core.resources.unknown
|
||||
import org.meshtastic.core.resources.unknown_channel
|
||||
import org.meshtastic.core.ui.component.MeshtasticTextDialog
|
||||
import org.meshtastic.core.ui.component.NodeKeyStatusIcon
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
|
|
@ -305,11 +305,7 @@ fun MessageScreen(
|
|||
val originalMessage by
|
||||
remember(replyingToPacketId, pagedMessages.itemCount) {
|
||||
derivedStateOf {
|
||||
replyingToPacketId?.let { id ->
|
||||
(0 until pagedMessages.itemCount).firstNotNullOfOrNull { index ->
|
||||
pagedMessages[index]?.takeIf { it.packetId == id }
|
||||
}
|
||||
}
|
||||
replyingToPacketId?.let { id -> pagedMessages.itemSnapshotList.firstOrNull { it?.packetId == id } }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ package org.meshtastic.feature.messaging
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
|
|
@ -53,6 +53,7 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
|
@ -65,8 +66,8 @@ import org.meshtastic.core.database.entity.Reaction
|
|||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.new_messages_below
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.new_messages_below
|
||||
import org.meshtastic.feature.messaging.component.MessageItem
|
||||
import org.meshtastic.feature.messaging.component.ReactionDialog
|
||||
|
||||
|
|
@ -192,13 +193,13 @@ private fun MessageListPagedContent(
|
|||
modifier: Modifier = Modifier,
|
||||
quickEmojis: List<String>,
|
||||
) {
|
||||
// Calculate unread divider position
|
||||
// Calculate unread divider position using snapshot to avoid side-effects and improve performance
|
||||
// Optimized: Use full snapshot index to correctly match LazyColumn index range
|
||||
val unreadDividerIndex by
|
||||
remember(state.messages.itemCount, state.firstUnreadMessageUuid) {
|
||||
derivedStateOf {
|
||||
state.firstUnreadMessageUuid?.let { uuid ->
|
||||
(0 until state.messages.itemCount).firstOrNull { index -> state.messages[index]?.uuid == uuid }
|
||||
}
|
||||
val uuid = state.firstUnreadMessageUuid ?: return@derivedStateOf null
|
||||
state.messages.itemSnapshotList.indexOfFirst { it?.uuid == uuid }.takeIf { it != -1 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +213,11 @@ private fun MessageListPagedContent(
|
|||
reverseLayout = true,
|
||||
contentPadding = PaddingValues(bottom = 24.dp),
|
||||
) {
|
||||
items(count = state.messages.itemCount, key = state.messages.itemKey { it.uuid }) { index ->
|
||||
items(
|
||||
count = state.messages.itemCount,
|
||||
key = state.messages.itemKey { it.uuid },
|
||||
contentType = state.messages.itemContentType { "message" },
|
||||
) { index ->
|
||||
val message = state.messages[index]
|
||||
val visuallyPrevMessage = if (index < state.messages.itemCount - 1) state.messages[index + 1] else null
|
||||
val visuallyNextMessage = if (index > 0) state.messages[index - 1] else null
|
||||
|
|
@ -234,27 +239,49 @@ private fun MessageListPagedContent(
|
|||
}
|
||||
|
||||
if (message != null) {
|
||||
renderPagedChatMessageRow(
|
||||
message = message,
|
||||
state = state,
|
||||
nodeMap = nodeMap,
|
||||
handlers = handlers,
|
||||
inSelectionMode = inSelectionMode,
|
||||
coroutineScope = coroutineScope,
|
||||
haptics = haptics,
|
||||
listState = listState,
|
||||
onShowStatusDialog = onShowStatusDialog,
|
||||
onShowReactions = onShowReactions,
|
||||
enableAnimations = enableAnimations,
|
||||
showUserName = !hasSamePrev,
|
||||
hasSamePrev = hasSamePrev,
|
||||
hasSameNext = hasSameNext,
|
||||
quickEmojis = quickEmojis,
|
||||
)
|
||||
val isFirstUnread = state.hasUnreadMessages && unreadDividerIndex == index
|
||||
val itemModifier = if (enableAnimations) Modifier.animateItem() else Modifier
|
||||
|
||||
// Show unread divider after the first unread message
|
||||
if (state.hasUnreadMessages && unreadDividerIndex == index) {
|
||||
UnreadMessagesDivider(modifier = if (enableAnimations) Modifier.animateItem() else Modifier)
|
||||
if (isFirstUnread) {
|
||||
// Wrap in Column to prevent overlapping of divider and message item
|
||||
// Apply animation to the container Column once
|
||||
Column(modifier = itemModifier) {
|
||||
UnreadMessagesDivider()
|
||||
RenderPagedChatMessageRow(
|
||||
message = message,
|
||||
state = state,
|
||||
nodeMap = nodeMap,
|
||||
handlers = handlers,
|
||||
inSelectionMode = inSelectionMode,
|
||||
coroutineScope = coroutineScope,
|
||||
haptics = haptics,
|
||||
listState = listState,
|
||||
onShowStatusDialog = onShowStatusDialog,
|
||||
onShowReactions = onShowReactions,
|
||||
showUserName = !hasSamePrev,
|
||||
hasSamePrev = hasSamePrev,
|
||||
hasSameNext = hasSameNext,
|
||||
quickEmojis = quickEmojis,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RenderPagedChatMessageRow(
|
||||
message = message,
|
||||
state = state,
|
||||
nodeMap = nodeMap,
|
||||
handlers = handlers,
|
||||
inSelectionMode = inSelectionMode,
|
||||
coroutineScope = coroutineScope,
|
||||
haptics = haptics,
|
||||
listState = listState,
|
||||
onShowStatusDialog = onShowStatusDialog,
|
||||
onShowReactions = onShowReactions,
|
||||
modifier = itemModifier,
|
||||
showUserName = !hasSamePrev,
|
||||
hasSamePrev = hasSamePrev,
|
||||
hasSameNext = hasSameNext,
|
||||
quickEmojis = quickEmojis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -263,7 +290,7 @@ private fun MessageListPagedContent(
|
|||
state.messages.apply {
|
||||
when {
|
||||
loadState.append is LoadState.Loading -> {
|
||||
item(key = "append_loading") {
|
||||
item(key = "append_loading", contentType = "loading") {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -280,7 +307,7 @@ private fun MessageListPagedContent(
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun LazyItemScope.renderPagedChatMessageRow(
|
||||
private fun RenderPagedChatMessageRow(
|
||||
message: Message,
|
||||
state: MessageListPagedState,
|
||||
nodeMap: Map<Int, Node>,
|
||||
|
|
@ -291,7 +318,7 @@ private fun LazyItemScope.renderPagedChatMessageRow(
|
|||
listState: LazyListState,
|
||||
onShowStatusDialog: (Message) -> Unit,
|
||||
onShowReactions: (List<Reaction>) -> Unit,
|
||||
enableAnimations: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
showUserName: Boolean,
|
||||
hasSamePrev: Boolean,
|
||||
hasSameNext: Boolean,
|
||||
|
|
@ -305,7 +332,7 @@ private fun LazyItemScope.renderPagedChatMessageRow(
|
|||
val node = nodeMap[message.node.num] ?: message.node
|
||||
|
||||
MessageItem(
|
||||
modifier = if (enableAnimations) Modifier.animateItem() else Modifier,
|
||||
modifier = modifier,
|
||||
node = node,
|
||||
ourNode = ourNode,
|
||||
message = message,
|
||||
|
|
@ -341,12 +368,10 @@ private fun LazyItemScope.renderPagedChatMessageRow(
|
|||
onNavigateToOriginalMessage = {
|
||||
coroutineScope.launch {
|
||||
// Note: With pagination, we can't guarantee the original message is loaded
|
||||
// This is a limitation of pagination - we would need to implement
|
||||
// a search/jump feature to load and scroll to specific messages
|
||||
// Optimized: Use snapshot to find index to avoid side-effects during search
|
||||
val targetIndex =
|
||||
(0 until state.messages.itemCount).firstOrNull { index ->
|
||||
state.messages[index]?.packetId == message.replyId
|
||||
}
|
||||
state.messages.itemSnapshotList.indexOfFirst { it?.packetId == message.replyId }.takeIf { it != -1 }
|
||||
|
||||
if (targetIndex != null) {
|
||||
listState.animateScrollToItem(index = targetIndex)
|
||||
}
|
||||
|
|
@ -408,19 +433,23 @@ private fun AutoScrollToBottomPaged(
|
|||
}
|
||||
|
||||
private fun findFirstVisibleUnreadMessage(messages: LazyPagingItems<Message>, visibleIndex: Int): Message? {
|
||||
val snapshot = messages.itemSnapshotList
|
||||
if (visibleIndex >= snapshot.size) return null
|
||||
val firstVisibleUnreadIndex =
|
||||
(visibleIndex until messages.itemCount).firstOrNull { i ->
|
||||
val msg = messages[i]
|
||||
(visibleIndex until snapshot.size).firstOrNull { i ->
|
||||
val msg = snapshot[i]
|
||||
msg != null && !msg.read && !msg.fromLocal
|
||||
}
|
||||
return firstVisibleUnreadIndex?.let { messages[it] }
|
||||
return firstVisibleUnreadIndex?.let { snapshot[it] }
|
||||
}
|
||||
|
||||
private fun findLastUnreadMessageIndex(messages: LazyPagingItems<Message>): Int? =
|
||||
(0 until messages.itemCount).lastOrNull { i ->
|
||||
val msg = messages[i]
|
||||
private fun findLastUnreadMessageIndex(messages: LazyPagingItems<Message>): Int? {
|
||||
val snapshot = messages.itemSnapshotList
|
||||
return (0 until snapshot.size).lastOrNull { i ->
|
||||
val msg = snapshot[i]
|
||||
msg != null && !msg.read && !msg.fromLocal
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
|
|
@ -503,7 +532,7 @@ internal fun UnreadMessagesDivider(modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun MessageStatusDialog(
|
||||
private fun MessageStatusDialog(
|
||||
message: Message,
|
||||
nodes: List<Node>,
|
||||
ourNode: Node?,
|
||||
|
|
|
|||
|
|
@ -16,13 +16,11 @@
|
|||
*/
|
||||
package org.meshtastic.feature.messaging
|
||||
|
||||
import android.os.RemoteException
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -41,7 +39,6 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
|
|||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
|
|
@ -50,9 +47,8 @@ import org.meshtastic.core.service.MeshServiceNotifications
|
|||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.messaging.domain.usecase.SendMessageUseCase
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config.DeviceConfig.Role
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
|
|
@ -70,6 +66,7 @@ constructor(
|
|||
private val customEmojiPrefs: CustomEmojiPrefs,
|
||||
private val homoglyphEncodingPrefs: HomoglyphPrefs,
|
||||
private val meshServiceNotifications: MeshServiceNotifications,
|
||||
private val sendMessageUseCase: SendMessageUseCase,
|
||||
) : ViewModel() {
|
||||
private val _title = MutableStateFlow("")
|
||||
val title: StateFlow<String> = _title.asStateFlow()
|
||||
|
|
@ -194,46 +191,8 @@ constructor(
|
|||
* broadcasting on channel 0.
|
||||
* @param replyId The ID of the message this is a reply to, if any.
|
||||
*/
|
||||
@Suppress("NestedBlockDepth")
|
||||
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}", replyId: Int? = null) {
|
||||
// contactKey: unique contact key filter (channel)+(nodeId)
|
||||
val channel = contactKey[0].digitToIntOrNull()
|
||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||
|
||||
// if the destination is a node, we need to ensure it's a
|
||||
// favorite so it does not get removed from the on-device node database.
|
||||
if (channel == null) { // no channel specified, so we assume it's a direct message
|
||||
val fwVersion = ourNodeInfo.value?.metadata?.firmware_version
|
||||
val destNode = nodeRepository.getNode(dest)
|
||||
val isClientBase = ourNodeInfo.value?.user?.role == Role.CLIENT_BASE
|
||||
|
||||
val capabilities = Capabilities(fwVersion)
|
||||
|
||||
if (capabilities.canSendVerifiedContacts) {
|
||||
sendSharedContact(destNode)
|
||||
} else {
|
||||
if (!destNode.isFavorite && !isClientBase) {
|
||||
favoriteNode(destNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Applying homoglyph encoding to the transmitted string if user has activated the feature
|
||||
// In most cases the value in "str" parameter will already contain the correct
|
||||
// transformed string from the text input. This call here added to make sure that
|
||||
// the feature is effective across all possible message paths (quick-chat, reply, etc.)
|
||||
val dataPacketText: String =
|
||||
if (homoglyphEncodingPrefs.homoglyphEncodingEnabled) {
|
||||
HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(str)
|
||||
} else {
|
||||
str
|
||||
}
|
||||
|
||||
val p =
|
||||
DataPacket(dest, channel ?: 0, dataPacketText, replyId).apply {
|
||||
from = ourNodeInfo.value?.user?.id ?: DataPacket.ID_LOCAL
|
||||
}
|
||||
sendDataPacket(p)
|
||||
viewModelScope.launch { sendMessageUseCase.invoke(str, contactKey, replyId) }
|
||||
}
|
||||
|
||||
fun sendReaction(emoji: String, replyId: Int, contactKey: String) =
|
||||
|
|
@ -253,30 +212,4 @@ constructor(
|
|||
val unreadCount = packetRepository.getUnreadCount(contact)
|
||||
if (unreadCount == 0) meshServiceNotifications.cancelMessageNotification(contact)
|
||||
}
|
||||
|
||||
private fun favoriteNode(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Favorite node error" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSharedContact(node: Node) = viewModelScope.launch {
|
||||
try {
|
||||
val contact =
|
||||
SharedContact(node_num = node.num, user = node.user, manually_verified = node.manuallyVerified)
|
||||
serviceRepository.onServiceAction(ServiceAction.SendContact(contact = contact))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Send shared contact error" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendDataPacket(p: DataPacket) {
|
||||
try {
|
||||
serviceRepository.meshService?.send(p)
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e { "Send DataPacket error: ${ex.message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,18 +65,18 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add
|
||||
import org.meshtastic.core.strings.cancel
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.message
|
||||
import org.meshtastic.core.strings.name
|
||||
import org.meshtastic.core.strings.quick_chat
|
||||
import org.meshtastic.core.strings.quick_chat_append
|
||||
import org.meshtastic.core.strings.quick_chat_edit
|
||||
import org.meshtastic.core.strings.quick_chat_instant
|
||||
import org.meshtastic.core.strings.quick_chat_new
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.message
|
||||
import org.meshtastic.core.resources.name
|
||||
import org.meshtastic.core.resources.quick_chat
|
||||
import org.meshtastic.core.resources.quick_chat_append
|
||||
import org.meshtastic.core.resources.quick_chat_edit
|
||||
import org.meshtastic.core.resources.quick_chat_instant
|
||||
import org.meshtastic.core.resources.quick_chat_new
|
||||
import org.meshtastic.core.resources.save
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.component.dragContainer
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.message_delivery_status
|
||||
import org.meshtastic.core.strings.react
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.react
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -47,12 +47,12 @@ import androidx.compose.ui.unit.sp
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.message_delivery_status
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.strings.select
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.resources.select
|
||||
|
||||
@Composable
|
||||
fun MessageActionsContent(
|
||||
|
|
|
|||
|
|
@ -66,11 +66,11 @@ import org.meshtastic.core.database.entity.Reaction
|
|||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.filter_message_label
|
||||
import org.meshtastic.core.strings.message_delivery_status
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.strings.sample_message
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.filter_message_label
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.reply
|
||||
import org.meshtastic.core.resources.sample_message
|
||||
import org.meshtastic.core.ui.component.AutoLinkText
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ import org.meshtastic.core.database.model.getStringResFrom
|
|||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.delivery_confirmed
|
||||
import org.meshtastic.core.strings.error
|
||||
import org.meshtastic.core.strings.message_delivery_status
|
||||
import org.meshtastic.core.strings.message_status_enroute
|
||||
import org.meshtastic.core.strings.message_status_queued
|
||||
import org.meshtastic.core.strings.react
|
||||
import org.meshtastic.core.strings.you
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.delivery_confirmed
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.message_status_enroute
|
||||
import org.meshtastic.core.resources.message_status_queued
|
||||
import org.meshtastic.core.resources.react
|
||||
import org.meshtastic.core.resources.you
|
||||
import org.meshtastic.core.ui.component.BottomSheetDialog
|
||||
import org.meshtastic.core.ui.component.Rssi
|
||||
import org.meshtastic.core.ui.component.Snr
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.feature.messaging.domain.usecase
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.feature.messaging.HomoglyphCharacterStringTransformer
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
class SendMessageUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val homoglyphEncodingPrefs: HomoglyphPrefs,
|
||||
) {
|
||||
|
||||
@Suppress("NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod")
|
||||
suspend operator fun invoke(
|
||||
text: String,
|
||||
contactKey: String = "0${DataPacket.ID_BROADCAST}",
|
||||
replyId: Int? = null,
|
||||
) {
|
||||
val channel = contactKey[0].digitToIntOrNull()
|
||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||
|
||||
val ourNode = nodeRepository.ourNodeInfo.value
|
||||
val fromId = ourNode?.user?.id ?: DataPacket.ID_LOCAL
|
||||
|
||||
// logic for direct messages
|
||||
if (channel == null) {
|
||||
val destNode = nodeRepository.getNode(dest)
|
||||
val fwVersion = ourNode?.metadata?.firmware_version
|
||||
val isClientBase = ourNode?.user?.role == Config.DeviceConfig.Role.CLIENT_BASE
|
||||
val capabilities = Capabilities(fwVersion)
|
||||
|
||||
if (capabilities.canSendVerifiedContacts) {
|
||||
sendSharedContact(destNode)
|
||||
} else {
|
||||
if (!destNode.isFavorite && !isClientBase) {
|
||||
favoriteNode(destNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply homoglyph encoding
|
||||
val finalMessageText =
|
||||
if (homoglyphEncodingPrefs.homoglyphEncodingEnabled) {
|
||||
HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(text)
|
||||
} else {
|
||||
text
|
||||
}
|
||||
|
||||
val packet = DataPacket(dest, channel ?: 0, finalMessageText, replyId).apply { from = fromId }
|
||||
|
||||
try {
|
||||
serviceRepository.meshService?.send(packet)
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "Failed to send data packet" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun favoriteNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "Favorite node error" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendSharedContact(node: Node) {
|
||||
try {
|
||||
val contact =
|
||||
SharedContact(node_num = node.num, user = node.user, manually_verified = node.manuallyVerified)
|
||||
serviceRepository.onServiceAction(ServiceAction.SendContact(contact = contact))
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "Send shared contact error" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.feature.messaging.domain.usecase
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
|
||||
class SendMessageUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var serviceRepository: ServiceRepository
|
||||
private lateinit var homoglyphEncodingPrefs: HomoglyphPrefs
|
||||
private lateinit var useCase: SendMessageUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeRepository = mockk(relaxed = true)
|
||||
serviceRepository = mockk(relaxed = true)
|
||||
homoglyphEncodingPrefs = mockk(relaxed = true)
|
||||
|
||||
useCase =
|
||||
SendMessageUseCase(
|
||||
nodeRepository = nodeRepository,
|
||||
serviceRepository = serviceRepository,
|
||||
homoglyphEncodingPrefs = homoglyphEncodingPrefs,
|
||||
)
|
||||
|
||||
mockkConstructor(Capabilities::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with broadcast message simply sends data packet`() = runTest {
|
||||
// Arrange
|
||||
val ourNode = mockk<Node>(relaxed = true)
|
||||
every { ourNode.user.id } returns "!1234"
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false
|
||||
|
||||
// Act
|
||||
useCase("Hello broadcast", "0${DataPacket.ID_BROADCAST}", null)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { serviceRepository.onServiceAction(any()) }
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with direct message to older firmware triggers favoriteNode`() = runTest {
|
||||
// Arrange
|
||||
val ourNode = mockk<Node>(relaxed = true)
|
||||
val metadata = mockk<DeviceMetadata>(relaxed = true)
|
||||
every { ourNode.user.id } returns "!local"
|
||||
every { ourNode.user.role } returns Config.DeviceConfig.Role.CLIENT
|
||||
every { ourNode.metadata } returns metadata
|
||||
every { metadata.firmware_version } returns "2.0.0" // Older firmware
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
|
||||
val destNode = mockk<Node>(relaxed = true)
|
||||
every { destNode.isFavorite } returns false
|
||||
every { nodeRepository.getNode("!dest") } returns destNode
|
||||
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false
|
||||
|
||||
every { anyConstructed<Capabilities>().canSendVerifiedContacts } returns false
|
||||
|
||||
// Act
|
||||
useCase("Direct message", "!dest", null)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { serviceRepository.onServiceAction(match { it is ServiceAction.Favorite }) }
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with direct message to new firmware triggers sendSharedContact`() = runTest {
|
||||
// Arrange
|
||||
val ourNode = mockk<Node>(relaxed = true)
|
||||
val metadata = mockk<DeviceMetadata>(relaxed = true)
|
||||
every { ourNode.user.id } returns "!local"
|
||||
every { ourNode.user.role } returns Config.DeviceConfig.Role.CLIENT
|
||||
every { ourNode.metadata } returns metadata
|
||||
every { metadata.firmware_version } returns "2.7.12" // Newer firmware
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
|
||||
val destNode = mockk<Node>(relaxed = true)
|
||||
every { nodeRepository.getNode("!dest") } returns destNode
|
||||
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false
|
||||
|
||||
every { anyConstructed<Capabilities>().canSendVerifiedContacts } returns true
|
||||
|
||||
// Act
|
||||
useCase("Direct message", "!dest", null)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { serviceRepository.onServiceAction(match { it is ServiceAction.SendContact }) }
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with homoglyph enabled transforms text`() = runTest {
|
||||
// Arrange
|
||||
val ourNode = mockk<Node>(relaxed = true)
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns true
|
||||
|
||||
// Let's use a cyrillic character 'A' (U+0410) that will be mapped to Latin 'A'
|
||||
val originalText = "\u0410pple"
|
||||
|
||||
// Act
|
||||
useCase(originalText, "0${DataPacket.ID_BROADCAST}", null)
|
||||
|
||||
// Assert
|
||||
// We verify that send was called with the transformed text (Latin 'A'pple)
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(match { it.text?.contains("Apple") == true }) }
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ graph TB
|
|||
:feature:node -.-> :core:model
|
||||
:feature:node -.-> :core:proto
|
||||
:feature:node -.-> :core:service
|
||||
:feature:node -.-> :core:strings
|
||||
:feature:node -.-> :core:resources
|
||||
:feature:node -.-> :core:ui
|
||||
:feature:node -.-> :core:navigation
|
||||
:feature:node -.-> :feature:map
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ dependencies {
|
|||
implementation(projects.core.model)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.strings)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.feature.map)
|
||||
|
|
|
|||
|
|
@ -56,15 +56,15 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.actions
|
||||
import org.meshtastic.core.strings.direct_message
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.mute_always
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.share_contact
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.actions
|
||||
import org.meshtastic.core.resources.direct_message
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.share_contact
|
||||
import org.meshtastic.core.resources.unmute
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.SwitchListItem
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
|
|
|||
|
|
@ -32,16 +32,16 @@ import org.meshtastic.core.database.entity.asDeviceVersion
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.administration
|
||||
import org.meshtastic.core.resources.firmware
|
||||
import org.meshtastic.core.resources.firmware_edition
|
||||
import org.meshtastic.core.resources.installed_firmware_version
|
||||
import org.meshtastic.core.resources.latest_alpha_firmware
|
||||
import org.meshtastic.core.resources.latest_stable_firmware
|
||||
import org.meshtastic.core.resources.remote_admin
|
||||
import org.meshtastic.core.resources.request_metadata
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.administration
|
||||
import org.meshtastic.core.strings.firmware
|
||||
import org.meshtastic.core.strings.firmware_edition
|
||||
import org.meshtastic.core.strings.installed_firmware_version
|
||||
import org.meshtastic.core.strings.latest_alpha_firmware
|
||||
import org.meshtastic.core.strings.latest_stable_firmware
|
||||
import org.meshtastic.core.strings.remote_admin
|
||||
import org.meshtastic.core.strings.request_metadata
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
|
||||
|
|
|
|||
|
|
@ -56,20 +56,20 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.compass_bearing
|
||||
import org.meshtastic.core.strings.compass_bearing_na
|
||||
import org.meshtastic.core.strings.compass_distance
|
||||
import org.meshtastic.core.strings.compass_location_disabled
|
||||
import org.meshtastic.core.strings.compass_no_location_fix
|
||||
import org.meshtastic.core.strings.compass_no_location_permission
|
||||
import org.meshtastic.core.strings.compass_no_magnetometer
|
||||
import org.meshtastic.core.strings.compass_title
|
||||
import org.meshtastic.core.strings.compass_uncertainty
|
||||
import org.meshtastic.core.strings.compass_uncertainty_unknown
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.strings.exchange_position
|
||||
import org.meshtastic.core.strings.last_position_update
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.compass_bearing
|
||||
import org.meshtastic.core.resources.compass_bearing_na
|
||||
import org.meshtastic.core.resources.compass_distance
|
||||
import org.meshtastic.core.resources.compass_location_disabled
|
||||
import org.meshtastic.core.resources.compass_no_location_fix
|
||||
import org.meshtastic.core.resources.compass_no_location_permission
|
||||
import org.meshtastic.core.resources.compass_no_magnetometer
|
||||
import org.meshtastic.core.resources.compass_title
|
||||
import org.meshtastic.core.resources.compass_uncertainty
|
||||
import org.meshtastic.core.resources.compass_uncertainty_unknown
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.exchange_position
|
||||
import org.meshtastic.core.resources.last_position_update
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.feature.node.compass.CompassUiState
|
||||
import org.meshtastic.feature.node.compass.CompassWarning
|
||||
|
|
|
|||
|
|
@ -47,14 +47,14 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.actions
|
||||
import org.meshtastic.core.strings.direct_message
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.share_contact
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.actions
|
||||
import org.meshtastic.core.resources.direct_message
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.mute_notifications
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.share_contact
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.SwitchListItem
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
|
|
|
|||
|
|
@ -36,21 +36,20 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.device
|
||||
import org.meshtastic.core.strings.hardware
|
||||
import org.meshtastic.core.strings.supported
|
||||
import org.meshtastic.core.strings.supported_by_community
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.device
|
||||
import org.meshtastic.core.resources.hardware
|
||||
import org.meshtastic.core.resources.ic_unverified
|
||||
import org.meshtastic.core.resources.img_hw_unknown
|
||||
import org.meshtastic.core.resources.supported
|
||||
import org.meshtastic.core.resources.supported_by_community
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
|
|
@ -119,7 +118,7 @@ private fun SupportStatusItem(isSupported: Boolean) {
|
|||
if (isSupported) {
|
||||
Icons.TwoTone.Verified
|
||||
} else {
|
||||
ImageVector.vectorResource(org.meshtastic.feature.node.R.drawable.unverified)
|
||||
org.jetbrains.compose.resources.vectorResource(org.meshtastic.core.resources.Res.drawable.ic_unverified)
|
||||
},
|
||||
leadingIconTint = if (isSupported) colorScheme.StatusGreen else colorScheme.StatusRed,
|
||||
trailingIcon = null,
|
||||
|
|
@ -134,9 +133,12 @@ private fun DeviceHardwareImage(deviceHardware: DeviceHardware, modifier: Modifi
|
|||
model = ImageRequest.Builder(LocalContext.current).data(imageUrl).build(),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = deviceHardware.displayName,
|
||||
placeholder = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
error = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
fallback = painterResource(org.meshtastic.feature.node.R.drawable.hw_unknown),
|
||||
placeholder =
|
||||
org.jetbrains.compose.resources.painterResource(org.meshtastic.core.resources.Res.drawable.img_hw_unknown),
|
||||
error =
|
||||
org.jetbrains.compose.resources.painterResource(org.meshtastic.core.resources.Res.drawable.img_hw_unknown),
|
||||
fallback =
|
||||
org.jetbrains.compose.resources.painterResource(org.meshtastic.core.resources.Res.drawable.img_hw_unknown),
|
||||
modifier = modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.distance
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.distance
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.altitude
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.altitude
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.ui.icon.Elevation
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
|
|||
|
|
@ -40,23 +40,27 @@ import org.meshtastic.core.model.util.UnitConversions
|
|||
import org.meshtastic.core.model.util.UnitConversions.toTempString
|
||||
import org.meshtastic.core.model.util.toSmallDistanceString
|
||||
import org.meshtastic.core.model.util.toSpeedString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.dew_point
|
||||
import org.meshtastic.core.strings.distance
|
||||
import org.meshtastic.core.strings.gas_resistance
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.lux
|
||||
import org.meshtastic.core.strings.pressure
|
||||
import org.meshtastic.core.strings.radiation
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.strings.uv_lux
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.strings.weight
|
||||
import org.meshtastic.core.strings.wind
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.dew_point
|
||||
import org.meshtastic.core.resources.distance
|
||||
import org.meshtastic.core.resources.gas_resistance
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.ic_dew_point
|
||||
import org.meshtastic.core.resources.ic_radioactive
|
||||
import org.meshtastic.core.resources.ic_soil_moisture
|
||||
import org.meshtastic.core.resources.ic_soil_temperature
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.pressure
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
import org.meshtastic.core.resources.uv_lux
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.resources.weight
|
||||
import org.meshtastic.core.resources.wind
|
||||
import org.meshtastic.feature.node.model.DrawableMetricInfo
|
||||
import org.meshtastic.feature.node.model.VectorMetricInfo
|
||||
import org.meshtastic.proto.Config
|
||||
|
|
@ -136,7 +140,7 @@ internal fun EnvironmentMetrics(
|
|||
DrawableMetricInfo(
|
||||
Res.string.dew_point,
|
||||
dewPoint.toTempString(isFahrenheit),
|
||||
org.meshtastic.feature.node.R.drawable.ic_outlined_dew_point_24,
|
||||
Res.drawable.ic_dew_point,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -147,7 +151,7 @@ internal fun EnvironmentMetrics(
|
|||
DrawableMetricInfo(
|
||||
Res.string.soil_temperature,
|
||||
st.toTempString(isFahrenheit),
|
||||
org.meshtastic.feature.node.R.drawable.soil_temperature,
|
||||
Res.drawable.ic_soil_temperature,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -157,16 +161,16 @@ internal fun EnvironmentMetrics(
|
|||
DrawableMetricInfo(
|
||||
Res.string.soil_moisture,
|
||||
"%d%%".format(sm),
|
||||
org.meshtastic.feature.node.R.drawable.soil_moisture,
|
||||
Res.drawable.ic_soil_moisture,
|
||||
),
|
||||
)
|
||||
}
|
||||
radiation?.let { r ->
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
Res.string.radiation,
|
||||
"%.1f µR/h".format(r),
|
||||
org.meshtastic.feature.node.R.drawable.ic_filled_radioactive_24,
|
||||
label = Res.string.radiation,
|
||||
value = "%.1f µR/h".format(r),
|
||||
icon = Res.drawable.ic_radioactive,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ import com.mikepenz.markdown.m3.Markdown
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.download
|
||||
import org.meshtastic.core.strings.error_no_app_to_handle_link
|
||||
import org.meshtastic.core.strings.view_release
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.download
|
||||
import org.meshtastic.core.resources.error_no_app_to_handle_link
|
||||
import org.meshtastic.core.resources.view_release
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.hops_away
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.hops_away
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package org.meshtastic.feature.node.component
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -42,15 +41,16 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.Clipboard
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.ui.util.thenIf
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalFoundationApi::class)
|
||||
|
|
@ -58,9 +58,9 @@ import org.meshtastic.core.ui.util.thenIf
|
|||
fun InfoCard(
|
||||
text: String,
|
||||
value: String,
|
||||
icon: ImageVector? = null,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector? = null,
|
||||
iconRes: DrawableResource? = null,
|
||||
rotateIcon: Float = 0f,
|
||||
) {
|
||||
val clipboard: Clipboard = LocalClipboard.current
|
||||
|
|
@ -120,6 +120,6 @@ fun InfoCard(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun DrawableInfoCard(@DrawableRes iconRes: Int, text: String, value: String, rotateIcon: Float = 0f) {
|
||||
internal fun DrawableInfoCard(iconRes: DrawableResource, text: String, value: String, rotateIcon: Float = 0f) {
|
||||
InfoCard(iconRes = iconRes, text = text, value = value, rotateIcon = rotateIcon)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.resources.vectorResource
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.R
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ic_antenna
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ fun LastHeardInfo(
|
|||
) {
|
||||
IconInfo(
|
||||
modifier = modifier,
|
||||
icon = ImageVector.vectorResource(id = R.drawable.ic_antenna_24),
|
||||
icon = vectorResource(Res.drawable.ic_antenna),
|
||||
contentDescription = stringResource(Res.string.node_sort_last_heard),
|
||||
label = if (showLabel) stringResource(Res.string.node_sort_last_heard) else null,
|
||||
text = formatAgo(lastHeard),
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.model.util.GPSFormat
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.strings.last_position_update
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.last_position_update
|
||||
import org.meshtastic.core.ui.component.BasicListItem
|
||||
import org.meshtastic.core.ui.component.icon
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ import androidx.compose.ui.unit.dp
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
|
||||
@Composable
|
||||
internal fun SectionCard(
|
||||
|
|
|
|||
|
|
@ -58,26 +58,26 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.copy
|
||||
import org.meshtastic.core.strings.details
|
||||
import org.meshtastic.core.strings.encryption_error
|
||||
import org.meshtastic.core.strings.encryption_error_text
|
||||
import org.meshtastic.core.strings.error
|
||||
import org.meshtastic.core.strings.hops_away
|
||||
import org.meshtastic.core.strings.node_id
|
||||
import org.meshtastic.core.strings.node_number
|
||||
import org.meshtastic.core.strings.node_sort_last_heard
|
||||
import org.meshtastic.core.strings.public_key
|
||||
import org.meshtastic.core.strings.role
|
||||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.short_name
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.strings.status_message
|
||||
import org.meshtastic.core.strings.supported
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.user_id
|
||||
import org.meshtastic.core.strings.via_mqtt
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.copy
|
||||
import org.meshtastic.core.resources.details
|
||||
import org.meshtastic.core.resources.encryption_error
|
||||
import org.meshtastic.core.resources.encryption_error_text
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.hops_away
|
||||
import org.meshtastic.core.resources.node_id
|
||||
import org.meshtastic.core.resources.node_number
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.resources.public_key
|
||||
import org.meshtastic.core.resources.role
|
||||
import org.meshtastic.core.resources.rssi
|
||||
import org.meshtastic.core.resources.short_name
|
||||
import org.meshtastic.core.resources.snr
|
||||
import org.meshtastic.core.resources.status_message
|
||||
import org.meshtastic.core.resources.supported
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.user_id
|
||||
import org.meshtastic.core.resources.via_mqtt
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
import org.meshtastic.core.ui.icon.ArrowCircleUp
|
||||
import org.meshtastic.core.ui.icon.ChannelUtilization
|
||||
|
|
|
|||
|
|
@ -61,18 +61,18 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.desc_node_filter_clear
|
||||
import org.meshtastic.core.strings.node_filter_exclude_infrastructure
|
||||
import org.meshtastic.core.strings.node_filter_ignored
|
||||
import org.meshtastic.core.strings.node_filter_include_unknown
|
||||
import org.meshtastic.core.strings.node_filter_only_direct
|
||||
import org.meshtastic.core.strings.node_filter_only_online
|
||||
import org.meshtastic.core.strings.node_filter_placeholder
|
||||
import org.meshtastic.core.strings.node_filter_show_ignored
|
||||
import org.meshtastic.core.strings.node_filter_title
|
||||
import org.meshtastic.core.strings.node_sort_button
|
||||
import org.meshtastic.core.strings.node_sort_title
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.desc_node_filter_clear
|
||||
import org.meshtastic.core.resources.node_filter_exclude_infrastructure
|
||||
import org.meshtastic.core.resources.node_filter_ignored
|
||||
import org.meshtastic.core.resources.node_filter_include_unknown
|
||||
import org.meshtastic.core.resources.node_filter_only_direct
|
||||
import org.meshtastic.core.resources.node_filter_only_online
|
||||
import org.meshtastic.core.resources.node_filter_placeholder
|
||||
import org.meshtastic.core.resources.node_filter_show_ignored
|
||||
import org.meshtastic.core.resources.node_filter_title
|
||||
import org.meshtastic.core.resources.node_sort_button
|
||||
import org.meshtastic.core.resources.node_sort_title
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
|||
|
|
@ -55,15 +55,15 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.elevation_suffix
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.air_utilization
|
||||
import org.meshtastic.core.strings.channel_utilization
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.ui.component.AirQualityInfo
|
||||
import org.meshtastic.core.ui.component.ChannelInfo
|
||||
import org.meshtastic.core.ui.component.DistanceInfo
|
||||
|
|
|
|||
|
|
@ -37,16 +37,16 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.connected
|
||||
import org.meshtastic.core.resources.connecting
|
||||
import org.meshtastic.core.resources.device_sleeping
|
||||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.unmessageable
|
||||
import org.meshtastic.core.resources.unmonitored_or_infrastructure
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.connected
|
||||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.device_sleeping
|
||||
import org.meshtastic.core.strings.disconnected
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.mute_always
|
||||
import org.meshtastic.core.strings.unmessageable
|
||||
import org.meshtastic.core.strings.unmonitored_or_infrastructure
|
||||
import org.meshtastic.core.ui.icon.CloudDone
|
||||
import org.meshtastic.core.ui.icon.CloudOffTwoTone
|
||||
import org.meshtastic.core.ui.icon.CloudSync
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add_a_note
|
||||
import org.meshtastic.core.strings.notes
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_a_note
|
||||
import org.meshtastic.core.resources.notes
|
||||
import org.meshtastic.core.resources.save
|
||||
|
||||
@Composable
|
||||
fun NotesSection(node: Node, onSaveNotes: (Int, String) -> Unit, modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ import androidx.compose.ui.unit.dp
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.exchange_position
|
||||
import org.meshtastic.core.strings.open_compass
|
||||
import org.meshtastic.core.strings.position
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.exchange_position
|
||||
import org.meshtastic.core.resources.open_compass
|
||||
import org.meshtastic.core.resources.position
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.NodeDetailAction
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_1
|
||||
import org.meshtastic.core.strings.channel_2
|
||||
import org.meshtastic.core.strings.channel_3
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_1
|
||||
import org.meshtastic.core.resources.channel_2
|
||||
import org.meshtastic.core.resources.channel_3
|
||||
import org.meshtastic.feature.node.model.VectorMetricInfo
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.sats
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ import org.jetbrains.compose.resources.StringResource
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.logs
|
||||
import org.meshtastic.core.strings.request_air_quality_metrics
|
||||
import org.meshtastic.core.strings.request_telemetry
|
||||
import org.meshtastic.core.strings.telemetry
|
||||
import org.meshtastic.core.strings.userinfo
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.logs
|
||||
import org.meshtastic.core.resources.request_air_quality_metrics
|
||||
import org.meshtastic.core.resources.request_telemetry
|
||||
import org.meshtastic.core.resources.telemetry
|
||||
import org.meshtastic.core.resources.userinfo
|
||||
import org.meshtastic.core.ui.icon.AirQuality
|
||||
import org.meshtastic.core.ui.icon.LineAxis
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
|
|||
|
|
@ -33,17 +33,17 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.env_metrics_log
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.node_id
|
||||
import org.meshtastic.core.strings.pax
|
||||
import org.meshtastic.core.strings.pax_metrics_log
|
||||
import org.meshtastic.core.strings.role
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.node_id
|
||||
import org.meshtastic.core.resources.pax
|
||||
import org.meshtastic.core.resources.pax_metrics_log
|
||||
import org.meshtastic.core.resources.role
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
|
||||
@Composable
|
||||
fun TemperatureInfo(
|
||||
|
|
|
|||
|
|
@ -61,10 +61,9 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.details
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.loading
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.details
|
||||
import org.meshtastic.core.resources.loading
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.SharedContactDialog
|
||||
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
|
||||
|
|
@ -100,20 +99,19 @@ fun NodeDetailScreen(
|
|||
onNavigate: (Route) -> Unit = {},
|
||||
onNavigateUp: () -> Unit = {},
|
||||
) {
|
||||
viewModel.start(nodeId)
|
||||
LaunchedEffect(nodeId) { viewModel.start(nodeId) }
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
if (effect is NodeRequestEffect.ShowFeedback) {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
NodeDetailScaffold(
|
||||
modifier = modifier,
|
||||
uiState = uiState,
|
||||
|
|
@ -149,8 +147,8 @@ private fun NodeDetailScaffold(
|
|||
modifier = modifier,
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = getString(Res.string.details),
|
||||
subtitle = node?.user?.long_name ?: "",
|
||||
title = stringResource(Res.string.details),
|
||||
subtitle = uiState.nodeName.asString(),
|
||||
ourNode = uiState.ourNode,
|
||||
showNodeChip = false,
|
||||
canNavigateUp = true,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.toRoute
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -31,44 +30,26 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
|
||||
import org.meshtastic.core.model.util.isDirectSignal
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.fallback_node_name
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.feature.node.component.NodeMenuAction
|
||||
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
|
||||
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* UI state for the Node Details screen.
|
||||
*
|
||||
* @property node The node being viewed, or null if loading.
|
||||
* @property nodeName The display name for the node, resolved in the UI.
|
||||
* @property ourNode Information about the locally connected node.
|
||||
* @property metricsState Aggregated sensor and signal metrics.
|
||||
* @property environmentState Standardized environmental sensor data.
|
||||
|
|
@ -76,8 +57,10 @@ import javax.inject.Inject
|
|||
* @property lastTracerouteTime Timestamp of the last successful traceroute request.
|
||||
* @property lastRequestNeighborsTime Timestamp of the last successful neighbor info request.
|
||||
*/
|
||||
@androidx.compose.runtime.Stable
|
||||
data class NodeDetailUiState(
|
||||
val node: Node? = null,
|
||||
val nodeName: UiText = UiText.DynamicString(""),
|
||||
val ourNode: Node? = null,
|
||||
val metricsState: MetricsState = MetricsState.Empty,
|
||||
val environmentState: EnvironmentMetricsState = EnvironmentMetricsState(),
|
||||
|
|
@ -93,17 +76,12 @@ data class NodeDetailUiState(
|
|||
@HiltViewModel
|
||||
class NodeDetailViewModel
|
||||
@Inject
|
||||
@Suppress("LongParameterList")
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val nodeManagementActions: NodeManagementActions,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||
) : ViewModel() {
|
||||
|
||||
private val nodeIdFromRoute: Int? =
|
||||
|
|
@ -120,166 +98,10 @@ constructor(
|
|||
activeNodeId
|
||||
.flatMapLatest { nodeId ->
|
||||
if (nodeId == null) return@flatMapLatest flowOf(NodeDetailUiState())
|
||||
buildUiStateFlow(nodeId)
|
||||
getNodeDetailsUseCase(nodeId)
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NodeDetailUiState())
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun buildUiStateFlow(nodeId: Int): Flow<NodeDetailUiState> {
|
||||
val nodeFlow =
|
||||
nodeRepository.nodeDBbyNum
|
||||
.map { it[nodeId] ?: Node.createFallback(nodeId, getString(Res.string.fallback_node_name)) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
// 1. Logs & Metrics Data (fetches telemetry, packets, paxcount, and response history)
|
||||
val metricsLogsFlow =
|
||||
combine(
|
||||
meshLogRepository.getTelemetryFrom(nodeId),
|
||||
meshLogRepository.getMeshPacketsFrom(nodeId),
|
||||
meshLogRepository.getMeshPacketsFrom(nodeId, PortNum.POSITION_APP.value),
|
||||
meshLogRepository.getLogsFrom(nodeId, PortNum.PAXCOUNTER_APP.value),
|
||||
meshLogRepository.getLogsFrom(nodeId, PortNum.TRACEROUTE_APP.value),
|
||||
meshLogRepository.getLogsFrom(nodeId, PortNum.NEIGHBORINFO_APP.value),
|
||||
) { args: Array<List<Any?>> ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
LogsGroup(
|
||||
telemetry = args[0] as List<Telemetry>,
|
||||
packets = args[1] as List<MeshPacket>,
|
||||
posPackets = args[2] as List<MeshPacket>,
|
||||
pax = args[3] as List<MeshLog>,
|
||||
trRes = args[4] as List<MeshLog>,
|
||||
niRes = args[5] as List<MeshLog>,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Identity & Config (local device info and radio profile)
|
||||
val identityFlow =
|
||||
combine(nodeRepository.ourNodeInfo, nodeRepository.myNodeInfo, radioConfigRepository.deviceProfileFlow) {
|
||||
ourNode,
|
||||
myInfo,
|
||||
profile,
|
||||
->
|
||||
IdentityGroup(ourNode, myInfo?.toMyNodeInfo(), profile)
|
||||
}
|
||||
|
||||
// 3. Metadata & Request Timestamps (firmware versions and last request times)
|
||||
val metadataFlow =
|
||||
combine(
|
||||
meshLogRepository.getMyNodeInfo().map { it?.firmware_edition }.distinctUntilChanged(),
|
||||
firmwareReleaseRepository.stableRelease,
|
||||
firmwareReleaseRepository.alphaRelease,
|
||||
nodeRequestActions.lastTracerouteTimes.map { it[nodeId] },
|
||||
nodeRequestActions.lastRequestNeighborTimes.map { it[nodeId] },
|
||||
) { args: Array<Any?> ->
|
||||
MetadataGroup(
|
||||
edition = args[0] as? FirmwareEdition,
|
||||
stable = args[1] as? FirmwareRelease,
|
||||
alpha = args[2] as? FirmwareRelease,
|
||||
trTime = args[3] as? Long,
|
||||
niTime = args[4] as? Long,
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Requests History (tracking traceroute and neighbor info requests sent from this device)
|
||||
val requestsFlow =
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.TRACEROUTE_APP),
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.NEIGHBORINFO_APP),
|
||||
) { trReqs, niReqs ->
|
||||
trReqs to niReqs
|
||||
}
|
||||
|
||||
// Assemble final UI state
|
||||
return combine(nodeFlow, metricsLogsFlow, identityFlow, metadataFlow, requestsFlow) {
|
||||
node,
|
||||
logs,
|
||||
identity,
|
||||
metadata,
|
||||
requests,
|
||||
->
|
||||
val (trReqs, niReqs) = requests
|
||||
val isLocal = node.num == identity.ourNode?.num
|
||||
val pioEnv = if (isLocal) identity.myInfo?.pioEnv else null
|
||||
val hw = deviceHardwareRepository.getDeviceHardwareByModel(node.user.hw_model.value, pioEnv).getOrNull()
|
||||
|
||||
val moduleConfig = identity.profile.module_config
|
||||
val displayUnits = identity.profile.config?.display?.units ?: Config.DisplayConfig.DisplayUnits.METRIC
|
||||
|
||||
val metricsState =
|
||||
MetricsState(
|
||||
node = node,
|
||||
isLocal = isLocal,
|
||||
deviceHardware = hw,
|
||||
reportedTarget = pioEnv,
|
||||
isManaged = identity.profile.config?.security?.is_managed ?: false,
|
||||
isFahrenheit =
|
||||
moduleConfig?.telemetry?.environment_display_fahrenheit == true ||
|
||||
(displayUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL),
|
||||
displayUnits = displayUnits,
|
||||
deviceMetrics = logs.telemetry.filter { it.device_metrics != null },
|
||||
powerMetrics = logs.telemetry.filter { it.power_metrics != null },
|
||||
hostMetrics = logs.telemetry.filter { it.host_metrics != null },
|
||||
signalMetrics = logs.packets.filter { it.isDirectSignal() },
|
||||
positionLogs = logs.posPackets.mapNotNull { it.toPosition() },
|
||||
paxMetrics = logs.pax,
|
||||
tracerouteRequests = trReqs,
|
||||
tracerouteResults = logs.trRes,
|
||||
neighborInfoRequests = niReqs,
|
||||
neighborInfoResults = logs.niRes,
|
||||
firmwareEdition = metadata.edition,
|
||||
latestStableFirmware = metadata.stable ?: FirmwareRelease(),
|
||||
latestAlphaFirmware = metadata.alpha ?: FirmwareRelease(),
|
||||
)
|
||||
|
||||
val environmentState =
|
||||
EnvironmentMetricsState(environmentMetrics = logs.telemetry.filter { it.hasValidEnvironmentMetrics() })
|
||||
|
||||
val availableLogs = buildSet {
|
||||
if (metricsState.hasDeviceMetrics()) add(LogsType.DEVICE)
|
||||
if (metricsState.hasPositionLogs()) {
|
||||
add(LogsType.NODE_MAP)
|
||||
add(LogsType.POSITIONS)
|
||||
}
|
||||
if (environmentState.hasEnvironmentMetrics()) add(LogsType.ENVIRONMENT)
|
||||
if (metricsState.hasSignalMetrics()) add(LogsType.SIGNAL)
|
||||
if (metricsState.hasPowerMetrics()) add(LogsType.POWER)
|
||||
if (metricsState.hasTracerouteLogs()) add(LogsType.TRACEROUTE)
|
||||
if (metricsState.hasNeighborInfoLogs()) add(LogsType.NEIGHBOR_INFO)
|
||||
if (metricsState.hasHostMetrics()) add(LogsType.HOST)
|
||||
if (metricsState.hasPaxMetrics()) add(LogsType.PAX)
|
||||
}
|
||||
|
||||
NodeDetailUiState(
|
||||
node = node,
|
||||
ourNode = identity.ourNode,
|
||||
metricsState = metricsState,
|
||||
environmentState = environmentState,
|
||||
availableLogs = availableLogs,
|
||||
lastTracerouteTime = metadata.trTime,
|
||||
lastRequestNeighborsTime = metadata.niTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class LogsGroup(
|
||||
val telemetry: List<Telemetry>,
|
||||
val packets: List<MeshPacket>,
|
||||
val posPackets: List<MeshPacket>,
|
||||
val pax: List<MeshLog>,
|
||||
val trRes: List<MeshLog>,
|
||||
val niRes: List<MeshLog>,
|
||||
)
|
||||
|
||||
private data class IdentityGroup(val ourNode: Node?, val myInfo: MyNodeInfo?, val profile: DeviceProfile)
|
||||
|
||||
private data class MetadataGroup(
|
||||
val edition: FirmwareEdition?,
|
||||
val stable: FirmwareRelease?,
|
||||
val alpha: FirmwareRelease?,
|
||||
val trTime: Long?,
|
||||
val niTime: Long?,
|
||||
)
|
||||
|
||||
val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects
|
||||
|
||||
fun start(nodeId: Int) {
|
||||
|
|
|
|||
|
|
@ -24,21 +24,21 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.favorite
|
||||
import org.meshtastic.core.resources.favorite_add
|
||||
import org.meshtastic.core.resources.favorite_remove
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.ignore_add
|
||||
import org.meshtastic.core.resources.ignore_remove
|
||||
import org.meshtastic.core.resources.mute_add
|
||||
import org.meshtastic.core.resources.mute_notifications
|
||||
import org.meshtastic.core.resources.mute_remove
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.remove_node_text
|
||||
import org.meshtastic.core.resources.unmute
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.favorite
|
||||
import org.meshtastic.core.strings.favorite_add
|
||||
import org.meshtastic.core.strings.favorite_remove
|
||||
import org.meshtastic.core.strings.ignore
|
||||
import org.meshtastic.core.strings.ignore_add
|
||||
import org.meshtastic.core.strings.ignore_remove
|
||||
import org.meshtastic.core.strings.mute_add
|
||||
import org.meshtastic.core.strings.mute_notifications
|
||||
import org.meshtastic.core.strings.mute_remove
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.remove_node_text
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
|
|||
|
|
@ -27,29 +27,29 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.resources.neighbor_info
|
||||
import org.meshtastic.core.resources.position
|
||||
import org.meshtastic.core.resources.request_air_quality_metrics
|
||||
import org.meshtastic.core.resources.request_device_metrics
|
||||
import org.meshtastic.core.resources.request_environment_metrics
|
||||
import org.meshtastic.core.resources.request_host_metrics
|
||||
import org.meshtastic.core.resources.request_pax_metrics
|
||||
import org.meshtastic.core.resources.request_power_metrics
|
||||
import org.meshtastic.core.resources.requesting_from
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.user_info
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.position
|
||||
import org.meshtastic.core.strings.request_air_quality_metrics
|
||||
import org.meshtastic.core.strings.request_device_metrics
|
||||
import org.meshtastic.core.strings.request_environment_metrics
|
||||
import org.meshtastic.core.strings.request_host_metrics
|
||||
import org.meshtastic.core.strings.request_pax_metrics
|
||||
import org.meshtastic.core.strings.request_power_metrics
|
||||
import org.meshtastic.core.strings.requesting_from
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.user_info
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed class NodeRequestEffect {
|
||||
data class ShowFeedback(val resource: StringResource, val args: List<Any> = emptyList()) : NodeRequestEffect()
|
||||
data class ShowFeedback(val text: UiText) : NodeRequestEffect()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
|
|
@ -70,7 +70,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
try {
|
||||
serviceRepository.meshService?.requestUserInfo(destNum)
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.user_info, longName)),
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.user_info, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request NodeInfo error: ${ex.message}" }
|
||||
|
|
@ -87,8 +89,7 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
_lastRequestNeighborTimes.update { it + (destNum to nowMillis) }
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
Res.string.requesting_from,
|
||||
listOf(Res.string.neighbor_info, longName),
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.neighbor_info, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
|
|
@ -108,7 +109,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
try {
|
||||
serviceRepository.meshService?.requestPosition(destNum, position)
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.position, longName)),
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.position, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request position error: ${ex.message}" }
|
||||
|
|
@ -134,7 +137,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
TelemetryType.PAX -> Res.string.request_pax_metrics
|
||||
}
|
||||
|
||||
_effects.emit(NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(typeRes, longName)))
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(UiText.Resource(Res.string.requesting_from, typeRes, longName)),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request telemetry error: ${ex.message}" }
|
||||
}
|
||||
|
|
@ -149,7 +154,9 @@ class NodeRequestActions @Inject constructor(private val serviceRepository: Serv
|
|||
serviceRepository.meshService?.requestTraceroute(packetId, destNum)
|
||||
_lastTracerouteTimes.update { it + (destNum to nowMillis) }
|
||||
_effects.emit(
|
||||
NodeRequestEffect.ShowFeedback(Res.string.requesting_from, listOf(Res.string.traceroute, longName)),
|
||||
NodeRequestEffect.ShowFeedback(
|
||||
UiText.Resource(Res.string.requesting_from, Res.string.traceroute, longName),
|
||||
),
|
||||
)
|
||||
} catch (ex: android.os.RemoteException) {
|
||||
Logger.e { "Request traceroute error: ${ex.message}" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.feature.node.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.feature.node.list.NodeFilterState
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.proto.Config
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetFilteredNodesUseCase @Inject constructor(private val nodeRepository: NodeRepository) {
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
operator fun invoke(filter: NodeFilterState, sort: NodeSortOption): Flow<List<Node>> = nodeRepository
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter.filterText,
|
||||
includeUnknown = filter.includeUnknown,
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list ->
|
||||
list
|
||||
.filter { node -> node.isIgnored == filter.showIgnored }
|
||||
.filter { node ->
|
||||
if (filter.excludeInfrastructure) {
|
||||
val role = node.user.role
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val infrastructureRoles =
|
||||
listOf(
|
||||
Config.DeviceConfig.Role.ROUTER,
|
||||
Config.DeviceConfig.Role.REPEATER,
|
||||
Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
Config.DeviceConfig.Role.CLIENT_BASE,
|
||||
)
|
||||
role !in infrastructureRoles && !node.isEffectivelyUnmessageable
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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 org.meshtastic.feature.node.domain.usecase
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
|
||||
import org.meshtastic.core.model.util.isDirectSignal
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.resources.fallback_node_name
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.feature.node.detail.NodeDetailUiState
|
||||
import org.meshtastic.feature.node.detail.NodeRequestActions
|
||||
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState
|
||||
import org.meshtastic.feature.node.model.LogsType
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetNodeDetailsUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
operator fun invoke(nodeId: Int): Flow<NodeDetailUiState> =
|
||||
nodeRepository.effectiveLogNodeId(nodeId).flatMapLatest { effectiveNodeId ->
|
||||
buildFlow(nodeId, effectiveNodeId)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun buildFlow(nodeId: Int, effectiveNodeId: Int): Flow<NodeDetailUiState> {
|
||||
val nodeFlow =
|
||||
nodeRepository.nodeDBbyNum.map { it[nodeId] ?: Node.createFallback(nodeId, "") }.distinctUntilChanged()
|
||||
|
||||
// 1. Logs & Metrics Data
|
||||
val metricsLogsFlow =
|
||||
combine(
|
||||
meshLogRepository.getTelemetryFrom(effectiveNodeId).onStart { emit(emptyList()) },
|
||||
meshLogRepository.getMeshPacketsFrom(effectiveNodeId).onStart { emit(emptyList()) },
|
||||
meshLogRepository.getMeshPacketsFrom(effectiveNodeId, PortNum.POSITION_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
meshLogRepository.getLogsFrom(effectiveNodeId, PortNum.PAXCOUNTER_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
meshLogRepository.getLogsFrom(effectiveNodeId, PortNum.TRACEROUTE_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
meshLogRepository.getLogsFrom(effectiveNodeId, PortNum.NEIGHBORINFO_APP.value).onStart {
|
||||
emit(emptyList())
|
||||
},
|
||||
) { args: Array<List<Any?>> ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
LogsGroup(
|
||||
telemetry = args[0] as List<Telemetry>,
|
||||
packets = args[1] as List<MeshPacket>,
|
||||
posPackets = args[2] as List<MeshPacket>,
|
||||
pax = args[3] as List<MeshLog>,
|
||||
trRes = args[4] as List<MeshLog>,
|
||||
niRes = args[5] as List<MeshLog>,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Identity & Config
|
||||
val identityFlow =
|
||||
combine(
|
||||
nodeRepository.ourNodeInfo,
|
||||
nodeRepository.myNodeInfo,
|
||||
radioConfigRepository.deviceProfileFlow.onStart { emit(DeviceProfile()) },
|
||||
) { ourNode, myInfo, profile ->
|
||||
IdentityGroup(ourNode, myInfo?.toMyNodeInfo(), profile)
|
||||
}
|
||||
|
||||
// 3. Metadata & Request Timestamps
|
||||
val metadataFlow =
|
||||
combine(
|
||||
meshLogRepository
|
||||
.getMyNodeInfo()
|
||||
.map { it?.firmware_edition }
|
||||
.distinctUntilChanged()
|
||||
.onStart { emit(null) },
|
||||
firmwareReleaseRepository.stableRelease,
|
||||
firmwareReleaseRepository.alphaRelease,
|
||||
nodeRequestActions.lastTracerouteTimes.map { it[nodeId] },
|
||||
nodeRequestActions.lastRequestNeighborTimes.map { it[nodeId] },
|
||||
) { edition, stable, alpha, trTime, niTime ->
|
||||
MetadataGroup(edition = edition, stable = stable, alpha = alpha, trTime = trTime, niTime = niTime)
|
||||
}
|
||||
|
||||
// 4. Requests History (we still query request logs by the target nodeId)
|
||||
val requestsFlow =
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.TRACEROUTE_APP).onStart { emit(emptyList()) },
|
||||
meshLogRepository.getRequestLogs(nodeId, PortNum.NEIGHBORINFO_APP).onStart { emit(emptyList()) },
|
||||
) { trReqs, niReqs ->
|
||||
trReqs to niReqs
|
||||
}
|
||||
|
||||
// Assemble final UI state
|
||||
return combine(nodeFlow, metricsLogsFlow, identityFlow, metadataFlow, requestsFlow) {
|
||||
node,
|
||||
logs,
|
||||
identity,
|
||||
metadata,
|
||||
requests,
|
||||
->
|
||||
val (trReqs, niReqs) = requests
|
||||
val isLocal = node.num == identity.ourNode?.num
|
||||
val pioEnv = if (isLocal) identity.myInfo?.pioEnv else null
|
||||
val hw = deviceHardwareRepository.getDeviceHardwareByModel(node.user.hw_model.value, pioEnv).getOrNull()
|
||||
|
||||
val moduleConfig = identity.profile.module_config
|
||||
val displayUnits = identity.profile.config?.display?.units ?: Config.DisplayConfig.DisplayUnits.METRIC
|
||||
|
||||
val metricsState =
|
||||
MetricsState(
|
||||
node = node,
|
||||
isLocal = isLocal,
|
||||
deviceHardware = hw,
|
||||
reportedTarget = pioEnv,
|
||||
isManaged = identity.profile.config?.security?.is_managed ?: false,
|
||||
isFahrenheit =
|
||||
moduleConfig?.telemetry?.environment_display_fahrenheit == true ||
|
||||
(displayUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL),
|
||||
displayUnits = displayUnits,
|
||||
deviceMetrics = logs.telemetry.filter { it.device_metrics != null },
|
||||
powerMetrics = logs.telemetry.filter { it.power_metrics != null },
|
||||
hostMetrics = logs.telemetry.filter { it.host_metrics != null },
|
||||
signalMetrics = logs.packets.filter { it.isDirectSignal() },
|
||||
positionLogs = logs.posPackets.mapNotNull { it.toPosition() },
|
||||
paxMetrics = logs.pax,
|
||||
tracerouteRequests = trReqs,
|
||||
tracerouteResults = logs.trRes,
|
||||
neighborInfoRequests = niReqs,
|
||||
neighborInfoResults = logs.niRes,
|
||||
firmwareEdition = metadata.edition,
|
||||
latestStableFirmware = metadata.stable ?: FirmwareRelease(),
|
||||
latestAlphaFirmware = metadata.alpha ?: FirmwareRelease(),
|
||||
)
|
||||
|
||||
val environmentState =
|
||||
EnvironmentMetricsState(environmentMetrics = logs.telemetry.filter { it.hasValidEnvironmentMetrics() })
|
||||
|
||||
val availableLogs = buildSet {
|
||||
if (metricsState.hasDeviceMetrics()) add(LogsType.DEVICE)
|
||||
if (metricsState.hasPositionLogs()) {
|
||||
add(LogsType.NODE_MAP)
|
||||
add(LogsType.POSITIONS)
|
||||
}
|
||||
if (environmentState.hasEnvironmentMetrics()) add(LogsType.ENVIRONMENT)
|
||||
if (metricsState.hasSignalMetrics()) add(LogsType.SIGNAL)
|
||||
if (metricsState.hasPowerMetrics()) add(LogsType.POWER)
|
||||
if (metricsState.hasTracerouteLogs()) add(LogsType.TRACEROUTE)
|
||||
if (metricsState.hasNeighborInfoLogs()) add(LogsType.NEIGHBOR_INFO)
|
||||
if (metricsState.hasHostMetrics()) add(LogsType.HOST)
|
||||
if (metricsState.hasPaxMetrics()) add(LogsType.PAX)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
val nodeName =
|
||||
node.user.long_name?.takeIf { it.isNotBlank() }?.let { UiText.DynamicString(it) }
|
||||
?: UiText.Resource(Res.string.fallback_node_name, node.user.id.takeLast(4))
|
||||
|
||||
NodeDetailUiState(
|
||||
node = node,
|
||||
nodeName = nodeName,
|
||||
ourNode = identity.ourNode,
|
||||
metricsState = metricsState,
|
||||
environmentState = environmentState,
|
||||
availableLogs = availableLogs,
|
||||
lastTracerouteTime = metadata.trTime,
|
||||
lastRequestNeighborsTime = metadata.niTime,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class LogsGroup(
|
||||
val telemetry: List<Telemetry>,
|
||||
val packets: List<MeshPacket>,
|
||||
val posPackets: List<MeshPacket>,
|
||||
val pax: List<MeshLog>,
|
||||
val trRes: List<MeshLog>,
|
||||
val niRes: List<MeshLog>,
|
||||
)
|
||||
|
||||
private data class IdentityGroup(val ourNode: Node?, val myInfo: MyNodeInfo?, val profile: DeviceProfile)
|
||||
|
||||
private data class MetadataGroup(
|
||||
val edition: FirmwareEdition?,
|
||||
val stable: FirmwareRelease?,
|
||||
val alpha: FirmwareRelease?,
|
||||
val trTime: Long?,
|
||||
val niTime: Long?,
|
||||
)
|
||||
}
|
||||
|
|
@ -68,18 +68,18 @@ 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.resources.Res
|
||||
import org.meshtastic.core.resources.add_favorite
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
import org.meshtastic.core.resources.ignore
|
||||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.node_count_template
|
||||
import org.meshtastic.core.resources.nodes
|
||||
import org.meshtastic.core.resources.remove
|
||||
import org.meshtastic.core.resources.remove_favorite
|
||||
import org.meshtastic.core.resources.remove_ignored
|
||||
import org.meshtastic.core.resources.unmute
|
||||
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
|
||||
import org.meshtastic.core.strings.nodes
|
||||
import org.meshtastic.core.strings.remove
|
||||
import org.meshtastic.core.strings.remove_favorite
|
||||
import org.meshtastic.core.strings.remove_ignored
|
||||
import org.meshtastic.core.strings.unmute
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticImportFAB
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
|
|
@ -39,12 +38,13 @@ import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.node.detail.NodeManagementActions
|
||||
import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||
import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class NodeListViewModel
|
||||
@Inject
|
||||
|
|
@ -54,6 +54,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
val nodeManagementActions: NodeManagementActions,
|
||||
private val getFilteredNodesUseCase: GetFilteredNodesUseCase,
|
||||
val nodeFilterPreferences: NodeFilterPreferences,
|
||||
) : ViewModel() {
|
||||
|
||||
|
|
@ -116,35 +117,7 @@ constructor(
|
|||
|
||||
val nodeList: StateFlow<List<Node>> =
|
||||
combine(nodeFilter, nodeSortOption, ::Pair)
|
||||
.flatMapLatest { (filter, sort) ->
|
||||
nodeRepository
|
||||
.getNodes(
|
||||
sort = sort,
|
||||
filter = filter.filterText,
|
||||
includeUnknown = filter.includeUnknown,
|
||||
onlyOnline = filter.onlyOnline,
|
||||
onlyDirect = filter.onlyDirect,
|
||||
)
|
||||
.map { list ->
|
||||
list
|
||||
.filter { node -> node.isIgnored == filter.showIgnored }
|
||||
.filter { node ->
|
||||
if (filter.excludeInfrastructure) {
|
||||
val role = node.user.role
|
||||
val infrastructureRoles =
|
||||
listOf(
|
||||
Config.DeviceConfig.Role.ROUTER,
|
||||
Config.DeviceConfig.Role.REPEATER,
|
||||
Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
Config.DeviceConfig.Role.CLIENT_BASE,
|
||||
)
|
||||
role !in infrastructureRoles && !node.isEffectivelyUnmessageable
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatMapLatest { (filter, sort) -> getFilteredNodesUseCase.invoke(filter, sort) }
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val unfilteredNodeList: StateFlow<List<Node>> =
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.info
|
||||
import org.meshtastic.core.strings.logs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.info
|
||||
import org.meshtastic.core.resources.logs
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Refresh
|
||||
|
|
|
|||
|
|
@ -63,12 +63,12 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.close
|
||||
import org.meshtastic.core.strings.delete
|
||||
import org.meshtastic.core.strings.info
|
||||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.close
|
||||
import org.meshtastic.core.resources.delete
|
||||
import org.meshtastic.core.resources.info
|
||||
import org.meshtastic.core.resources.rssi
|
||||
import org.meshtastic.core.resources.snr
|
||||
import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import java.text.DateFormat
|
||||
|
|
|
|||
|
|
@ -66,16 +66,15 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.air_util_definition
|
||||
import org.meshtastic.core.strings.air_utilization
|
||||
import org.meshtastic.core.strings.battery
|
||||
import org.meshtastic.core.strings.ch_util_definition
|
||||
import org.meshtastic.core.strings.channel_utilization
|
||||
import org.meshtastic.core.strings.device_metrics_log
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_util_definition
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
import org.meshtastic.core.resources.battery
|
||||
import org.meshtastic.core.resources.ch_util_definition
|
||||
import org.meshtastic.core.resources.channel_utilization
|
||||
import org.meshtastic.core.resources.device_metrics_log
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.component.MaterialBatteryInfo
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Cyan
|
||||
|
|
@ -141,7 +140,7 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,15 +33,15 @@ import com.patrykandpatrick.vico.compose.cartesian.data.lineSeries
|
|||
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.baro_pressure
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.lux
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.strings.uv_lux
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.baro_pressure
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
import org.meshtastic.core.resources.uv_lux
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
|
|||
|
|
@ -51,21 +51,20 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.env_metrics_log
|
||||
import org.meshtastic.core.strings.gas_resistance
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.humidity
|
||||
import org.meshtastic.core.strings.iaq
|
||||
import org.meshtastic.core.strings.iaq_definition
|
||||
import org.meshtastic.core.strings.lux
|
||||
import org.meshtastic.core.strings.radiation
|
||||
import org.meshtastic.core.strings.soil_moisture
|
||||
import org.meshtastic.core.strings.soil_temperature
|
||||
import org.meshtastic.core.strings.temperature
|
||||
import org.meshtastic.core.strings.uv_lux
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.env_metrics_log
|
||||
import org.meshtastic.core.resources.gas_resistance
|
||||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.iaq_definition
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
import org.meshtastic.core.resources.temperature
|
||||
import org.meshtastic.core.resources.uv_lux
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.component.IaqDisplayMode
|
||||
import org.meshtastic.core.ui.component.IndoorAirQuality
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
|
@ -87,7 +86,7 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNa
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,13 +59,12 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.disk_free_indexed
|
||||
import org.meshtastic.core.strings.free_memory
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.load_indexed
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.user_string
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.disk_free_indexed
|
||||
import org.meshtastic.core.resources.free_memory
|
||||
import org.meshtastic.core.resources.load_indexed
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.user_string
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.DataArray
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -88,7 +87,7 @@ fun HostMetricsLogScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), o
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,18 +27,17 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.navigation.toRoute
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -46,11 +45,8 @@ import org.jetbrains.compose.resources.StringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
import org.meshtastic.core.common.util.toInstant
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
|
@ -58,26 +54,21 @@ import org.meshtastic.core.di.CoroutineDispatchers
|
|||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.okay
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.view_on_map
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.fallback_node_name
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.okay
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.view_on_map
|
||||
import org.meshtastic.core.ui.util.AlertManager
|
||||
import org.meshtastic.core.ui.util.toMessageRes
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.map.model.TracerouteOverlay
|
||||
import org.meshtastic.feature.node.detail.NodeRequestActions
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
|
||||
import org.meshtastic.feature.node.model.MetricsState
|
||||
import org.meshtastic.feature.node.model.TimeFrame
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import java.io.BufferedWriter
|
||||
|
|
@ -89,8 +80,6 @@ import java.util.Locale
|
|||
import javax.inject.Inject
|
||||
import org.meshtastic.proto.Paxcount as ProtoPaxcount
|
||||
|
||||
private fun MeshPacket.hasValidSignal(): Boolean = rx_time > 0 && (rx_snr != 0f || rx_rssi != 0)
|
||||
|
||||
/**
|
||||
* ViewModel responsible for managing and graphing metrics (telemetry, signal strength, paxcount) for a specific node.
|
||||
*/
|
||||
|
|
@ -103,27 +92,111 @@ constructor(
|
|||
private val app: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||
private val nodeRequestActions: NodeRequestActions,
|
||||
private val alertManager: AlertManager,
|
||||
private val getNodeDetailsUseCase: GetNodeDetailsUseCase,
|
||||
) : ViewModel() {
|
||||
private var destNum: Int? =
|
||||
|
||||
private val nodeIdFromRoute: Int? =
|
||||
runCatching { savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum }.getOrNull()
|
||||
|
||||
private var jobs: Job? = null
|
||||
private val manualNodeId = MutableStateFlow<Int?>(null)
|
||||
private val activeNodeId =
|
||||
combine(MutableStateFlow(nodeIdFromRoute), manualNodeId) { fromRoute, manual -> manual ?: fromRoute }
|
||||
|
||||
private val tracerouteOverlayCache = MutableStateFlow<Map<Int, TracerouteOverlay>>(emptyMap())
|
||||
|
||||
val state: StateFlow<MetricsState> =
|
||||
activeNodeId
|
||||
.flatMapLatest { nodeId ->
|
||||
if (nodeId == null) return@flatMapLatest flowOf(MetricsState.Empty)
|
||||
getNodeDetailsUseCase(nodeId).map { it.metricsState }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MetricsState.Empty)
|
||||
|
||||
private val environmentState: StateFlow<EnvironmentMetricsState> =
|
||||
activeNodeId
|
||||
.flatMapLatest { nodeId ->
|
||||
if (nodeId == null) return@flatMapLatest flowOf(EnvironmentMetricsState())
|
||||
getNodeDetailsUseCase(nodeId).map { it.environmentState }
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), EnvironmentMetricsState())
|
||||
|
||||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
|
||||
/** The active time window for filtering graphed data. */
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
/** Returns the list of time frames that are actually available based on the oldest data point. */
|
||||
val availableTimeFrames: StateFlow<List<TimeFrame>> =
|
||||
combine(state, environmentState) { currentState, envState ->
|
||||
val stateOldest = currentState.oldestTimestampSeconds()
|
||||
val envOldest = envState.environmentMetrics.minOfOrNull { (it.time ?: 0).toLong() }?.takeIf { it > 0 }
|
||||
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
|
||||
TimeFrame.entries.filter { it.isAvailable(oldest) }
|
||||
}
|
||||
.stateInWhileSubscribed(TimeFrame.entries)
|
||||
|
||||
fun setTimeFrame(timeFrame: TimeFrame) {
|
||||
_timeFrame.value = timeFrame
|
||||
}
|
||||
|
||||
/** Exposes filtered and unit-converted environment metrics for the UI. */
|
||||
val filteredEnvironmentMetrics: StateFlow<List<Telemetry>> =
|
||||
combine(environmentState, _timeFrame, state) { envState, timeFrame, currentState ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
val data = envState.environmentMetrics.filter { (it.time ?: 0).toLong() >= threshold }
|
||||
if (currentState.isFahrenheit) {
|
||||
data.map { telemetry ->
|
||||
val em = telemetry.environment_metrics ?: return@map telemetry
|
||||
telemetry.copy(
|
||||
environment_metrics =
|
||||
em.copy(
|
||||
temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
soil_temperature =
|
||||
em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
data
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
/** Exposes graphing data specifically for the filtered environment metrics. */
|
||||
val environmentGraphingData: StateFlow<EnvironmentGraphingData> =
|
||||
filteredEnvironmentMetrics
|
||||
.map { filtered -> EnvironmentMetricsState(filtered).environmentMetricsForGraphing(useFahrenheit = false) }
|
||||
.stateInWhileSubscribed(EnvironmentGraphingData(emptyList(), emptyList()))
|
||||
|
||||
/** Exposes filtered and decoded pax metrics for the UI. */
|
||||
val filteredPaxMetrics: StateFlow<List<Pair<MeshLog, ProtoPaxcount>>> =
|
||||
combine(state, _timeFrame) { currentState, timeFrame ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
currentState.paxMetrics
|
||||
.filter { (it.received_date / 1000) >= threshold }
|
||||
.mapNotNull { log -> decodePaxFromLog(log)?.let { log to it } }
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects
|
||||
|
||||
val lastTraceRouteTime: StateFlow<Long?> =
|
||||
combine(nodeRequestActions.lastTracerouteTimes, activeNodeId) { map, id -> id?.let { map[it] } }
|
||||
.stateInWhileSubscribed(null)
|
||||
|
||||
val lastRequestNeighborsTime: StateFlow<Long?> =
|
||||
combine(nodeRequestActions.lastRequestNeighborTimes, activeNodeId) { map, id -> id?.let { map[it] } }
|
||||
.stateInWhileSubscribed(null)
|
||||
|
||||
fun getUser(nodeNum: Int) = nodeRepository.getUser(nodeNum)
|
||||
|
||||
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) { meshLogRepository.deleteLog(uuid) }
|
||||
|
||||
/** Returns the map overlay for a specific traceroute request ID. */
|
||||
fun getTracerouteOverlay(requestId: Int): TracerouteOverlay? {
|
||||
val cached = tracerouteOverlayCache.value[requestId]
|
||||
if (cached != null) return cached
|
||||
|
|
@ -170,103 +243,35 @@ constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
Logger.d { "MetricsViewModel created" }
|
||||
}
|
||||
|
||||
fun clearPosition() = viewModelScope.launch(dispatchers.io) {
|
||||
destNum?.let { meshLogRepository.deleteLogs(it, PortNum.POSITION_APP.value) }
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
meshLogRepository.deleteLogs(it, PortNum.POSITION_APP.value)
|
||||
}
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(MetricsState.Empty)
|
||||
|
||||
/** Current aggregated metrics state, including signal history and sensor logs. */
|
||||
val state: StateFlow<MetricsState> = _state
|
||||
|
||||
private val environmentState = MutableStateFlow(EnvironmentMetricsState())
|
||||
|
||||
private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
|
||||
|
||||
/** The active time window for filtering graphed data. */
|
||||
val timeFrame: StateFlow<TimeFrame> = _timeFrame
|
||||
|
||||
/** Returns the list of time frames that are actually available based on the oldest data point. */
|
||||
val availableTimeFrames: StateFlow<List<TimeFrame>> =
|
||||
combine(_state, environmentState) { state, envState ->
|
||||
val stateOldest = state.oldestTimestampSeconds()
|
||||
val envOldest = envState.environmentMetrics.minOfOrNull { (it.time ?: 0).toLong() }?.takeIf { it > 0 }
|
||||
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
|
||||
TimeFrame.entries.filter { it.isAvailable(oldest) }
|
||||
}
|
||||
.stateInWhileSubscribed(TimeFrame.entries)
|
||||
|
||||
fun setTimeFrame(timeFrame: TimeFrame) {
|
||||
_timeFrame.value = timeFrame
|
||||
}
|
||||
|
||||
/** Exposes filtered and unit-converted environment metrics for the UI. */
|
||||
val filteredEnvironmentMetrics: StateFlow<List<Telemetry>> =
|
||||
combine(environmentState, _timeFrame, _state) { envState, timeFrame, state ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
val data = envState.environmentMetrics.filter { (it.time ?: 0).toLong() >= threshold }
|
||||
if (state.isFahrenheit) {
|
||||
data.map { telemetry ->
|
||||
val em = telemetry.environment_metrics ?: return@map telemetry
|
||||
telemetry.copy(
|
||||
environment_metrics =
|
||||
em.copy(
|
||||
temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
soil_temperature =
|
||||
em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
data
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
/** Exposes graphing data specifically for the filtered environment metrics. */
|
||||
val environmentGraphingData: StateFlow<EnvironmentGraphingData> =
|
||||
filteredEnvironmentMetrics
|
||||
.map { filtered -> EnvironmentMetricsState(filtered).environmentMetricsForGraphing(useFahrenheit = false) }
|
||||
.stateInWhileSubscribed(EnvironmentGraphingData(emptyList(), emptyList()))
|
||||
|
||||
/** Exposes filtered and decoded pax metrics for the UI. */
|
||||
val filteredPaxMetrics: StateFlow<List<Pair<MeshLog, ProtoPaxcount>>> =
|
||||
combine(_state, _timeFrame) { state, timeFrame ->
|
||||
val threshold = timeFrame.timeThreshold()
|
||||
state.paxMetrics
|
||||
.filter { (it.received_date / 1000) >= threshold }
|
||||
.mapNotNull { log -> decodePaxFromLog(log)?.let { log to it } }
|
||||
}
|
||||
.stateInWhileSubscribed(emptyList())
|
||||
|
||||
val effects: SharedFlow<NodeRequestEffect> = nodeRequestActions.effects
|
||||
|
||||
val lastTraceRouteTime: StateFlow<Long?> =
|
||||
nodeRequestActions.lastTracerouteTimes.map { it[destNum] }.stateInWhileSubscribed(null)
|
||||
|
||||
val lastRequestNeighborsTime: StateFlow<Long?> =
|
||||
nodeRequestActions.lastRequestNeighborTimes.map { it[destNum] }.stateInWhileSubscribed(null)
|
||||
|
||||
fun requestPosition() {
|
||||
destNum?.let { nodeRequestActions.requestPosition(viewModelScope, it, state.value.node?.user?.long_name ?: "") }
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestPosition(viewModelScope, it, state.value.node?.user?.long_name ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTelemetry(type: TelemetryType) {
|
||||
destNum?.let {
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestTelemetry(viewModelScope, it, state.value.node?.user?.long_name ?: "", type)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestTraceroute() {
|
||||
destNum?.let {
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestTraceroute(viewModelScope, it, state.value.node?.user?.long_name ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
fun requestNeighborInfo() {
|
||||
destNum?.let {
|
||||
(manualNodeId.value ?: nodeIdFromRoute)?.let {
|
||||
nodeRequestActions.requestNeighborInfo(viewModelScope, it, state.value.node?.user?.long_name ?: "")
|
||||
}
|
||||
}
|
||||
|
|
@ -278,7 +283,6 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/** Shows the detail dialog for a traceroute result, with an option to view on the map. */
|
||||
fun showTracerouteDetail(
|
||||
annotatedMessage: AnnotatedString,
|
||||
requestId: Int,
|
||||
|
|
@ -318,188 +322,17 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
init {
|
||||
initializeFlows()
|
||||
}
|
||||
|
||||
fun setNodeId(id: Int) {
|
||||
if (destNum != id) {
|
||||
destNum = id
|
||||
initializeFlows()
|
||||
if (manualNodeId.value != id) {
|
||||
manualNodeId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun initializeFlows() {
|
||||
jobs?.cancel()
|
||||
val currentDestNum = destNum
|
||||
jobs =
|
||||
viewModelScope.launch {
|
||||
if (currentDestNum != null) {
|
||||
val logNodeIdFlow = nodeRepository.effectiveLogNodeId(currentDestNum)
|
||||
|
||||
launch {
|
||||
combine(nodeRepository.nodeDBbyNum, nodeRepository.myNodeInfo) { nodes, myInfo ->
|
||||
nodes[currentDestNum] to (nodes.keys.firstOrNull() to myInfo)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { (node, localData) ->
|
||||
val (ourNodeNum, myInfo) = localData
|
||||
// Create a fallback node if not found in database (for hidden clients, etc.)
|
||||
val actualNode =
|
||||
node
|
||||
?: Node.createFallback(currentDestNum, getString(Res.string.fallback_node_name))
|
||||
val pioEnv = if (currentDestNum == ourNodeNum) myInfo?.pioEnv else null
|
||||
val hwModel = actualNode.user.hw_model.value
|
||||
val deviceHardware =
|
||||
deviceHardwareRepository.getDeviceHardwareByModel(hwModel, target = pioEnv)
|
||||
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
node = actualNode,
|
||||
isLocal = currentDestNum == ourNodeNum,
|
||||
deviceHardware = deviceHardware.getOrNull(),
|
||||
reportedTarget = pioEnv,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
radioConfigRepository.deviceProfileFlow.collect { profile ->
|
||||
val moduleConfig = profile.module_config
|
||||
val displayUnits = profile.config?.display?.units
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
isManaged = profile.config?.security?.is_managed ?: false,
|
||||
isFahrenheit =
|
||||
moduleConfig?.telemetry?.environment_display_fahrenheit == true ||
|
||||
(displayUnits == Config.DisplayConfig.DisplayUnits.IMPERIAL),
|
||||
displayUnits = displayUnits ?: Config.DisplayConfig.DisplayUnits.METRIC,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getTelemetryFrom(it) }
|
||||
.collect { telemetry ->
|
||||
val device = mutableListOf<Telemetry>()
|
||||
val power = mutableListOf<Telemetry>()
|
||||
val host = mutableListOf<Telemetry>()
|
||||
val env = mutableListOf<Telemetry>()
|
||||
|
||||
for (item in telemetry) {
|
||||
if (item.device_metrics != null) device.add(item)
|
||||
if (item.power_metrics != null) power.add(item)
|
||||
if (item.host_metrics != null) host.add(item)
|
||||
if (item.hasValidEnvironmentMetrics()) env.add(item)
|
||||
}
|
||||
|
||||
_state.update { state ->
|
||||
state.copy(deviceMetrics = device, powerMetrics = power, hostMetrics = host)
|
||||
}
|
||||
environmentState.update { it.copy(environmentMetrics = env) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getMeshPacketsFrom(it) }
|
||||
.collect { meshPackets ->
|
||||
_state.update { state ->
|
||||
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(currentDestNum, PortNum.TRACEROUTE_APP),
|
||||
logNodeIdFlow.flatMapLatest {
|
||||
meshLogRepository.getLogsFrom(it, PortNum.TRACEROUTE_APP.value)
|
||||
},
|
||||
) { request, response ->
|
||||
_state.update { state ->
|
||||
state.copy(tracerouteRequests = request, tracerouteResults = response)
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
launch {
|
||||
combine(
|
||||
meshLogRepository.getRequestLogs(currentDestNum, PortNum.NEIGHBORINFO_APP),
|
||||
logNodeIdFlow.flatMapLatest {
|
||||
meshLogRepository.getLogsFrom(it, PortNum.NEIGHBORINFO_APP.value)
|
||||
},
|
||||
) { request, response ->
|
||||
_state.update { state ->
|
||||
state.copy(neighborInfoRequests = request, neighborInfoResults = response)
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getMeshPacketsFrom(it, PortNum.POSITION_APP.value) }
|
||||
.collect { packets ->
|
||||
val distinctPositions =
|
||||
packets
|
||||
.mapNotNull { it.toPosition() }
|
||||
.asFlow()
|
||||
.distinctUntilChanged { old, new ->
|
||||
old.time == new.time ||
|
||||
(old.latitude_i == new.latitude_i && old.longitude_i == new.longitude_i)
|
||||
}
|
||||
.toList()
|
||||
_state.update { state -> state.copy(positionLogs = distinctPositions) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
logNodeIdFlow
|
||||
.flatMapLatest { meshLogRepository.getLogsFrom(it, PortNum.PAXCOUNTER_APP.value) }
|
||||
.collect { logs -> _state.update { state -> state.copy(paxMetrics = logs) } }
|
||||
}
|
||||
|
||||
launch {
|
||||
firmwareReleaseRepository.stableRelease.filterNotNull().collect { latestStable ->
|
||||
_state.update { state -> state.copy(latestStableFirmware = latestStable) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
firmwareReleaseRepository.alphaRelease.filterNotNull().collect { latestAlpha ->
|
||||
_state.update { state -> state.copy(latestAlphaFirmware = latestAlpha) }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
meshLogRepository
|
||||
.getMyNodeInfo()
|
||||
.map { it?.firmware_edition }
|
||||
.distinctUntilChanged()
|
||||
.collect { firmwareEdition ->
|
||||
_state.update { state -> state.copy(firmwareEdition = firmwareEdition) }
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d { "MetricsViewModel created" }
|
||||
} else {
|
||||
Logger.d { "MetricsViewModel: destNum is null, skipping metrics flows initialization." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Logger.d { "MetricsViewModel cleared" }
|
||||
}
|
||||
|
||||
/** Write the persisted Position data out to a CSV file in the specified location. */
|
||||
fun savePositionCSV(uri: Uri) = viewModelScope.launch(dispatchers.main) {
|
||||
val positions = state.value.positionLogs
|
||||
writeToUri(uri) { writer ->
|
||||
|
|
@ -518,7 +351,6 @@ constructor(
|
|||
val speed = position.ground_speed
|
||||
val heading = "%.2f".format((position.ground_track ?: 0) * 1e-5)
|
||||
|
||||
// date,time,latitude,longitude,altitude,satsInView,speed,heading
|
||||
writer.appendLine(
|
||||
"$rxDateTime,\"$latitude\",\"$longitude\",\"$altitude\",\"$satsInView\",\"$speed\",\"$heading\"",
|
||||
)
|
||||
|
|
@ -541,12 +373,10 @@ constructor(
|
|||
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod", "ReturnCount")
|
||||
fun decodePaxFromLog(log: MeshLog): ProtoPaxcount? {
|
||||
// First, try to parse from the binary fromRadio field (robust, like telemetry)
|
||||
try {
|
||||
val packet = log.fromRadio.packet
|
||||
val decoded = packet?.decoded
|
||||
if (packet != null && decoded != null && decoded.portnum == PortNum.PAXCOUNTER_APP) {
|
||||
// Requests for paxcount (want_response = true) should not be logged as data points.
|
||||
if (decoded.want_response == true) return null
|
||||
val pax = ProtoPaxcount.ADAPTER.decode(decoded.payload)
|
||||
if ((pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0 || (pax.uptime ?: 0) != 0) return pax
|
||||
|
|
@ -554,10 +384,9 @@ constructor(
|
|||
} catch (e: IOException) {
|
||||
Logger.e(e) { "Failed to parse Paxcount from binary data" }
|
||||
}
|
||||
// Fallback: Attempt to parse Paxcount from raw_message as base64 or hex string.
|
||||
try {
|
||||
val base64 = log.raw_message.trim()
|
||||
if (base64.matches(Regex("^[A-Za-z0-9+/=\r\n]+$"))) {
|
||||
if (base64.matches(Regex("^[A-Za-z0-9+/=\\r\\n]+$"))) {
|
||||
val bytes = android.util.Base64.decode(base64, android.util.Base64.DEFAULT)
|
||||
return ProtoPaxcount.ADAPTER.decode(bytes)
|
||||
} else if (base64.matches(Regex("^[0-9a-fA-F]+$")) && base64.length % 2 == 0) {
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.getNeighborInfoResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.neighbor_info
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.neighbor_info
|
||||
import org.meshtastic.core.resources.routing_error_no_response
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.Groups
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -77,7 +76,7 @@ fun NeighborInfoLogScreen(
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +136,7 @@ fun NeighborInfoLogScreen(
|
|||
)
|
||||
val text = if (result != null) "Success" else stringResource(Res.string.routing_error_no_response)
|
||||
val icon = if (result != null) MeshtasticIcons.Groups else MeshtasticIcons.PersonOff
|
||||
val header = stringResource(Res.string.neighbor_info)
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
|
|
@ -149,10 +149,7 @@ fun NeighborInfoLogScreen(
|
|||
result
|
||||
?.fromRadio
|
||||
?.packet
|
||||
?.getNeighborInfoResponse(
|
||||
::getUsername,
|
||||
header = getString(Res.string.neighbor_info),
|
||||
)
|
||||
?.getNeighborInfoResponse(::getUsername, header = header)
|
||||
?.let {
|
||||
val message =
|
||||
annotateNeighborInfo(
|
||||
|
|
|
|||
|
|
@ -59,14 +59,13 @@ import org.meshtastic.core.common.util.toInstant
|
|||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.ble_devices
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.no_pax_metrics_logs
|
||||
import org.meshtastic.core.strings.pax
|
||||
import org.meshtastic.core.strings.pax_metrics_log
|
||||
import org.meshtastic.core.strings.uptime
|
||||
import org.meshtastic.core.strings.wifi_devices
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.ble_devices
|
||||
import org.meshtastic.core.resources.no_pax_metrics_logs
|
||||
import org.meshtastic.core.resources.pax
|
||||
import org.meshtastic.core.resources.pax_metrics_log
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.resources.wifi_devices
|
||||
import org.meshtastic.core.ui.component.IconInfo
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.Paxcount
|
||||
|
|
@ -189,7 +188,7 @@ fun PaxMetricsScreen(metricsViewModel: MetricsViewModel = hiltViewModel(), onNav
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,17 +65,16 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.alt
|
||||
import org.meshtastic.core.strings.clear
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.heading
|
||||
import org.meshtastic.core.strings.latitude
|
||||
import org.meshtastic.core.strings.longitude
|
||||
import org.meshtastic.core.strings.sats
|
||||
import org.meshtastic.core.strings.save
|
||||
import org.meshtastic.core.strings.speed
|
||||
import org.meshtastic.core.strings.timestamp
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.alt
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.heading
|
||||
import org.meshtastic.core.resources.latitude
|
||||
import org.meshtastic.core.resources.longitude
|
||||
import org.meshtastic.core.resources.sats
|
||||
import org.meshtastic.core.resources.save
|
||||
import org.meshtastic.core.resources.speed
|
||||
import org.meshtastic.core.resources.timestamp
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.Delete
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -182,7 +181,7 @@ fun PositionLogScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateU
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,13 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_1
|
||||
import org.meshtastic.core.strings.channel_2
|
||||
import org.meshtastic.core.strings.channel_3
|
||||
import org.meshtastic.core.strings.current
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.power_metrics_log
|
||||
import org.meshtastic.core.strings.voltage
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_1
|
||||
import org.meshtastic.core.resources.channel_2
|
||||
import org.meshtastic.core.resources.channel_3
|
||||
import org.meshtastic.core.resources.current
|
||||
import org.meshtastic.core.resources.power_metrics_log
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Gold
|
||||
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
|
||||
import org.meshtastic.feature.node.detail.NodeRequestEffect
|
||||
|
|
@ -121,7 +120,7 @@ fun PowerMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigate
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,13 +58,12 @@ import com.patrykandpatrick.vico.compose.cartesian.data.lineSeries
|
|||
import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer
|
||||
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.rssi
|
||||
import org.meshtastic.core.strings.rssi_definition
|
||||
import org.meshtastic.core.strings.signal_quality
|
||||
import org.meshtastic.core.strings.snr
|
||||
import org.meshtastic.core.strings.snr_definition
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.rssi
|
||||
import org.meshtastic.core.resources.rssi_definition
|
||||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.snr
|
||||
import org.meshtastic.core.resources.snr_definition
|
||||
import org.meshtastic.core.ui.component.LoraSignalIndicator
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Blue
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Green
|
||||
|
|
@ -98,7 +97,7 @@ fun SignalMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,18 +52,17 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.fullRouteDiscovery
|
||||
import org.meshtastic.core.model.getTracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.getString
|
||||
import org.meshtastic.core.strings.routing_error_no_response
|
||||
import org.meshtastic.core.strings.traceroute
|
||||
import org.meshtastic.core.strings.traceroute_diff
|
||||
import org.meshtastic.core.strings.traceroute_direct
|
||||
import org.meshtastic.core.strings.traceroute_duration
|
||||
import org.meshtastic.core.strings.traceroute_hops
|
||||
import org.meshtastic.core.strings.traceroute_log
|
||||
import org.meshtastic.core.strings.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.strings.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.strings.traceroute_time_and_text
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.routing_error_no_response
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.traceroute_diff
|
||||
import org.meshtastic.core.resources.traceroute_direct
|
||||
import org.meshtastic.core.resources.traceroute_duration
|
||||
import org.meshtastic.core.resources.traceroute_hops
|
||||
import org.meshtastic.core.resources.traceroute_log
|
||||
import org.meshtastic.core.resources.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.resources.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.resources.traceroute_time_and_text
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.icon.Group
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
|
|
@ -98,7 +97,7 @@ fun TracerouteLogScreen(
|
|||
when (effect) {
|
||||
is NodeRequestEffect.ShowFeedback -> {
|
||||
@Suppress("SpreadOperator")
|
||||
snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
|
||||
snackbarHostState.showSnackbar(effect.text.resolve())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,6 +141,8 @@ fun TracerouteLogScreen(
|
|||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
items(state.tracerouteRequests, key = { it.uuid }) { log ->
|
||||
val headerTowardsStr = stringResource(Res.string.traceroute_route_towards_dest)
|
||||
val headerBackStr = stringResource(Res.string.traceroute_route_back_to_us)
|
||||
val result =
|
||||
remember(state.tracerouteRequests, log.fromRadio.packet?.id) {
|
||||
state.tracerouteResults.find {
|
||||
|
|
@ -169,7 +170,7 @@ fun TracerouteLogScreen(
|
|||
res.fromRadio.packet?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = stringResource(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = stringResource(Res.string.traceroute_route_back_to_us),
|
||||
headerBack = headerBackStr,
|
||||
),
|
||||
statusGreen = statusGreen,
|
||||
statusYellow = statusYellow,
|
||||
|
|
@ -186,7 +187,7 @@ fun TracerouteLogScreen(
|
|||
?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = stringResource(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = stringResource(Res.string.traceroute_route_back_to_us),
|
||||
headerBack = headerBackStr,
|
||||
)
|
||||
?.let { AnnotatedString(it) }
|
||||
}
|
||||
|
|
@ -214,8 +215,8 @@ fun TracerouteLogScreen(
|
|||
?.packet
|
||||
?.getTracerouteResponse(
|
||||
::getUsername,
|
||||
headerTowards = getString(Res.string.traceroute_route_towards_dest),
|
||||
headerBack = getString(Res.string.traceroute_route_back_to_us),
|
||||
headerTowards = headerTowardsStr,
|
||||
headerBack = headerBackStr,
|
||||
)
|
||||
?.let {
|
||||
annotateTraceroute(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue