2024-11-26 08:38:12 -03:00
|
|
|
/*
|
2025-01-02 06:50:26 -03:00
|
|
|
* Copyright (c) 2025 Meshtastic LLC
|
2024-11-26 08:38:12 -03:00
|
|
|
*
|
|
|
|
|
* 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/>.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-02-17 13:34:52 -08:00
|
|
|
package com.geeksville.mesh.model
|
|
|
|
|
|
2020-04-09 16:33:42 -07:00
|
|
|
import android.app.Application
|
2020-03-17 11:35:19 -07:00
|
|
|
import android.net.Uri
|
2020-02-18 12:22:45 -08:00
|
|
|
import android.os.RemoteException
|
2025-08-20 18:49:06 -04:00
|
|
|
import androidx.compose.material3.SnackbarDuration
|
2025-05-17 11:39:53 -05:00
|
|
|
import androidx.compose.material3.SnackbarHostState
|
2025-08-20 18:49:06 -04:00
|
|
|
import androidx.compose.material3.SnackbarResult
|
2022-04-22 17:22:06 -03:00
|
|
|
import androidx.lifecycle.LiveData
|
2022-02-08 13:50:21 -08:00
|
|
|
import androidx.lifecycle.ViewModel
|
2022-09-15 22:24:04 -03:00
|
|
|
import androidx.lifecycle.asLiveData
|
2020-09-23 22:47:45 -04:00
|
|
|
import androidx.lifecycle.viewModelScope
|
2025-05-20 13:36:11 -05:00
|
|
|
import com.geeksville.mesh.AdminProtos
|
2024-11-26 09:00:44 -03:00
|
|
|
import com.geeksville.mesh.AppOnlyProtos
|
|
|
|
|
import com.geeksville.mesh.ChannelProtos
|
2023-05-21 06:04:53 -03:00
|
|
|
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
2022-09-12 00:26:12 -03:00
|
|
|
import com.geeksville.mesh.ConfigProtos.Config
|
|
|
|
|
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
2022-11-22 22:01:37 -03:00
|
|
|
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
2024-11-26 09:00:44 -03:00
|
|
|
import com.geeksville.mesh.MeshProtos
|
|
|
|
|
import com.geeksville.mesh.channel
|
|
|
|
|
import com.geeksville.mesh.channelSet
|
|
|
|
|
import com.geeksville.mesh.channelSettings
|
|
|
|
|
import com.geeksville.mesh.config
|
|
|
|
|
import com.geeksville.mesh.copy
|
2025-08-07 14:17:01 -05:00
|
|
|
import com.geeksville.mesh.repository.radio.MeshActivity
|
2023-05-24 06:43:58 -03:00
|
|
|
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
2025-06-09 19:45:20 +02:00
|
|
|
import com.geeksville.mesh.service.MeshServiceNotifications
|
2022-02-08 13:50:21 -08:00
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
2020-09-23 22:47:45 -04:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2022-02-08 13:50:21 -08:00
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
2025-08-07 14:17:01 -05:00
|
|
|
import kotlinx.coroutines.flow.SharedFlow
|
2024-11-19 11:59:28 -03:00
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
2022-02-08 13:50:21 -08:00
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
2025-05-15 08:05:30 -05:00
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
2023-10-12 22:52:54 -03:00
|
|
|
import kotlinx.coroutines.flow.filterNotNull
|
2022-11-08 23:11:18 -03:00
|
|
|
import kotlinx.coroutines.flow.launchIn
|
2025-05-26 19:36:32 -05:00
|
|
|
import kotlinx.coroutines.flow.map
|
2025-06-03 19:58:21 -05:00
|
|
|
import kotlinx.coroutines.flow.mapNotNull
|
2022-11-08 23:11:18 -03:00
|
|
|
import kotlinx.coroutines.flow.onEach
|
2025-08-07 14:17:01 -05:00
|
|
|
import kotlinx.coroutines.flow.shareIn
|
2024-05-27 09:56:26 -03:00
|
|
|
import kotlinx.coroutines.flow.stateIn
|
2020-09-23 22:47:45 -04:00
|
|
|
import kotlinx.coroutines.launch
|
2025-09-26 17:45:11 -04:00
|
|
|
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.QuickChatActionRepository
|
|
|
|
|
import org.meshtastic.core.data.repository.RadioConfigRepository
|
2025-09-24 16:23:05 -04:00
|
|
|
import org.meshtastic.core.database.entity.MyNodeEntity
|
|
|
|
|
import org.meshtastic.core.database.entity.QuickChatAction
|
|
|
|
|
import org.meshtastic.core.database.entity.asDeviceVersion
|
|
|
|
|
import org.meshtastic.core.database.model.Node
|
2025-09-23 15:52:09 -04:00
|
|
|
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
2025-09-26 17:45:11 -04:00
|
|
|
import org.meshtastic.core.model.util.toChannelSet
|
2025-09-30 16:55:56 -04:00
|
|
|
import org.meshtastic.core.service.IMeshService
|
|
|
|
|
import org.meshtastic.core.service.ServiceRepository
|
2025-09-22 20:59:39 -05:00
|
|
|
import org.meshtastic.core.strings.R
|
2025-09-30 18:22:22 -05:00
|
|
|
import timber.log.Timber
|
2022-02-08 13:50:21 -08:00
|
|
|
import javax.inject.Inject
|
2020-04-08 09:53:04 -07:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
// Given a human name, strip out the first letter of the first three words and return that as the
|
|
|
|
|
// initials for
|
2025-02-15 23:25:35 -05:00
|
|
|
// that user, ignoring emojis. If the original name is only one word, strip vowels from the original
|
|
|
|
|
// name and if the result is 3 or more characters, use the first three characters. If not, just take
|
|
|
|
|
// the first 3 characters of the original name.
|
2025-08-13 12:51:19 -05:00
|
|
|
fun getInitials(fullName: String): String {
|
|
|
|
|
val maxInitialLength = 4
|
|
|
|
|
val minWordCountForInitials = 2
|
|
|
|
|
val name = fullName.trim().withoutEmojis()
|
2020-09-17 00:37:51 -07:00
|
|
|
val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }
|
2020-04-08 09:53:04 -07:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val initials =
|
|
|
|
|
when (words.size) {
|
2025-08-13 12:51:19 -05:00
|
|
|
in 0 until minWordCountForInitials -> {
|
|
|
|
|
val nameWithoutVowels =
|
2025-07-25 07:07:01 -05:00
|
|
|
if (name.isNotEmpty()) {
|
|
|
|
|
name.first() + name.drop(1).filterNot { c -> c.lowercase() in "aeiou" }
|
|
|
|
|
} else {
|
|
|
|
|
""
|
|
|
|
|
}
|
2025-08-13 12:51:19 -05:00
|
|
|
if (nameWithoutVowels.length >= maxInitialLength) nameWithoutVowels else name
|
2024-10-13 23:02:05 -03:00
|
|
|
}
|
2025-05-17 11:39:53 -05:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
else -> words.map { it.first() }.joinToString("")
|
|
|
|
|
}
|
2025-08-13 12:51:19 -05:00
|
|
|
return initials.take(maxInitialLength)
|
2020-04-08 09:53:04 -07:00
|
|
|
}
|
2020-03-15 16:30:12 -07:00
|
|
|
|
2025-02-15 23:25:35 -05:00
|
|
|
private fun String.withoutEmojis(): String = filterNot { char -> char.isSurrogate() }
|
|
|
|
|
|
2023-09-16 09:51:16 -03:00
|
|
|
/**
|
2025-07-25 07:07:01 -05:00
|
|
|
* Builds a [Channel] list from the difference between two [ChannelSettings] lists. Only changes are included in the
|
|
|
|
|
* resulting list.
|
2023-10-06 18:38:06 -03:00
|
|
|
*
|
|
|
|
|
* @param new The updated [ChannelSettings] list.
|
2024-07-28 04:50:54 -07:00
|
|
|
* @param old The current [ChannelSettings] list (required when disabling unused channels).
|
2023-10-06 18:38:06 -03:00
|
|
|
* @return A [Channel] list containing only the modified channels.
|
2023-09-16 09:51:16 -03:00
|
|
|
*/
|
2025-07-25 07:07:01 -05:00
|
|
|
internal fun getChannelList(new: List<ChannelSettings>, old: List<ChannelSettings>): List<ChannelProtos.Channel> =
|
|
|
|
|
buildList {
|
|
|
|
|
for (i in 0..maxOf(old.lastIndex, new.lastIndex)) {
|
|
|
|
|
if (old.getOrNull(i) != new.getOrNull(i)) {
|
|
|
|
|
add(
|
|
|
|
|
channel {
|
|
|
|
|
role =
|
|
|
|
|
when (i) {
|
|
|
|
|
0 -> ChannelProtos.Channel.Role.PRIMARY
|
|
|
|
|
in 1..new.lastIndex -> ChannelProtos.Channel.Role.SECONDARY
|
|
|
|
|
else -> ChannelProtos.Channel.Role.DISABLED
|
|
|
|
|
}
|
|
|
|
|
index = i
|
|
|
|
|
settings = new.getOrNull(i) ?: channelSettings {}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-05-15 08:05:30 -05:00
|
|
|
}
|
2023-10-06 18:38:06 -03:00
|
|
|
}
|
2025-05-17 11:39:53 -05:00
|
|
|
|
2024-09-09 19:57:45 -03:00
|
|
|
data class Contact(
|
|
|
|
|
val contactKey: String,
|
|
|
|
|
val shortName: String,
|
|
|
|
|
val longName: String,
|
|
|
|
|
val lastMessageTime: String?,
|
|
|
|
|
val lastMessageText: String?,
|
|
|
|
|
val unreadCount: Int,
|
|
|
|
|
val messageCount: Int,
|
|
|
|
|
val isMuted: Boolean,
|
2025-05-20 16:05:40 -05:00
|
|
|
val isUnmessageable: Boolean,
|
2025-07-25 07:07:01 -05:00
|
|
|
val nodeColors: Pair<Int, Int>? = null,
|
2024-09-09 19:57:45 -03:00
|
|
|
)
|
|
|
|
|
|
2025-08-13 12:51:19 -05:00
|
|
|
@Suppress("LongParameterList", "LargeClass", "UnusedPrivateProperty")
|
2022-02-08 13:50:21 -08:00
|
|
|
@HiltViewModel
|
2025-07-25 07:07:01 -05:00
|
|
|
class UIViewModel
|
|
|
|
|
@Inject
|
|
|
|
|
constructor(
|
2022-02-08 13:50:21 -08:00
|
|
|
private val app: Application,
|
2024-11-26 09:57:23 -03:00
|
|
|
private val nodeDB: NodeRepository,
|
2023-05-21 06:04:53 -03:00
|
|
|
private val radioConfigRepository: RadioConfigRepository,
|
2025-09-25 09:57:26 -04:00
|
|
|
private val serviceRepository: ServiceRepository,
|
2025-08-13 12:51:19 -05:00
|
|
|
radioInterfaceService: RadioInterfaceService,
|
2025-09-26 19:54:31 -04:00
|
|
|
meshLogRepository: MeshLogRepository,
|
2022-08-11 16:43:26 +01:00
|
|
|
private val quickChatActionRepository: QuickChatActionRepository,
|
2025-06-03 19:58:21 -05:00
|
|
|
firmwareReleaseRepository: FirmwareReleaseRepository,
|
2025-09-23 15:52:09 -04:00
|
|
|
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
2025-07-25 07:07:01 -05:00
|
|
|
private val meshServiceNotifications: MeshServiceNotifications,
|
2025-09-30 18:22:22 -05:00
|
|
|
) : ViewModel() {
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2025-09-23 15:52:09 -04:00
|
|
|
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
|
2025-05-17 11:39:53 -05:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
|
|
|
|
|
|
2025-09-25 09:57:26 -04:00
|
|
|
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = serviceRepository.clientNotification
|
2025-06-20 23:19:51 +00:00
|
|
|
|
2025-06-16 17:06:23 +00:00
|
|
|
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
|
2025-09-25 09:57:26 -04:00
|
|
|
serviceRepository.clearClientNotification()
|
2025-06-16 17:06:23 +00:00
|
|
|
meshServiceNotifications.clearClientNotification(notification)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 14:17:01 -05:00
|
|
|
/**
|
|
|
|
|
* Emits events for mesh network send/receive activity. This is a SharedFlow to ensure all events are delivered,
|
|
|
|
|
* even if they are the same.
|
|
|
|
|
*/
|
|
|
|
|
val meshActivity: SharedFlow<MeshActivity> =
|
|
|
|
|
radioInterfaceService.meshActivity.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
|
|
|
|
|
|
2025-05-17 11:39:53 -05:00
|
|
|
data class AlertData(
|
|
|
|
|
val title: String,
|
|
|
|
|
val message: String? = null,
|
|
|
|
|
val html: String? = null,
|
|
|
|
|
val onConfirm: (() -> Unit)? = null,
|
|
|
|
|
val onDismiss: (() -> Unit)? = null,
|
2025-06-16 17:06:23 +00:00
|
|
|
val choices: Map<String, () -> Unit> = emptyMap(),
|
2025-05-17 11:39:53 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private val _currentAlert: MutableStateFlow<AlertData?> = MutableStateFlow(null)
|
|
|
|
|
val currentAlert = _currentAlert.asStateFlow()
|
|
|
|
|
|
|
|
|
|
fun showAlert(
|
|
|
|
|
title: String,
|
|
|
|
|
message: String? = null,
|
|
|
|
|
html: String? = null,
|
|
|
|
|
onConfirm: (() -> Unit)? = {},
|
|
|
|
|
dismissable: Boolean = true,
|
2025-06-16 17:06:23 +00:00
|
|
|
choices: Map<String, () -> Unit> = emptyMap(),
|
2025-05-17 11:39:53 -05:00
|
|
|
) {
|
|
|
|
|
_currentAlert.value =
|
|
|
|
|
AlertData(
|
|
|
|
|
title = title,
|
|
|
|
|
message = message,
|
|
|
|
|
html = html,
|
|
|
|
|
onConfirm = {
|
|
|
|
|
onConfirm?.invoke()
|
2025-05-31 20:36:35 -05:00
|
|
|
dismissAlert()
|
2025-05-17 11:39:53 -05:00
|
|
|
},
|
2025-07-25 07:07:01 -05:00
|
|
|
onDismiss = { if (dismissable) dismissAlert() },
|
2025-06-16 17:06:23 +00:00
|
|
|
choices = choices,
|
2025-05-17 11:39:53 -05:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun dismissAlert() {
|
|
|
|
|
_currentAlert.value = null
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val meshService: IMeshService?
|
2025-09-25 09:57:26 -04:00
|
|
|
get() = serviceRepository.meshService
|
2023-02-02 17:13:44 -03:00
|
|
|
|
2025-09-18 07:40:33 -04:00
|
|
|
private val localConfig = MutableStateFlow<LocalConfig>(LocalConfig.getDefaultInstance())
|
|
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val config
|
2025-09-18 07:40:33 -04:00
|
|
|
get() = localConfig.value
|
2022-06-20 22:46:45 -03:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
private val _moduleConfig = MutableStateFlow<LocalModuleConfig>(LocalModuleConfig.getDefaultInstance())
|
2022-11-22 22:01:37 -03:00
|
|
|
val moduleConfig: StateFlow<LocalModuleConfig> = _moduleConfig
|
2025-07-25 07:07:01 -05:00
|
|
|
val module
|
|
|
|
|
get() = _moduleConfig.value
|
2022-11-22 22:01:37 -03:00
|
|
|
|
2023-10-07 08:22:12 -03:00
|
|
|
private val _channels = MutableStateFlow(channelSet {})
|
2025-07-25 07:07:01 -05:00
|
|
|
val channels: StateFlow<AppOnlyProtos.ChannelSet>
|
|
|
|
|
get() = _channels
|
2022-09-12 19:07:30 -03:00
|
|
|
|
2025-05-17 11:39:53 -05:00
|
|
|
val quickChatActions
|
2025-07-25 07:07:01 -05:00
|
|
|
get() =
|
|
|
|
|
quickChatActionRepository
|
|
|
|
|
.getAllActions()
|
|
|
|
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
2022-08-11 16:43:26 +01:00
|
|
|
|
2023-10-20 18:31:13 -03:00
|
|
|
// hardware info about our local device (can be null)
|
2025-07-25 07:07:01 -05:00
|
|
|
val myNodeInfo: StateFlow<MyNodeEntity?>
|
|
|
|
|
get() = nodeDB.myNodeInfo
|
|
|
|
|
|
|
|
|
|
val ourNodeInfo: StateFlow<Node?>
|
|
|
|
|
get() = nodeDB.ourNodeInfo
|
2024-09-11 20:01:16 -03:00
|
|
|
|
2025-08-20 18:49:06 -04:00
|
|
|
val snackBarHostState = SnackbarHostState()
|
2025-07-25 07:07:01 -05:00
|
|
|
|
2025-08-13 12:51:19 -05:00
|
|
|
fun showSnackBar(text: Int) = showSnackBar(app.getString(text))
|
2025-07-25 07:07:01 -05:00
|
|
|
|
2025-08-20 18:49:06 -04:00
|
|
|
fun showSnackBar(
|
|
|
|
|
text: String,
|
|
|
|
|
actionLabel: String? = null,
|
|
|
|
|
withDismissAction: Boolean = false,
|
|
|
|
|
duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
|
|
|
|
|
onActionPerformed: (() -> Unit) = {},
|
|
|
|
|
onDismissed: (() -> Unit) = {},
|
|
|
|
|
) = viewModelScope.launch {
|
|
|
|
|
snackBarHostState.showSnackbar(text, actionLabel, withDismissAction, duration).run {
|
|
|
|
|
when (this) {
|
|
|
|
|
SnackbarResult.ActionPerformed -> onActionPerformed()
|
|
|
|
|
SnackbarResult.Dismissed -> onDismissed()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-10-19 17:16:16 -03:00
|
|
|
|
2020-04-07 17:42:31 -07:00
|
|
|
init {
|
2025-09-25 09:57:26 -04:00
|
|
|
serviceRepository.errorMessage
|
2025-07-25 07:07:01 -05:00
|
|
|
.filterNotNull()
|
|
|
|
|
.onEach {
|
|
|
|
|
showAlert(
|
|
|
|
|
title = app.getString(R.string.client_notification),
|
|
|
|
|
message = it,
|
2025-09-25 09:57:26 -04:00
|
|
|
onConfirm = { serviceRepository.clearErrorMessage() },
|
2025-07-25 07:07:01 -05:00
|
|
|
dismissable = false,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.launchIn(viewModelScope)
|
|
|
|
|
|
2025-09-18 07:40:33 -04:00
|
|
|
radioConfigRepository.localConfigFlow.onEach { config -> localConfig.value = config }.launchIn(viewModelScope)
|
2025-07-25 07:07:01 -05:00
|
|
|
radioConfigRepository.moduleConfigFlow
|
|
|
|
|
.onEach { config -> _moduleConfig.value = config }
|
|
|
|
|
.launchIn(viewModelScope)
|
|
|
|
|
radioConfigRepository.channelSetFlow
|
|
|
|
|
.onEach { channelSet -> _channels.value = channelSet }
|
|
|
|
|
.launchIn(viewModelScope)
|
2023-04-22 12:06:25 -03:00
|
|
|
|
2025-09-30 18:22:22 -05:00
|
|
|
Timber.d("ViewModel created")
|
2020-04-07 17:42:31 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
|
|
|
|
|
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
|
|
|
|
|
get() = _sharedContactRequested.asStateFlow()
|
|
|
|
|
|
2025-07-03 12:42:04 +00:00
|
|
|
fun setSharedContactRequested(sharedContact: AdminProtos.SharedContact?) {
|
2025-08-25 06:58:50 -05:00
|
|
|
_sharedContactRequested.value = sharedContact
|
2025-07-03 12:42:04 +00:00
|
|
|
}
|
|
|
|
|
|
2023-10-21 07:24:46 -03:00
|
|
|
// Connection state to our radio device
|
2025-07-25 07:07:01 -05:00
|
|
|
val connectionState
|
2025-09-25 09:57:26 -04:00
|
|
|
get() = serviceRepository.connectionState
|
2025-07-25 07:07:01 -05:00
|
|
|
|
2024-11-20 18:40:24 -03:00
|
|
|
private val _requestChannelSet = MutableStateFlow<AppOnlyProtos.ChannelSet?>(null)
|
2025-07-25 07:07:01 -05:00
|
|
|
val requestChannelSet: StateFlow<AppOnlyProtos.ChannelSet?>
|
|
|
|
|
get() = _requestChannelSet
|
2022-05-17 17:29:21 -03:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
fun requestChannelUrl(url: Uri) = runCatching { _requestChannelSet.value = url.toChannelSet() }
|
|
|
|
|
.onFailure { ex ->
|
2025-09-30 18:22:22 -05:00
|
|
|
Timber.e(ex, "Channel url error")
|
2025-08-13 12:51:19 -05:00
|
|
|
showSnackBar(R.string.channel_invalid)
|
2025-07-25 07:07:01 -05:00
|
|
|
}
|
2022-05-17 17:29:21 -03:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val latestStableFirmwareRelease = firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() }
|
2025-06-03 19:58:21 -05:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
/** Called immediately after activity observes requestChannelUrl */
|
2022-05-17 17:29:21 -03:00
|
|
|
fun clearRequestChannelUrl() {
|
2024-11-20 18:40:24 -03:00
|
|
|
_requestChannelSet.value = null
|
2022-05-17 17:29:21 -03:00
|
|
|
}
|
|
|
|
|
|
2022-09-12 00:26:12 -03:00
|
|
|
var region: Config.LoRaConfig.RegionCode
|
2022-09-18 18:35:13 -03:00
|
|
|
get() = config.lora.region
|
2021-03-04 10:25:24 +08:00
|
|
|
set(value) {
|
2022-09-12 00:26:12 -03:00
|
|
|
updateLoraConfig { it.copy { region = value } }
|
2021-02-05 21:29:28 -08:00
|
|
|
}
|
|
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
override fun onCleared() {
|
|
|
|
|
super.onCleared()
|
2025-09-30 18:22:22 -05:00
|
|
|
Timber.d("ViewModel cleared")
|
2020-04-08 09:53:04 -07:00
|
|
|
}
|
|
|
|
|
|
2023-04-22 12:06:25 -03:00
|
|
|
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
|
2022-09-12 00:26:12 -03:00
|
|
|
val data = body(config.lora)
|
2022-10-11 16:27:36 -03:00
|
|
|
setConfig(config { lora = data })
|
2022-09-12 00:26:12 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the radio config (also updates our saved copy in preferences)
|
2022-10-11 16:27:36 -03:00
|
|
|
fun setConfig(config: Config) {
|
2024-09-24 07:44:25 -03:00
|
|
|
try {
|
|
|
|
|
meshService?.setConfig(config.toByteArray())
|
|
|
|
|
} catch (ex: RemoteException) {
|
2025-09-30 18:22:22 -05:00
|
|
|
Timber.e(ex, "Set config error")
|
2024-09-24 07:44:25 -03:00
|
|
|
}
|
2021-02-27 11:44:05 +08:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
fun addQuickChatAction(action: QuickChatAction) =
|
|
|
|
|
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }
|
2022-08-11 16:43:26 +01:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
fun deleteQuickChatAction(action: QuickChatAction) =
|
|
|
|
|
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.delete(action) }
|
2022-08-12 15:35:27 +01:00
|
|
|
|
2022-08-16 11:46:57 +01:00
|
|
|
fun updateActionPositions(actions: List<QuickChatAction>) {
|
2024-11-19 11:59:28 -03:00
|
|
|
viewModelScope.launch(Dispatchers.IO) {
|
2022-08-16 12:25:40 +01:00
|
|
|
for (position in actions.indices) {
|
2022-08-16 11:46:57 +01:00
|
|
|
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
|
|
|
|
|
}
|
2022-08-12 15:35:27 +01:00
|
|
|
}
|
|
|
|
|
}
|
2020-04-08 09:53:04 -07:00
|
|
|
|
2024-04-07 16:26:47 -03:00
|
|
|
val tracerouteResponse: LiveData<String?>
|
2025-09-25 09:57:26 -04:00
|
|
|
get() = serviceRepository.tracerouteResponse.asLiveData()
|
2023-09-16 09:51:16 -03:00
|
|
|
|
|
|
|
|
fun clearTracerouteResponse() {
|
2025-09-25 09:57:26 -04:00
|
|
|
serviceRepository.clearTracerouteResponse()
|
2023-09-16 09:51:16 -03:00
|
|
|
}
|
2024-02-13 14:32:52 -07:00
|
|
|
|
2025-09-23 15:52:09 -04:00
|
|
|
val appIntroCompleted: StateFlow<Boolean> = uiPreferencesDataSource.appIntroCompleted
|
2025-08-16 14:58:21 -04:00
|
|
|
|
|
|
|
|
fun onAppIntroCompleted() {
|
2025-09-23 15:52:09 -04:00
|
|
|
uiPreferencesDataSource.setAppIntroCompleted(true)
|
2025-08-16 14:58:21 -04:00
|
|
|
}
|
2023-09-16 09:51:16 -03:00
|
|
|
}
|