kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: migrate to Compose navigation
rodzic
d14a8de78e
commit
04d94a6eb1
|
@ -151,10 +151,10 @@ dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.15.0'
|
implementation 'androidx.core:core-ktx:1.15.0'
|
||||||
implementation 'androidx.core:core-location-altitude:1.0.0-alpha03'
|
implementation 'androidx.core:core-location-altitude:1.0.0-alpha03'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.8.5'
|
implementation 'androidx.fragment:fragment-ktx:1.8.5'
|
||||||
|
implementation 'androidx.fragment:fragment-compose:1.8.5'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0'
|
|
||||||
implementation 'androidx.datastore:datastore:1.1.1'
|
implementation 'androidx.datastore:datastore:1.1.1'
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
@ -197,7 +197,6 @@ dependencies {
|
||||||
implementation 'androidx.compose.material:material-icons-extended'
|
implementation 'androidx.compose.material:material-icons-extended'
|
||||||
implementation 'androidx.activity:activity-compose'
|
implementation 'androidx.activity:activity-compose'
|
||||||
implementation 'androidx.compose.runtime:runtime-livedata'
|
implementation 'androidx.compose.runtime:runtime-livedata'
|
||||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.36.0"
|
|
||||||
|
|
||||||
// Android Studio Preview support
|
// Android Studio Preview support
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
|
|
|
@ -26,29 +26,17 @@ import android.content.pm.PackageManager
|
||||||
import android.hardware.usb.UsbManager
|
import android.hardware.usb.UsbManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.FragmentTransaction
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.geeksville.mesh.android.BindFailedException
|
import com.geeksville.mesh.android.BindFailedException
|
||||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||||
import com.geeksville.mesh.android.Logging
|
import com.geeksville.mesh.android.Logging
|
||||||
|
@ -61,7 +49,6 @@ import com.geeksville.mesh.android.permissionMissing
|
||||||
import com.geeksville.mesh.android.rationaleDialog
|
import com.geeksville.mesh.android.rationaleDialog
|
||||||
import com.geeksville.mesh.android.shouldShowRequestPermissionRationale
|
import com.geeksville.mesh.android.shouldShowRequestPermissionRationale
|
||||||
import com.geeksville.mesh.concurrent.handledLaunch
|
import com.geeksville.mesh.concurrent.handledLaunch
|
||||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
|
||||||
import com.geeksville.mesh.model.BluetoothViewModel
|
import com.geeksville.mesh.model.BluetoothViewModel
|
||||||
import com.geeksville.mesh.model.DeviceVersion
|
import com.geeksville.mesh.model.DeviceVersion
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
|
@ -69,88 +56,23 @@ import com.geeksville.mesh.service.MeshService
|
||||||
import com.geeksville.mesh.service.MeshServiceNotifications
|
import com.geeksville.mesh.service.MeshServiceNotifications
|
||||||
import com.geeksville.mesh.service.ServiceRepository
|
import com.geeksville.mesh.service.ServiceRepository
|
||||||
import com.geeksville.mesh.service.startService
|
import com.geeksville.mesh.service.startService
|
||||||
import com.geeksville.mesh.ui.ChannelFragment
|
import com.geeksville.mesh.ui.MainMenuAction
|
||||||
import com.geeksville.mesh.ui.ContactsFragment
|
import com.geeksville.mesh.ui.MainScreen
|
||||||
import com.geeksville.mesh.ui.DebugFragment
|
|
||||||
import com.geeksville.mesh.ui.QuickChatSettingsFragment
|
|
||||||
import com.geeksville.mesh.ui.SettingsFragment
|
|
||||||
import com.geeksville.mesh.ui.UsersFragment
|
|
||||||
import com.geeksville.mesh.ui.components.ScannedQrCodeDialog
|
|
||||||
import com.geeksville.mesh.ui.map.MapFragment
|
|
||||||
import com.geeksville.mesh.ui.message.navigateToMessages
|
|
||||||
import com.geeksville.mesh.ui.navigateToNavGraph
|
|
||||||
import com.geeksville.mesh.ui.navigateToShareMessage
|
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import com.geeksville.mesh.util.Exceptions
|
import com.geeksville.mesh.util.Exceptions
|
||||||
import com.geeksville.mesh.util.LanguageUtils
|
import com.geeksville.mesh.util.LanguageUtils
|
||||||
import com.geeksville.mesh.util.getPackageInfoCompat
|
import com.geeksville.mesh.util.getPackageInfoCompat
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/*
|
|
||||||
UI design
|
|
||||||
|
|
||||||
material setup instructions: https://material.io/develop/android/docs/getting-started/
|
|
||||||
dark theme (or use system eventually) https://material.io/develop/android/theming/dark/
|
|
||||||
|
|
||||||
NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app
|
|
||||||
title.
|
|
||||||
|
|
||||||
Fragments:
|
|
||||||
|
|
||||||
SettingsFragment shows "Settings"
|
|
||||||
username
|
|
||||||
shortname
|
|
||||||
bluetooth pairing list
|
|
||||||
(eventually misc device settings that are not channel related)
|
|
||||||
|
|
||||||
Channel fragment "Channel"
|
|
||||||
qr code, copy link button
|
|
||||||
ch number
|
|
||||||
misc other settings
|
|
||||||
(eventually a way of choosing between past channels)
|
|
||||||
|
|
||||||
ChatFragment "Messages"
|
|
||||||
a text box to enter new texts
|
|
||||||
a scrolling list of rows. each row is a text and a sender info layout
|
|
||||||
|
|
||||||
NodeListFragment "Users"
|
|
||||||
a node info row for every node
|
|
||||||
|
|
||||||
ViewModels:
|
|
||||||
|
|
||||||
BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle)
|
|
||||||
|
|
||||||
MeshModel contains: (manages entire service relationship)
|
|
||||||
current received texts
|
|
||||||
current radio macaddr
|
|
||||||
current node infos (updated dynamically)
|
|
||||||
|
|
||||||
eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/
|
|
||||||
use numbers of # chat messages and # of members in the badges.
|
|
||||||
|
|
||||||
(per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs )
|
|
||||||
|
|
||||||
|
|
||||||
eventually:
|
|
||||||
make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder
|
|
||||||
*/
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity(), Logging {
|
class MainActivity : AppCompatActivity(), Logging {
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
|
||||||
|
|
||||||
// Used to schedule a coroutine in the GUI thread
|
// Used to schedule a coroutine in the GUI thread
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
|
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
|
||||||
|
|
||||||
|
@ -166,7 +88,7 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
info("Bluetooth permissions granted")
|
info("Bluetooth permissions granted")
|
||||||
} else {
|
} else {
|
||||||
warn("Bluetooth permissions denied")
|
warn("Bluetooth permissions denied")
|
||||||
showSnackbar(permissionMissing)
|
model.showSnackbar(permissionMissing)
|
||||||
}
|
}
|
||||||
requestedEnable = false
|
requestedEnable = false
|
||||||
bluetoothViewModel.permissionsUpdated()
|
bluetoothViewModel.permissionsUpdated()
|
||||||
|
@ -178,45 +100,10 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
info("Notification permissions granted")
|
info("Notification permissions granted")
|
||||||
} else {
|
} else {
|
||||||
warn("Notification permissions denied")
|
warn("Notification permissions denied")
|
||||||
showSnackbar(getString(R.string.notification_denied), Snackbar.LENGTH_SHORT)
|
model.showSnackbar(getString(R.string.notification_denied))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
|
|
||||||
|
|
||||||
private val tabInfos = arrayOf(
|
|
||||||
TabInfo(
|
|
||||||
"Messages",
|
|
||||||
R.drawable.ic_twotone_message_24,
|
|
||||||
ContactsFragment()
|
|
||||||
),
|
|
||||||
TabInfo(
|
|
||||||
"Users",
|
|
||||||
R.drawable.ic_twotone_people_24,
|
|
||||||
UsersFragment()
|
|
||||||
),
|
|
||||||
TabInfo(
|
|
||||||
"Map",
|
|
||||||
R.drawable.ic_twotone_map_24,
|
|
||||||
MapFragment()
|
|
||||||
),
|
|
||||||
TabInfo(
|
|
||||||
"Channel",
|
|
||||||
R.drawable.ic_twotone_contactless_24,
|
|
||||||
ChannelFragment()
|
|
||||||
),
|
|
||||||
TabInfo(
|
|
||||||
"Settings",
|
|
||||||
R.drawable.ic_twotone_settings_applications_24,
|
|
||||||
SettingsFragment()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val tabsAdapter = object : FragmentStateAdapter(supportFragmentManager, lifecycle) {
|
|
||||||
override fun getItemCount(): Int = tabInfos.size
|
|
||||||
override fun createFragment(position: Int): Fragment = tabInfos[position].content
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -239,45 +126,9 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
(application as GeeksvilleApplication).askToRate(this)
|
(application as GeeksvilleApplication).askToRate(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
setContent {
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
initToolbar()
|
|
||||||
|
|
||||||
binding.pager.adapter = tabsAdapter
|
|
||||||
binding.pager.isUserInputEnabled =
|
|
||||||
false // Gestures for screen switching doesn't work so good with the map view
|
|
||||||
// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops
|
|
||||||
TabLayoutMediator(binding.tabLayout, binding.pager, false, false) { tab, position ->
|
|
||||||
// tab.text = tabInfos[position].text // I think it looks better with icons only
|
|
||||||
tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon)
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
|
||||||
val mainTab = tab?.position ?: 0
|
|
||||||
model.setCurrentTab(mainTab)
|
|
||||||
}
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) { }
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) { }
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.composeView.setContent {
|
|
||||||
val connState by model.connectionState.collectAsStateWithLifecycle()
|
|
||||||
val channels by model.channels.collectAsStateWithLifecycle()
|
|
||||||
val requestChannelSet by model.requestChannelSet.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
AppTheme {
|
AppTheme {
|
||||||
if (connState.isConnected()) {
|
MainScreen(model, ::onMainMenuAction)
|
||||||
if (requestChannelSet != null) {
|
|
||||||
ScannedQrCodeDialog(
|
|
||||||
channels = channels,
|
|
||||||
incoming = requestChannelSet!!,
|
|
||||||
onDismiss = model::clearRequestChannelUrl,
|
|
||||||
onConfirm = model::setChannels,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,28 +136,6 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initToolbar() {
|
|
||||||
val toolbar = binding.toolbar as Toolbar
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateConnectionStatusImage(connected: MeshService.ConnectionState) {
|
|
||||||
if (model.actionBarMenu == null) return
|
|
||||||
|
|
||||||
val (image, tooltip) = when (connected) {
|
|
||||||
MeshService.ConnectionState.CONNECTED -> R.drawable.cloud_on to R.string.connected
|
|
||||||
MeshService.ConnectionState.DEVICE_SLEEP -> R.drawable.ic_twotone_cloud_upload_24 to R.string.device_sleeping
|
|
||||||
MeshService.ConnectionState.DISCONNECTED -> R.drawable.cloud_off to R.string.disconnected
|
|
||||||
}
|
|
||||||
|
|
||||||
val item = model.actionBarMenu?.findItem(R.id.connectStatusImage)
|
|
||||||
if (item != null) {
|
|
||||||
item.setIcon(image)
|
|
||||||
item.setTitle(tooltip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
|
@ -328,7 +157,9 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
MeshServiceNotifications.OPEN_MESSAGE_ACTION -> {
|
MeshServiceNotifications.OPEN_MESSAGE_ACTION -> {
|
||||||
val contactKey =
|
val contactKey =
|
||||||
intent.getStringExtra(MeshServiceNotifications.OPEN_MESSAGE_EXTRA_CONTACT_KEY)
|
intent.getStringExtra(MeshServiceNotifications.OPEN_MESSAGE_EXTRA_CONTACT_KEY)
|
||||||
showMessages(contactKey)
|
if (contactKey != null) {
|
||||||
|
// showMessages(contactKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||||
|
@ -341,7 +172,7 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
if (text != null) {
|
if (text != null) {
|
||||||
shareMessages(text)
|
// shareMessages(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -440,27 +271,6 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSnackbar(msgId: Int) {
|
|
||||||
try {
|
|
||||||
Snackbar.make(binding.root, msgId, Snackbar.LENGTH_LONG).show()
|
|
||||||
} catch (ex: IllegalStateException) {
|
|
||||||
errormsg("Snackbar couldn't find view for msgId $msgId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSnackbar(msg: String, duration: Int = Snackbar.LENGTH_INDEFINITE) {
|
|
||||||
try {
|
|
||||||
Snackbar.make(binding.root, msg, duration)
|
|
||||||
.apply { view.findViewById<TextView>(R.id.snackbar_text).isSingleLine = false }
|
|
||||||
.setAction(R.string.okay) {
|
|
||||||
// dismiss
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} catch (ex: IllegalStateException) {
|
|
||||||
errormsg("Snackbar couldn't find view for msgString $msg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||||
return try {
|
return try {
|
||||||
super.dispatchTouchEvent(ev)
|
super.dispatchTouchEvent(ev)
|
||||||
|
@ -475,10 +285,7 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
|
|
||||||
private var connectionJob: Job? = null
|
private var connectionJob: Job? = null
|
||||||
|
|
||||||
private val mesh = object :
|
private val mesh = object : ServiceClient<IMeshService>(IMeshService.Stub::asInterface) {
|
||||||
ServiceClient<IMeshService>({
|
|
||||||
IMeshService.Stub.asInterface(it)
|
|
||||||
}) {
|
|
||||||
override fun onConnected(service: IMeshService) {
|
override fun onConnected(service: IMeshService) {
|
||||||
connectionJob = mainScope.handledLaunch {
|
connectionJob = mainScope.handledLaunch {
|
||||||
serviceRepository.setMeshService(service)
|
serviceRepository.setMeshService(service)
|
||||||
|
@ -551,11 +358,6 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
model.connectionState.asLiveData().observe(this) { state ->
|
|
||||||
onMeshConnectionChanged(state)
|
|
||||||
updateConnectionStatusImage(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
bluetoothViewModel.enabled.observe(this) { enabled ->
|
bluetoothViewModel.enabled.observe(this) { enabled ->
|
||||||
if (!enabled && !requestedEnable && model.selectedBluetooth) {
|
if (!enabled && !requestedEnable && model.selectedBluetooth) {
|
||||||
requestedEnable = true
|
requestedEnable = true
|
||||||
|
@ -571,17 +373,6 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call showSnackbar() whenever [snackbarText] updates with a non-null value
|
|
||||||
model.snackbarText.observe(this) { text ->
|
|
||||||
if (text is Int) showSnackbar(text)
|
|
||||||
if (text is String) showSnackbar(text)
|
|
||||||
if (text != null) model.clearSnackbarText()
|
|
||||||
}
|
|
||||||
|
|
||||||
model.currentTab.observe(this) {
|
|
||||||
binding.tabLayout.getTabAt(it)?.select()
|
|
||||||
}
|
|
||||||
|
|
||||||
model.tracerouteResponse.observe(this) { response ->
|
model.tracerouteResponse.observe(this) { response ->
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
|
@ -605,118 +396,32 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSettingsPage() {
|
private fun showSettingsPage() {
|
||||||
binding.pager.currentItem = 5
|
// binding.pager.currentItem = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showMessages(contactKey: String?) {
|
private fun onMainMenuAction(action: MainMenuAction) {
|
||||||
model.setCurrentTab(0)
|
when (action) {
|
||||||
if (contactKey != null) {
|
MainMenuAction.ABOUT -> {
|
||||||
supportFragmentManager.navigateToMessages(contactKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shareMessages(message: String?) {
|
|
||||||
model.setCurrentTab(0)
|
|
||||||
if (message != null) {
|
|
||||||
supportFragmentManager.navigateToShareMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
// Inflate the menu; this adds items to the action bar if it is present.
|
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
|
||||||
model.actionBarMenu = menu
|
|
||||||
|
|
||||||
updateConnectionStatusImage(model.connectionState.value)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val handler: Handler by lazy {
|
|
||||||
Handler(Looper.getMainLooper())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menu.findItem(R.id.stress_test).isVisible =
|
|
||||||
BuildConfig.DEBUG // only show stress test for debug builds (for now)
|
|
||||||
menu.findItem(R.id.radio_config).isEnabled = !model.isManaged
|
|
||||||
return super.onPrepareOptionsMenu(menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
// Handle action bar item clicks here. The action bar will
|
|
||||||
// automatically handle clicks on the Home/Up button, so long
|
|
||||||
// as you specify a parent activity in AndroidManifest.xml.
|
|
||||||
return when (item.itemId) {
|
|
||||||
R.id.about -> {
|
|
||||||
getVersionInfo()
|
getVersionInfo()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.connectStatusImage -> {
|
MainMenuAction.EXPORT_MESSAGES -> {
|
||||||
Toast.makeText(applicationContext, item.title, Toast.LENGTH_SHORT).show()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.debug -> {
|
|
||||||
val fragmentManager: FragmentManager = supportFragmentManager
|
|
||||||
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
|
|
||||||
val nameFragment = DebugFragment()
|
|
||||||
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
|
|
||||||
fragmentTransaction.addToBackStack(null)
|
|
||||||
fragmentTransaction.commit()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.stress_test -> {
|
|
||||||
fun postPing() {
|
|
||||||
// Send ping message and arrange delayed recursion.
|
|
||||||
debug("Sending ping")
|
|
||||||
val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
|
||||||
.format(Date(System.currentTimeMillis()))
|
|
||||||
model.sendMessage(str)
|
|
||||||
handler.postDelayed({ postPing() }, 30000)
|
|
||||||
}
|
|
||||||
item.isChecked = !item.isChecked // toggle ping test
|
|
||||||
if (item.isChecked) {
|
|
||||||
postPing()
|
|
||||||
} else {
|
|
||||||
handler.removeCallbacksAndMessages(null)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.radio_config -> {
|
|
||||||
supportFragmentManager.navigateToNavGraph()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.save_messages_csv -> {
|
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "application/csv"
|
type = "application/csv"
|
||||||
putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
|
putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
|
||||||
}
|
}
|
||||||
createDocumentLauncher.launch(intent)
|
createDocumentLauncher.launch(intent)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.theme -> {
|
MainMenuAction.THEME -> {
|
||||||
chooseThemeDialog()
|
chooseThemeDialog()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.preferences_language -> {
|
MainMenuAction.LANGUAGE -> {
|
||||||
chooseLangDialog()
|
chooseLangDialog()
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.show_intro -> {
|
MainMenuAction.SHOW_INTRO -> {
|
||||||
startActivity(Intent(this, AppIntroduction::class.java))
|
startActivity(Intent(this, AppIntroduction::class.java))
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
R.id.preferences_quick_chat -> {
|
else -> {}
|
||||||
val fragmentManager: FragmentManager = supportFragmentManager
|
|
||||||
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
|
|
||||||
val nameFragment = QuickChatSettingsFragment()
|
|
||||||
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
|
|
||||||
fragmentTransaction.addToBackStack(null)
|
|
||||||
fragmentTransaction.commit()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.view.Menu
|
import androidx.compose.material.SnackbarHostState
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -171,8 +171,6 @@ class UIViewModel @Inject constructor(
|
||||||
private val quickChatActionRepository: QuickChatActionRepository,
|
private val quickChatActionRepository: QuickChatActionRepository,
|
||||||
private val preferences: SharedPreferences
|
private val preferences: SharedPreferences
|
||||||
) : ViewModel(), Logging {
|
) : ViewModel(), Logging {
|
||||||
|
|
||||||
var actionBarMenu: Menu? = null
|
|
||||||
val meshService: IMeshService? get() = radioConfigRepository.meshService
|
val meshService: IMeshService? get() = radioConfigRepository.meshService
|
||||||
|
|
||||||
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
|
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
|
||||||
|
@ -255,12 +253,15 @@ class UIViewModel @Inject constructor(
|
||||||
fun getNode(userId: String?) = nodeDB.getNode(userId ?: DataPacket.ID_BROADCAST)
|
fun getNode(userId: String?) = nodeDB.getNode(userId ?: DataPacket.ID_BROADCAST)
|
||||||
fun getUser(userId: String?) = nodeDB.getUser(userId ?: DataPacket.ID_BROADCAST)
|
fun getUser(userId: String?) = nodeDB.getUser(userId ?: DataPacket.ID_BROADCAST)
|
||||||
|
|
||||||
private val _snackbarText = MutableLiveData<Any?>(null)
|
val snackbarState = SnackbarHostState()
|
||||||
val snackbarText: LiveData<Any?> get() = _snackbarText
|
fun showSnackbar(text: Int) = showSnackbar(app.getString(text))
|
||||||
|
fun showSnackbar(text: String) = viewModelScope.launch {
|
||||||
|
snackbarState.showSnackbar(text)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
radioConfigRepository.errorMessage.filterNotNull().onEach {
|
radioConfigRepository.errorMessage.filterNotNull().onEach {
|
||||||
_snackbarText.value = it
|
showSnackbar(it)
|
||||||
radioConfigRepository.clearErrorMessage()
|
radioConfigRepository.clearErrorMessage()
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
@ -460,17 +461,6 @@ class UIViewModel @Inject constructor(
|
||||||
_requestChannelSet.value = null
|
_requestChannelSet.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSnackbar(resString: Any) {
|
|
||||||
_snackbarText.value = resString
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called immediately after activity observes [snackbarText]
|
|
||||||
*/
|
|
||||||
fun clearSnackbarText() {
|
|
||||||
_snackbarText.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
var txEnabled: Boolean
|
var txEnabled: Boolean
|
||||||
get() = config.lora.txEnabled
|
get() = config.lora.txEnabled
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -694,13 +684,6 @@ class UIViewModel @Inject constructor(
|
||||||
radioConfigRepository.clearTracerouteResponse()
|
radioConfigRepository.clearTracerouteResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _currentTab = MutableLiveData(0)
|
|
||||||
val currentTab: LiveData<Int> get() = _currentTab
|
|
||||||
|
|
||||||
fun setCurrentTab(tab: Int) {
|
|
||||||
_currentTab.value = tab
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNodeFilterText(text: String) {
|
fun setNodeFilterText(text: String) {
|
||||||
nodeFilterText.value = text
|
nodeFilterText.value = text
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,7 @@
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
@ -63,12 +59,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
@ -76,7 +70,6 @@ import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||||
|
@ -87,7 +80,6 @@ import com.geeksville.mesh.analytics.DataPair
|
||||||
import com.geeksville.mesh.android.BuildUtils.debug
|
import com.geeksville.mesh.android.BuildUtils.debug
|
||||||
import com.geeksville.mesh.android.BuildUtils.errormsg
|
import com.geeksville.mesh.android.BuildUtils.errormsg
|
||||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||||
import com.geeksville.mesh.android.Logging
|
|
||||||
import com.geeksville.mesh.android.getCameraPermissions
|
import com.geeksville.mesh.android.getCameraPermissions
|
||||||
import com.geeksville.mesh.android.hasCameraPermission
|
import com.geeksville.mesh.android.hasCameraPermission
|
||||||
import com.geeksville.mesh.channelSet
|
import com.geeksville.mesh.channelSet
|
||||||
|
@ -109,32 +101,9 @@ import com.geeksville.mesh.ui.components.config.EditChannelDialog
|
||||||
import com.geeksville.mesh.ui.components.dragContainer
|
import com.geeksville.mesh.ui.components.dragContainer
|
||||||
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
|
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
|
||||||
import com.geeksville.mesh.ui.components.rememberDragDropState
|
import com.geeksville.mesh.ui.components.rememberDragDropState
|
||||||
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class ChannelFragment : ScreenFragment("Channel"), Logging {
|
|
||||||
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setContent {
|
|
||||||
AppCompatTheme {
|
|
||||||
ChannelScreen(model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -291,8 +260,11 @@ fun ChannelScreen(
|
||||||
modemPresetName = modemPresetName,
|
modemPresetName = modemPresetName,
|
||||||
onAddClick = {
|
onAddClick = {
|
||||||
with(channelSet) {
|
with(channelSet) {
|
||||||
if (settingsCount > index) channelSet = copy { settings[index] = it }
|
if (settingsCount > index) {
|
||||||
else channelSet = copy { settings.add(it) }
|
channelSet = copy { settings[index] = it }
|
||||||
|
} else {
|
||||||
|
channelSet = copy { settings.add(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showEditChannelDialog = null
|
showEditChannelDialog = null
|
||||||
},
|
},
|
|
@ -46,7 +46,6 @@ import com.geeksville.mesh.android.Logging
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.model.Contact
|
import com.geeksville.mesh.model.Contact
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.ui.message.navigateToMessages
|
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -71,7 +70,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
|
||||||
onLongClick(contact)
|
onLongClick(contact)
|
||||||
} else {
|
} else {
|
||||||
debug("calling MessagesFragment filter:${contact.contactKey}")
|
debug("calling MessagesFragment filter:${contact.contactKey}")
|
||||||
parentFragmentManager.navigateToMessages(contact.contactKey)
|
// parentFragmentManager.navigateToMessages(contact.contactKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,8 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -36,13 +31,9 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TopAppBar
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.outlined.CloudDownload
|
import androidx.compose.material.icons.outlined.CloudDownload
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
@ -51,8 +42,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
@ -64,63 +53,15 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.database.entity.MeshLog
|
import com.geeksville.mesh.database.entity.MeshLog
|
||||||
import com.geeksville.mesh.model.DebugViewModel
|
import com.geeksville.mesh.model.DebugViewModel
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DebugFragment : Fragment() {
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
|
||||||
setContent {
|
|
||||||
val viewModel: DebugViewModel = hiltViewModel()
|
|
||||||
|
|
||||||
AppTheme {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(id = R.string.debug_panel)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { parentFragmentManager.popBackStack() }) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
stringResource(id = R.string.navigate_back),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
Button(onClick = viewModel::deleteAllLogs) {
|
|
||||||
Text(text = stringResource(R.string.clear))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { innerPadding ->
|
|
||||||
DebugScreen(
|
|
||||||
viewModel = viewModel,
|
|
||||||
contentPadding = innerPadding,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
|
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -185,7 +126,6 @@ private fun Int.asNodeId(): String {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DebugScreen(
|
internal fun DebugScreen(
|
||||||
viewModel: DebugViewModel = hiltViewModel(),
|
viewModel: DebugViewModel = hiltViewModel(),
|
||||||
contentPadding: PaddingValues,
|
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
|
val logs by viewModel.meshLog.collectAsStateWithLifecycle()
|
||||||
|
@ -203,7 +143,6 @@ internal fun DebugScreen(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = contentPadding,
|
|
||||||
) {
|
) {
|
||||||
items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) }
|
items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) }
|
||||||
}
|
}
|
||||||
|
@ -300,3 +239,16 @@ private fun DebugScreenPreview() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DebugMenuActions(
|
||||||
|
viewModel: DebugViewModel = hiltViewModel(),
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = viewModel::deleteAllLogs,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.clear))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,327 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Meshtastic LLC
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.BottomNavigation
|
||||||
|
import androidx.compose.material.BottomNavigationItem
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.SnackbarHost
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.twotone.Chat
|
||||||
|
import androidx.compose.material.icons.twotone.CloudDone
|
||||||
|
import androidx.compose.material.icons.twotone.CloudOff
|
||||||
|
import androidx.compose.material.icons.twotone.CloudUpload
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.twotone.Contactless
|
||||||
|
import androidx.compose.material.icons.twotone.Map
|
||||||
|
import androidx.compose.material.icons.twotone.People
|
||||||
|
import androidx.compose.material.icons.twotone.Settings
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDestination
|
||||||
|
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||||
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.geeksville.mesh.R
|
||||||
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
|
import com.geeksville.mesh.service.MeshService
|
||||||
|
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
|
||||||
|
import com.geeksville.mesh.ui.components.ScannedQrCodeDialog
|
||||||
|
|
||||||
|
enum class TopLevelDestination(val label: String, val icon: ImageVector, val route: Route) {
|
||||||
|
Contacts("Contacts", Icons.AutoMirrored.TwoTone.Chat, Route.Contacts),
|
||||||
|
Nodes("Nodes", Icons.TwoTone.People, Route.Nodes),
|
||||||
|
Map("Map", Icons.TwoTone.Map, Route.Map),
|
||||||
|
Channels("Channels", Icons.TwoTone.Contactless, Route.Channels),
|
||||||
|
Settings("Settings", Icons.TwoTone.Settings, Route.Settings),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun NavDestination.isTopLevel(): Boolean = entries.any { hasRoute(it.route::class) }
|
||||||
|
|
||||||
|
fun fromNavDestination(destination: NavDestination?): TopLevelDestination? = entries
|
||||||
|
.find { dest -> destination?.hierarchy?.any { it.hasRoute(dest.route::class) } == true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
viewModel: UIViewModel = hiltViewModel(),
|
||||||
|
onAction: (MainMenuAction) -> Unit
|
||||||
|
) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
val backStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentDestination = backStackEntry?.destination
|
||||||
|
|
||||||
|
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||||
|
val localConfig by viewModel.localConfig.collectAsStateWithLifecycle()
|
||||||
|
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
if (connectionState.isConnected()) {
|
||||||
|
requestChannelSet?.let { newChannelSet ->
|
||||||
|
ScannedQrCodeDialog(viewModel, newChannelSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
MainAppBar(
|
||||||
|
isManaged = localConfig.security.isManaged,
|
||||||
|
connectionState = connectionState,
|
||||||
|
currentDestination = currentDestination,
|
||||||
|
canNavigateBack = navController.previousBackStackEntry != null,
|
||||||
|
navigateUp = navController::navigateUp,
|
||||||
|
) { action ->
|
||||||
|
when (action) {
|
||||||
|
MainMenuAction.DEBUG -> navController.navigate(Route.DebugPanel)
|
||||||
|
MainMenuAction.RADIO_CONFIG -> navController.navigate(Route.RadioConfig())
|
||||||
|
MainMenuAction.QUICK_CHAT -> navController.navigate(Route.QuickChat)
|
||||||
|
else -> onAction(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
BottomNavigation(
|
||||||
|
navController = navController,
|
||||||
|
currentDestination = currentDestination,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = viewModel.snackbarState) }
|
||||||
|
) { innerPadding ->
|
||||||
|
NavGraph(
|
||||||
|
model = viewModel,
|
||||||
|
navController = navController,
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class MainMenuAction(@StringRes val stringRes: Int) {
|
||||||
|
DEBUG(R.string.debug_panel),
|
||||||
|
RADIO_CONFIG(R.string.device_settings),
|
||||||
|
EXPORT_MESSAGES(R.string.save_messages),
|
||||||
|
THEME(R.string.theme),
|
||||||
|
LANGUAGE(R.string.preferences_language),
|
||||||
|
SHOW_INTRO(R.string.intro_show),
|
||||||
|
QUICK_CHAT(R.string.quick_chat),
|
||||||
|
ABOUT(R.string.about),
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainAppBar(
|
||||||
|
isManaged: Boolean,
|
||||||
|
connectionState: MeshService.ConnectionState,
|
||||||
|
currentDestination: NavDestination?,
|
||||||
|
canNavigateBack: Boolean,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onAction: (MainMenuAction) -> Unit
|
||||||
|
) = AnimatedVisibility(
|
||||||
|
visible = currentDestination?.hasRoute<Route.Messages>() == false,
|
||||||
|
enter = slideInVertically(animationSpec = tween(durationMillis = 200)),
|
||||||
|
exit = slideOutVertically(animationSpec = tween(durationMillis = 200)),
|
||||||
|
) {
|
||||||
|
val isTopLevelRoute = currentDestination?.isTopLevel() == true
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
when {
|
||||||
|
currentDestination == null || isTopLevelRoute -> {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(id = R.drawable.app_icon),
|
||||||
|
contentDescription = stringResource(id = R.string.application_icon),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(text = stringResource(id = R.string.app_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDestination.hasRoute<Route.DebugPanel>() ->
|
||||||
|
Text(stringResource(id = R.string.debug_panel))
|
||||||
|
|
||||||
|
currentDestination.hasRoute<Route.QuickChat>() ->
|
||||||
|
Text(stringResource(id = R.string.quick_chat))
|
||||||
|
|
||||||
|
else -> Text("Node name here") // TODO show destNode longName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
navigationIcon = if (canNavigateBack && !isTopLevelRoute) {
|
||||||
|
{
|
||||||
|
IconButton(onClick = navigateUp) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.navigate_back),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
when {
|
||||||
|
currentDestination == null || isTopLevelRoute ->
|
||||||
|
MainMenuActions(isManaged, connectionState, onAction)
|
||||||
|
|
||||||
|
currentDestination.hasRoute<Route.DebugPanel>() ->
|
||||||
|
DebugMenuActions()
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MainMenuActions(
|
||||||
|
isManaged: Boolean,
|
||||||
|
connectionState: MeshService.ConnectionState,
|
||||||
|
onAction: (MainMenuAction) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val (image, tooltip) = when (connectionState) {
|
||||||
|
MeshService.ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone to R.string.connected
|
||||||
|
MeshService.ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload to R.string.device_sleeping
|
||||||
|
MeshService.ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff to R.string.disconnected
|
||||||
|
}
|
||||||
|
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
Toast.makeText(context, tooltip, Toast.LENGTH_SHORT).show()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = image,
|
||||||
|
contentDescription = stringResource(id = tooltip),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { showMenu = true }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.MoreVert,
|
||||||
|
contentDescription = "Overflow menu",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false },
|
||||||
|
modifier = Modifier.background(MaterialTheme.colors.background.copy(alpha = 1f)),
|
||||||
|
) {
|
||||||
|
MainMenuAction.entries.forEach { action ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
onAction(action)
|
||||||
|
showMenu = false
|
||||||
|
},
|
||||||
|
enabled = when (action) {
|
||||||
|
MainMenuAction.RADIO_CONFIG -> !isManaged
|
||||||
|
else -> true
|
||||||
|
},
|
||||||
|
) { Text(stringResource(id = action.stringRes)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BottomNavigation(
|
||||||
|
navController: NavController,
|
||||||
|
currentDestination: NavDestination?,
|
||||||
|
) {
|
||||||
|
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = topLevelDestination != null,
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { it / 2 },
|
||||||
|
animationSpec = tween(durationMillis = 200),
|
||||||
|
),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { it / 2 },
|
||||||
|
animationSpec = tween(durationMillis = 200),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
BottomNavigation {
|
||||||
|
TopLevelDestination.entries.forEach {
|
||||||
|
val isSelected = it == topLevelDestination
|
||||||
|
BottomNavigationItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = it.icon,
|
||||||
|
contentDescription = it.name,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// label = { Text(it.label) },
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = {
|
||||||
|
if (!isSelected) {
|
||||||
|
navController.navigate(it.route) {
|
||||||
|
// Pop up to the start destination of the graph to
|
||||||
|
// avoid building up a large stack of destinations
|
||||||
|
// on the back stack as users select items
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
// Avoid multiple copies of the same destination when
|
||||||
|
// reselecting the same item
|
||||||
|
launchSingleTop = true
|
||||||
|
// Restore state when reselecting a previously selected item
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,19 +17,9 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.IconButton
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TopAppBar
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Forward
|
import androidx.compose.material.icons.automirrored.filled.Forward
|
||||||
import androidx.compose.material.icons.automirrored.filled.List
|
import androidx.compose.material.icons.automirrored.filled.List
|
||||||
import androidx.compose.material.icons.automirrored.filled.Message
|
import androidx.compose.material.icons.automirrored.filled.Message
|
||||||
|
@ -54,35 +44,27 @@ import androidx.compose.material.icons.filled.Speed
|
||||||
import androidx.compose.material.icons.filled.Usb
|
import androidx.compose.material.icons.filled.Usb
|
||||||
import androidx.compose.material.icons.filled.Wifi
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.fragment.compose.AndroidFragment
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.toRoute
|
||||||
import com.geeksville.mesh.MeshProtos.DeviceMetadata
|
import com.geeksville.mesh.MeshProtos.DeviceMetadata
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.Logging
|
|
||||||
import com.geeksville.mesh.model.MetricsViewModel
|
import com.geeksville.mesh.model.MetricsViewModel
|
||||||
import com.geeksville.mesh.model.RadioConfigViewModel
|
import com.geeksville.mesh.model.RadioConfigViewModel
|
||||||
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
|
import com.geeksville.mesh.ui.components.DeviceMetricsScreen
|
||||||
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
|
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
|
||||||
import com.geeksville.mesh.ui.components.NodeMapScreen
|
import com.geeksville.mesh.ui.components.NodeMapScreen
|
||||||
import com.geeksville.mesh.ui.components.PositionLogScreen
|
import com.geeksville.mesh.ui.components.PositionLogScreen
|
||||||
import com.geeksville.mesh.ui.components.SignalMetricsScreen
|
import com.geeksville.mesh.ui.components.SignalMetricsScreen
|
||||||
import com.geeksville.mesh.ui.components.TracerouteLogScreen
|
import com.geeksville.mesh.ui.components.TracerouteLogScreen
|
||||||
import com.geeksville.mesh.util.UiText
|
|
||||||
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigScreen
|
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigScreen
|
||||||
import com.geeksville.mesh.ui.components.config.AudioConfigScreen
|
import com.geeksville.mesh.ui.components.config.AudioConfigScreen
|
||||||
import com.geeksville.mesh.ui.components.config.BluetoothConfigScreen
|
import com.geeksville.mesh.ui.components.config.BluetoothConfigScreen
|
||||||
|
@ -106,76 +88,11 @@ import com.geeksville.mesh.ui.components.config.SerialConfigScreen
|
||||||
import com.geeksville.mesh.ui.components.config.StoreForwardConfigScreen
|
import com.geeksville.mesh.ui.components.config.StoreForwardConfigScreen
|
||||||
import com.geeksville.mesh.ui.components.config.TelemetryConfigScreen
|
import com.geeksville.mesh.ui.components.config.TelemetryConfigScreen
|
||||||
import com.geeksville.mesh.ui.components.config.UserConfigScreen
|
import com.geeksville.mesh.ui.components.config.UserConfigScreen
|
||||||
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
|
import com.geeksville.mesh.ui.map.MapView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import com.geeksville.mesh.ui.message.MessageScreen
|
||||||
|
import com.geeksville.mesh.util.UiText
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
internal fun FragmentManager.navigateToNavGraph(
|
|
||||||
destNum: Int? = null,
|
|
||||||
startDestination: String = "RadioConfig",
|
|
||||||
) {
|
|
||||||
val radioConfigFragment = NavGraphFragment().apply {
|
|
||||||
arguments = bundleOf("destNum" to destNum, "startDestination" to startDestination)
|
|
||||||
}
|
|
||||||
beginTransaction()
|
|
||||||
.replace(R.id.mainActivityLayout, radioConfigFragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class NavGraphFragment : ScreenFragment("NavGraph"), Logging {
|
|
||||||
|
|
||||||
private val model: RadioConfigViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val destNum = arguments?.getSerializable("destNum") as? Int
|
|
||||||
val startDestination: Any = when (arguments?.getString("startDestination")) {
|
|
||||||
"NodeDetails" -> Route.NodeDetail(destNum!!)
|
|
||||||
else -> Route.RadioConfig(destNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
|
||||||
setContent {
|
|
||||||
val node by model.destNode.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
AppCompatTheme {
|
|
||||||
val navController: NavHostController = rememberNavController()
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
MeshAppBar(
|
|
||||||
currentScreen = node?.user?.longName
|
|
||||||
?: stringResource(R.string.unknown_username),
|
|
||||||
canNavigateBack = true,
|
|
||||||
navigateUp = {
|
|
||||||
if (navController.previousBackStackEntry != null) {
|
|
||||||
navController.navigateUp()
|
|
||||||
} else {
|
|
||||||
parentFragmentManager.popBackStack()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { innerPadding ->
|
|
||||||
NavGraph(
|
|
||||||
navController = navController,
|
|
||||||
startDestination = startDestination,
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class AdminRoute(@StringRes val title: Int) {
|
enum class AdminRoute(@StringRes val title: Int) {
|
||||||
REBOOT(R.string.reboot),
|
REBOOT(R.string.reboot),
|
||||||
SHUTDOWN(R.string.shutdown),
|
SHUTDOWN(R.string.shutdown),
|
||||||
|
@ -184,13 +101,21 @@ enum class AdminRoute(@StringRes val title: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Route {
|
sealed interface Route {
|
||||||
|
@Serializable data object Contacts : Route
|
||||||
|
@Serializable data object Nodes : Route
|
||||||
|
@Serializable data object Map : Route
|
||||||
|
@Serializable data object Channels : Route
|
||||||
|
@Serializable data object Settings : Route
|
||||||
|
|
||||||
|
@Serializable data object DebugPanel : Route
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Messages(val contactKey: String, val message: String = "") : Route
|
data class Messages(val contactKey: String, val message: String = "") : Route
|
||||||
|
@Serializable data object QuickChat : Route
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RadioConfig(val destNum: Int? = null) : Route
|
data class RadioConfig(val destNum: Int? = null) : Route
|
||||||
@Serializable data object User : Route
|
@Serializable data object User : Route
|
||||||
@Serializable data object Channels : Route
|
@Serializable data object ChannelConfig : Route
|
||||||
@Serializable data object Device : Route
|
@Serializable data object Device : Route
|
||||||
@Serializable data object Position : Route
|
@Serializable data object Position : Route
|
||||||
@Serializable data object Power : Route
|
@Serializable data object Power : Route
|
||||||
|
@ -227,7 +152,7 @@ sealed interface Route {
|
||||||
// Config (type = AdminProtos.AdminMessage.ConfigType)
|
// Config (type = AdminProtos.AdminMessage.ConfigType)
|
||||||
enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
|
enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
|
||||||
USER("User", Route.User, Icons.Default.Person, 0),
|
USER("User", Route.User, Icons.Default.Person, 0),
|
||||||
CHANNELS("Channels", Route.Channels, Icons.AutoMirrored.Default.List, 0),
|
CHANNELS("Channels", Route.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
|
||||||
DEVICE("Device", Route.Device, Icons.Default.Router, 0),
|
DEVICE("Device", Route.Device, Icons.Default.Router, 0),
|
||||||
POSITION("Position", Route.Position, Icons.Default.LocationOn, 1),
|
POSITION("Position", Route.Position, Icons.Default.LocationOn, 1),
|
||||||
POWER("Power", Route.Power, Icons.Default.Power, 2),
|
POWER("Power", Route.Power, Icons.Default.Power, 2),
|
||||||
|
@ -291,41 +216,54 @@ sealed class ResponseState<out T> {
|
||||||
fun isWaiting() = this !is Empty
|
fun isWaiting() = this !is Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MeshAppBar(
|
|
||||||
currentScreen: String,
|
|
||||||
canNavigateBack: Boolean,
|
|
||||||
navigateUp: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(currentScreen) },
|
|
||||||
modifier = modifier,
|
|
||||||
navigationIcon = {
|
|
||||||
if (canNavigateBack) {
|
|
||||||
IconButton(onClick = navigateUp) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(id = R.string.navigate_back),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun NavGraph(
|
fun NavGraph(
|
||||||
|
model: UIViewModel = hiltViewModel(),
|
||||||
navController: NavHostController = rememberNavController(),
|
navController: NavHostController = rememberNavController(),
|
||||||
startDestination: Any,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = startDestination,
|
startDestination = Route.Contacts,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
composable<Route.Contacts> {
|
||||||
|
AndroidFragment<ContactsFragment>()
|
||||||
|
}
|
||||||
|
composable<Route.Nodes> {
|
||||||
|
NodeScreen(
|
||||||
|
model = model,
|
||||||
|
navigateToMessages = { navController.navigate(Route.Messages(it)) },
|
||||||
|
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable<Route.Map> {
|
||||||
|
MapView(model)
|
||||||
|
}
|
||||||
|
composable<Route.Channels> {
|
||||||
|
ChannelScreen(model)
|
||||||
|
}
|
||||||
|
composable<Route.Settings> {
|
||||||
|
AndroidFragment<SettingsFragment>(Modifier.fillMaxSize())
|
||||||
|
}
|
||||||
|
composable<Route.DebugPanel> {
|
||||||
|
DebugScreen()
|
||||||
|
}
|
||||||
|
composable<Route.Messages> { backStackEntry ->
|
||||||
|
val args = backStackEntry.toRoute<Route.Messages>()
|
||||||
|
MessageScreen(
|
||||||
|
contactKey = args.contactKey,
|
||||||
|
message = args.message,
|
||||||
|
viewModel = model,
|
||||||
|
navigateToMessages = { navController.navigate(Route.Messages(it)) },
|
||||||
|
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
|
||||||
|
onNavigateBack = navController::navigateUp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable<Route.QuickChat> {
|
||||||
|
QuickChatScreen()
|
||||||
|
}
|
||||||
composable<Route.NodeDetail> {
|
composable<Route.NodeDetail> {
|
||||||
NodeDetailScreen { navController.navigate(route = it) }
|
NodeDetailScreen { navController.navigate(route = it) }
|
||||||
}
|
}
|
||||||
|
@ -360,7 +298,7 @@ fun NavGraph(
|
||||||
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
|
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
|
||||||
UserConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
|
UserConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
|
||||||
}
|
}
|
||||||
composable<Route.Channels> {
|
composable<Route.ChannelConfig> {
|
||||||
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
|
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
|
||||||
ChannelConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
|
ChannelConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
@ -31,67 +27,21 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.android.Logging
|
|
||||||
import com.geeksville.mesh.model.Node
|
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.ui.components.NodeMenuAction
|
|
||||||
import com.geeksville.mesh.ui.components.NodeFilterTextField
|
import com.geeksville.mesh.ui.components.NodeFilterTextField
|
||||||
|
import com.geeksville.mesh.ui.components.NodeMenuAction
|
||||||
import com.geeksville.mesh.ui.components.rememberTimeTickWithLifecycle
|
import com.geeksville.mesh.ui.components.rememberTimeTickWithLifecycle
|
||||||
import com.geeksville.mesh.ui.message.navigateToMessages
|
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class UsersFragment : ScreenFragment("Users"), Logging {
|
|
||||||
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private fun navigateToMessages(node: Node) = node.user.let { user ->
|
|
||||||
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
|
|
||||||
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
|
||||||
val contactKey = "$channel${user.id}"
|
|
||||||
info("calling MessagesFragment filter: $contactKey")
|
|
||||||
parentFragmentManager.navigateToMessages(contactKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToNodeDetails(nodeNum: Int) {
|
|
||||||
info("calling NodeDetails --> destNum: $nodeNum")
|
|
||||||
parentFragmentManager.navigateToNavGraph(nodeNum, "NodeDetails")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setContent {
|
|
||||||
AppTheme {
|
|
||||||
NodesScreen(
|
|
||||||
model = model,
|
|
||||||
navigateToMessages = ::navigateToMessages,
|
|
||||||
navigateToNodeDetails = ::navigateToNodeDetails,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
fun NodesScreen(
|
@Composable
|
||||||
|
fun NodeScreen(
|
||||||
model: UIViewModel = hiltViewModel(),
|
model: UIViewModel = hiltViewModel(),
|
||||||
navigateToMessages: (Node) -> Unit,
|
navigateToMessages: (String) -> Unit,
|
||||||
navigateToNodeDetails: (Int) -> Unit,
|
navigateToNodeDetails: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by model.nodesUiState.collectAsStateWithLifecycle()
|
val state by model.nodesUiState.collectAsStateWithLifecycle()
|
||||||
|
@ -135,7 +85,12 @@ fun NodesScreen(
|
||||||
when (menuItem) {
|
when (menuItem) {
|
||||||
is NodeMenuAction.Remove -> model.removeNode(node.num)
|
is NodeMenuAction.Remove -> model.removeNode(node.num)
|
||||||
is NodeMenuAction.Ignore -> model.ignoreNode(node)
|
is NodeMenuAction.Ignore -> model.ignoreNode(node)
|
||||||
is NodeMenuAction.DirectMessage -> navigateToMessages(node)
|
is NodeMenuAction.DirectMessage -> {
|
||||||
|
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC
|
||||||
|
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
||||||
|
navigateToMessages("$channel${node.user.id}")
|
||||||
|
}
|
||||||
|
|
||||||
is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
|
is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
|
||||||
is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
|
is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
|
||||||
is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)
|
is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)
|
|
@ -17,10 +17,6 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui
|
package com.geeksville.mesh.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
@ -49,13 +45,10 @@ import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.ListItem
|
import androidx.compose.material.ListItem
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Switch
|
import androidx.compose.material.Switch
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TextButton
|
import androidx.compose.material.TextButton
|
||||||
import androidx.compose.material.TopAppBar
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.FastForward
|
import androidx.compose.material.icons.filled.FastForward
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -66,9 +59,7 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.onFocusEvent
|
import androidx.compose.ui.focus.onFocusEvent
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
@ -76,55 +67,15 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.Logging
|
|
||||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.ui.components.dragContainer
|
import com.geeksville.mesh.ui.components.dragContainer
|
||||||
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
|
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
|
||||||
import com.geeksville.mesh.ui.components.rememberDragDropState
|
import com.geeksville.mesh.ui.components.rememberDragDropState
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging {
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
|
||||||
setContent {
|
|
||||||
AppTheme {
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(id = R.string.quick_chat)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { parentFragmentManager.popBackStack() }) {
|
|
||||||
Icon(
|
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
stringResource(id = R.string.navigate_back),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) { innerPadding ->
|
|
||||||
QuickChatScreen(
|
|
||||||
modifier = Modifier.padding(innerPadding)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun QuickChatScreen(
|
internal fun QuickChatScreen(
|
|
@ -23,36 +23,22 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.android.Logging
|
import com.geeksville.mesh.android.Logging
|
||||||
import com.geeksville.mesh.R
|
|
||||||
import com.geeksville.mesh.databinding.ShareFragmentBinding
|
import com.geeksville.mesh.databinding.ShareFragmentBinding
|
||||||
import com.geeksville.mesh.model.Contact
|
import com.geeksville.mesh.model.Contact
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.ui.message.navigateToMessages
|
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
internal fun FragmentManager.navigateToShareMessage(message: String) {
|
|
||||||
val shareFragment = ShareFragment().apply {
|
|
||||||
arguments = bundleOf("message" to message)
|
|
||||||
}
|
|
||||||
beginTransaction()
|
|
||||||
.add(R.id.mainActivityLayout, shareFragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShareFragment : ScreenFragment("Messages"), Logging {
|
class ShareFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
|
@ -67,10 +53,10 @@ class ShareFragment : ScreenFragment("Messages"), Logging {
|
||||||
|
|
||||||
private fun shareMessage(contact: Contact) {
|
private fun shareMessage(contact: Contact) {
|
||||||
debug("calling MessagesFragment filter:${contact.contactKey}")
|
debug("calling MessagesFragment filter:${contact.contactKey}")
|
||||||
parentFragmentManager.navigateToMessages(
|
// parentFragmentManager.navigateToMessages(
|
||||||
contact.contactKey,
|
// contact.contactKey,
|
||||||
arguments?.getString("message").toString()
|
// arguments?.getString("message").toString()
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onClick(contact: Contact) {
|
private fun onClick(contact: Contact) {
|
||||||
|
|
|
@ -49,13 +49,30 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.channelSet
|
import com.geeksville.mesh.channelSet
|
||||||
import com.geeksville.mesh.copy
|
import com.geeksville.mesh.copy
|
||||||
import com.geeksville.mesh.model.Channel
|
import com.geeksville.mesh.model.Channel
|
||||||
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.ui.components.config.ChannelSelection
|
import com.geeksville.mesh.ui.components.config.ChannelSelection
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScannedQrCodeDialog(
|
||||||
|
viewModel: UIViewModel,
|
||||||
|
incoming: ChannelSet,
|
||||||
|
) {
|
||||||
|
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
ScannedQrCodeDialog(
|
||||||
|
channels = channels,
|
||||||
|
incoming = incoming,
|
||||||
|
onDismiss = viewModel::clearRequestChannelUrl,
|
||||||
|
onConfirm = viewModel::setChannels,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables the user to select which channels to accept after scanning a QR code.
|
* Enables the user to select which channels to accept after scanning a QR code.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,10 +18,6 @@
|
||||||
package com.geeksville.mesh.ui.map
|
package com.geeksville.mesh.ui.map
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
@ -45,21 +41,17 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.MeshProtos.Waypoint
|
import com.geeksville.mesh.MeshProtos.Waypoint
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.BuildUtils.debug
|
import com.geeksville.mesh.android.BuildUtils.debug
|
||||||
import com.geeksville.mesh.android.Logging
|
|
||||||
import com.geeksville.mesh.android.getLocationPermissions
|
import com.geeksville.mesh.android.getLocationPermissions
|
||||||
import com.geeksville.mesh.android.gpsDisabled
|
import com.geeksville.mesh.android.gpsDisabled
|
||||||
import com.geeksville.mesh.android.hasGps
|
import com.geeksville.mesh.android.hasGps
|
||||||
|
@ -71,8 +63,6 @@ import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.model.map.CustomTileSource
|
import com.geeksville.mesh.model.map.CustomTileSource
|
||||||
import com.geeksville.mesh.model.map.MarkerWithLabel
|
import com.geeksville.mesh.model.map.MarkerWithLabel
|
||||||
import com.geeksville.mesh.model.map.clustering.RadiusMarkerClusterer
|
import com.geeksville.mesh.model.map.clustering.RadiusMarkerClusterer
|
||||||
import com.geeksville.mesh.ui.ScreenFragment
|
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
|
||||||
import com.geeksville.mesh.util.SqlTileWriterExt
|
import com.geeksville.mesh.util.SqlTileWriterExt
|
||||||
import com.geeksville.mesh.util.addCopyright
|
import com.geeksville.mesh.util.addCopyright
|
||||||
import com.geeksville.mesh.util.addScaleBarOverlay
|
import com.geeksville.mesh.util.addScaleBarOverlay
|
||||||
|
@ -81,7 +71,6 @@ import com.geeksville.mesh.util.formatAgo
|
||||||
import com.geeksville.mesh.util.zoomIn
|
import com.geeksville.mesh.util.zoomIn
|
||||||
import com.geeksville.mesh.waypoint
|
import com.geeksville.mesh.waypoint
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
|
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
|
||||||
import org.osmdroid.config.Configuration
|
import org.osmdroid.config.Configuration
|
||||||
import org.osmdroid.events.MapEventsReceiver
|
import org.osmdroid.events.MapEventsReceiver
|
||||||
|
@ -104,27 +93,6 @@ import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MapFragment : ScreenFragment("Map Fragment"), Logging {
|
|
||||||
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setContent {
|
|
||||||
AppTheme {
|
|
||||||
MapView(model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MapView.UpdateMarkers(
|
private fun MapView.UpdateMarkers(
|
||||||
nodeMarkers: List<MarkerWithLabel>,
|
nodeMarkers: List<MarkerWithLabel>,
|
||||||
|
|
|
@ -17,10 +17,6 @@
|
||||||
|
|
||||||
package com.geeksville.mesh.ui.message
|
package com.geeksville.mesh.ui.message
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -66,10 +62,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.focus.onFocusEvent
|
import androidx.compose.ui.focus.onFocusEvent
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
@ -80,95 +74,26 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.R
|
import com.geeksville.mesh.R
|
||||||
import com.geeksville.mesh.android.Logging
|
|
||||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||||
import com.geeksville.mesh.model.Node
|
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.model.getChannel
|
import com.geeksville.mesh.model.getChannel
|
||||||
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
|
||||||
import com.geeksville.mesh.ui.components.NodeMenuAction
|
import com.geeksville.mesh.ui.components.NodeMenuAction
|
||||||
import com.geeksville.mesh.ui.message.components.MessageList
|
import com.geeksville.mesh.ui.message.components.MessageList
|
||||||
import com.geeksville.mesh.ui.navigateToNavGraph
|
|
||||||
import com.geeksville.mesh.ui.theme.AppTheme
|
import com.geeksville.mesh.ui.theme.AppTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
internal fun FragmentManager.navigateToMessages(contactKey: String, message: String = "") {
|
|
||||||
val messagesFragment = MessagesFragment().apply {
|
|
||||||
arguments = bundleOf("contactKey" to contactKey, "message" to message)
|
|
||||||
}
|
|
||||||
beginTransaction()
|
|
||||||
.add(R.id.mainActivityLayout, messagesFragment)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MessagesFragment : Fragment(), Logging {
|
|
||||||
private val model: UIViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private fun navigateToMessages(node: Node) = node.user.let { user ->
|
|
||||||
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
|
|
||||||
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
|
|
||||||
val contactKey = "$channel${user.id}"
|
|
||||||
info("calling MessagesFragment filter: $contactKey")
|
|
||||||
parentFragmentManager.navigateToMessages(contactKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToNodeDetails(nodeNum: Int) {
|
|
||||||
info("calling NodeDetails --> destNum: $nodeNum")
|
|
||||||
parentFragmentManager.navigateToNavGraph(nodeNum, "NodeDetails")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val contactKey = arguments?.getString("contactKey").toString()
|
|
||||||
val message = arguments?.getString("message").toString()
|
|
||||||
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
||||||
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
|
|
||||||
setContent {
|
|
||||||
AppTheme {
|
|
||||||
MessageScreen(
|
|
||||||
contactKey = contactKey,
|
|
||||||
message = message,
|
|
||||||
viewModel = model,
|
|
||||||
navigateToMessages = ::navigateToMessages,
|
|
||||||
navigateToNodeDetails = ::navigateToNodeDetails,
|
|
||||||
) { parentFragmentManager.popBackStack() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class MessageMenuAction {
|
|
||||||
data object ClipboardCopy : MessageMenuAction()
|
|
||||||
data object Delete : MessageMenuAction()
|
|
||||||
data object Dismiss : MessageMenuAction()
|
|
||||||
data object SelectAll : MessageMenuAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessageScreen(
|
internal fun MessageScreen(
|
||||||
contactKey: String,
|
contactKey: String,
|
||||||
message: String,
|
message: String,
|
||||||
viewModel: UIViewModel = hiltViewModel(),
|
viewModel: UIViewModel = hiltViewModel(),
|
||||||
navigateToMessages: (Node) -> Unit,
|
navigateToMessages: (String) -> Unit,
|
||||||
navigateToNodeDetails: (Int) -> Unit,
|
navigateToNodeDetails: (Int) -> Unit,
|
||||||
onNavigateBack: () -> Unit
|
onNavigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
@ -281,7 +206,11 @@ internal fun MessageScreen(
|
||||||
when (action) {
|
when (action) {
|
||||||
is NodeMenuAction.Remove -> viewModel.removeNode(action.node.num)
|
is NodeMenuAction.Remove -> viewModel.removeNode(action.node.num)
|
||||||
is NodeMenuAction.Ignore -> viewModel.ignoreNode(action.node)
|
is NodeMenuAction.Ignore -> viewModel.ignoreNode(action.node)
|
||||||
is NodeMenuAction.DirectMessage -> navigateToMessages(action.node)
|
is NodeMenuAction.DirectMessage -> {
|
||||||
|
val hasPKC = viewModel.ourNodeInfo.value?.hasPKC == true && action.node.hasPKC
|
||||||
|
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else action.node.channel
|
||||||
|
navigateToMessages("$channel${action.node.user.id}")
|
||||||
|
}
|
||||||
is NodeMenuAction.RequestUserInfo -> viewModel.requestUserInfo(action.node.num)
|
is NodeMenuAction.RequestUserInfo -> viewModel.requestUserInfo(action.node.num)
|
||||||
is NodeMenuAction.RequestPosition -> viewModel.requestPosition(action.node.num)
|
is NodeMenuAction.RequestPosition -> viewModel.requestPosition(action.node.num)
|
||||||
is NodeMenuAction.TraceRoute -> viewModel.requestTraceroute(action.node.num)
|
is NodeMenuAction.TraceRoute -> viewModel.requestTraceroute(action.node.num)
|
||||||
|
@ -324,6 +253,13 @@ private fun DeleteMessageDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class MessageMenuAction {
|
||||||
|
data object ClipboardCopy : MessageMenuAction()
|
||||||
|
data object Delete : MessageMenuAction()
|
||||||
|
data object Dismiss : MessageMenuAction()
|
||||||
|
data object SelectAll : MessageMenuAction()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ActionModeTopBar(
|
private fun ActionModeTopBar(
|
||||||
selectedList: Set<Long>,
|
selectedList: Set<Long>,
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Meshtastic LLC
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.geeksville.mesh.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.Shapes
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
val Shapes = Shapes(
|
|
||||||
small = RoundedCornerShape(4.dp),
|
|
||||||
medium = RoundedCornerShape(4.dp),
|
|
||||||
large = RoundedCornerShape(0.dp)
|
|
||||||
)
|
|
|
@ -61,8 +61,6 @@ fun AppTheme(
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colors = colors,
|
colors = colors,
|
||||||
typography = Typography,
|
|
||||||
shapes = Shapes,
|
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2025 Meshtastic LLC
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.geeksville.mesh.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.material.Typography
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
|
||||||
val Typography = Typography(
|
|
||||||
h3 = TextStyle(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 24.sp
|
|
||||||
),
|
|
||||||
h4 = TextStyle(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp
|
|
||||||
),
|
|
||||||
body1 = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 16.sp
|
|
||||||
),
|
|
||||||
body2 = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp
|
|
||||||
),
|
|
||||||
/* Other default text styles to override
|
|
||||||
button = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.W500,
|
|
||||||
fontSize = 14.sp
|
|
||||||
),
|
|
||||||
caption = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
|
@ -1,97 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Copyright (c) 2025 Meshtastic LLC
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity"
|
|
||||||
android:id="@+id/mainActivityLayout">
|
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:id="@+id/appBarLayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
style="@style/MyToolbar"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/appIconImageVIew"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/application_icon"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:scaleX="1.5"
|
|
||||||
android:scaleY="1.5"
|
|
||||||
app:srcCompat="@drawable/app_icon"
|
|
||||||
tools:layout_editor_absoluteX="16dp"
|
|
||||||
tools:layout_editor_absoluteY="18dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:minHeight="?actionBarSize"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:text="@string/app_name"
|
|
||||||
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
|
||||||
android:id="@+id/tab_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:tabGravity="fill"
|
|
||||||
app:tabIconTint="@color/tab_color_selector"
|
|
||||||
app:tabIndicatorColor="@color/selectedColor" />
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
|
||||||
android:id="@+id/pager"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
|
||||||
android:id="@+id/composeView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
</FrameLayout>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Copyright (c) 2025 Meshtastic LLC
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<menu
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/connectStatusImage"
|
|
||||||
android:contentDescription="@string/connection_status"
|
|
||||||
android:icon="@drawable/cloud_off"
|
|
||||||
app:iconTint="@color/toolbarText"
|
|
||||||
android:title="@string/disconnected"
|
|
||||||
app:showAsAction="ifRoom"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/debug"
|
|
||||||
android:title="@string/debug_panel"
|
|
||||||
app:showAsAction="withText" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/stress_test"
|
|
||||||
android:checkable="true"
|
|
||||||
android:checked="false"
|
|
||||||
android:title="@string/protocol_stress_test" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/radio_config"
|
|
||||||
app:showAsAction="withText"
|
|
||||||
android:title="@string/device_settings" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/save_messages_csv"
|
|
||||||
app:showAsAction="withText"
|
|
||||||
android:title="@string/save_messages" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/theme"
|
|
||||||
android:title="@string/theme"
|
|
||||||
app:showAsAction="withText" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/preferences_language"
|
|
||||||
android:title="@string/preferences_language"
|
|
||||||
app:showAsAction="withText" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/show_intro"
|
|
||||||
android:title="@string/intro_show"
|
|
||||||
app:showAsAction="withText" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/preferences_quick_chat"
|
|
||||||
android:title="@string/quick_chat"
|
|
||||||
app:showAsAction="withText" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/about"
|
|
||||||
android:title="@string/about"
|
|
||||||
app:showAsAction="withText" />
|
|
||||||
</menu>
|
|
|
@ -72,7 +72,6 @@
|
||||||
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String</ID>
|
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String</ID>
|
||||||
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int</ID>
|
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int</ID>
|
||||||
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long</ID>
|
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long</ID>
|
||||||
<ID>CyclomaticComplexMethod:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
|
||||||
<ID>CyclomaticComplexMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID>
|
<ID>CyclomaticComplexMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID>
|
||||||
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
|
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
|
||||||
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||||
|
@ -81,7 +80,6 @@
|
||||||
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
|
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
|
||||||
<ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt$()</ID>
|
<ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt$()</ID>
|
||||||
<ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt.SourceCount$()</ID>
|
<ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt.SourceCount$()</ID>
|
||||||
<ID>EmptyFunctionBlock:MainActivity.kt$MainActivity.<no name provided>${ }</ID>
|
|
||||||
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
|
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
|
||||||
<ID>EmptyFunctionBlock:NsdManager.kt$<no name provided>${ }</ID>
|
<ID>EmptyFunctionBlock:NsdManager.kt$<no name provided>${ }</ID>
|
||||||
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
|
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
|
||||||
|
@ -129,7 +127,6 @@
|
||||||
<ID>FinalNewline:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID>
|
<ID>FinalNewline:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID>
|
||||||
<ID>FinalNewline:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID>
|
<ID>FinalNewline:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID>
|
||||||
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
|
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
|
||||||
<ID>FinalNewline:Theme.kt$com.geeksville.mesh.ui.theme.Theme.kt</ID>
|
|
||||||
<ID>FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
|
<ID>FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
|
||||||
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
||||||
<ID>ForbiddenComment:MapFragment.kt$// TODO: Accept filename input param from user</ID>
|
<ID>ForbiddenComment:MapFragment.kt$// TODO: Accept filename input param from user</ID>
|
||||||
|
@ -158,10 +155,8 @@
|
||||||
<ID>LongMethod:EditListPreference.kt$@Composable inline fun <reified T> EditListPreference( title: String, list: List<T>, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List<T>) -> Unit, modifier: Modifier = Modifier, )</ID>
|
<ID>LongMethod:EditListPreference.kt$@Composable inline fun <reified T> EditListPreference( title: String, list: List<T>, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List<T>) -> Unit, modifier: Modifier = Modifier, )</ID>
|
||||||
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit, )</ID>
|
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -> Unit, )</ID>
|
||||||
<ID>LongMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigItemList( mqttConfig: MQTTConfig, enabled: Boolean, onSaveClicked: (MQTTConfig) -> Unit, )</ID>
|
<ID>LongMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigItemList( mqttConfig: MQTTConfig, enabled: Boolean, onSaveClicked: (MQTTConfig) -> Unit, )</ID>
|
||||||
<ID>LongMethod:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
|
||||||
<ID>LongMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID>
|
<ID>LongMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID>
|
||||||
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
|
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
|
||||||
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigItemList( networkConfig: NetworkConfig, enabled: Boolean, onSaveClicked: (NetworkConfig) -> Unit, )</ID>
|
|
||||||
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigItemList( powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -> Unit, )</ID>
|
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigItemList( powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -> Unit, )</ID>
|
||||||
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||||
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigItemList( serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -> Unit, )</ID>
|
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigItemList( serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -> Unit, )</ID>
|
||||||
|
@ -219,7 +214,7 @@
|
||||||
<ID>MagicNumber:ContactsFragment.kt$ContactsFragment.ActionModeCallback$8</ID>
|
<ID>MagicNumber:ContactsFragment.kt$ContactsFragment.ActionModeCallback$8</ID>
|
||||||
<ID>MagicNumber:ContextServices.kt$33</ID>
|
<ID>MagicNumber:ContextServices.kt$33</ID>
|
||||||
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
|
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
|
||||||
<ID>MagicNumber:DebugFragment.kt$3</ID>
|
<ID>MagicNumber:Debug.kt$3</ID>
|
||||||
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$100</ID>
|
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$100</ID>
|
||||||
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$10000</ID>
|
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$10000</ID>
|
||||||
<ID>MagicNumber:DownloadButton.kt$1.25f</ID>
|
<ID>MagicNumber:DownloadButton.kt$1.25f</ID>
|
||||||
|
@ -257,8 +252,6 @@
|
||||||
<ID>MagicNumber:LocationUtils.kt$6366000</ID>
|
<ID>MagicNumber:LocationUtils.kt$6366000</ID>
|
||||||
<ID>MagicNumber:LocationUtils.kt$GPSFormat$3</ID>
|
<ID>MagicNumber:LocationUtils.kt$GPSFormat$3</ID>
|
||||||
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
|
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
|
||||||
<ID>MagicNumber:MainActivity.kt$MainActivity$30000</ID>
|
|
||||||
<ID>MagicNumber:MainActivity.kt$MainActivity$5</ID>
|
|
||||||
<ID>MagicNumber:MapFragment.kt$0.5f</ID>
|
<ID>MagicNumber:MapFragment.kt$0.5f</ID>
|
||||||
<ID>MagicNumber:MapFragment.kt$1.3</ID>
|
<ID>MagicNumber:MapFragment.kt$1.3</ID>
|
||||||
<ID>MagicNumber:MapFragment.kt$1000</ID>
|
<ID>MagicNumber:MapFragment.kt$1000</ID>
|
||||||
|
@ -396,12 +389,9 @@
|
||||||
<ID>MaxLineLength:LoRaConfigItemList.kt$value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq</ID>
|
<ID>MaxLineLength:LoRaConfigItemList.kt$value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq</ID>
|
||||||
<ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID>
|
<ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID>
|
||||||
<ID>MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*</ID>
|
<ID>MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*</ID>
|
||||||
<ID>MaxLineLength:MainActivity.kt$/* UI design material setup instructions: https://material.io/develop/android/docs/getting-started/ dark theme (or use system eventually) https://material.io/develop/android/theming/dark/ NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app title. Fragments: SettingsFragment shows "Settings" username shortname bluetooth pairing list (eventually misc device settings that are not channel related) Channel fragment "Channel" qr code, copy link button ch number misc other settings (eventually a way of choosing between past channels) ChatFragment "Messages" a text box to enter new texts a scrolling list of rows. each row is a text and a sender info layout NodeListFragment "Users" a node info row for every node ViewModels: BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle) MeshModel contains: (manages entire service relationship) current received texts current radio macaddr current node infos (updated dynamically) eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/ use numbers of # chat messages and # of members in the badges. (per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs ) eventually: make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder */</ID>
|
|
||||||
<ID>MaxLineLength:MainActivity.kt$MainActivity$/* This problem can occur if we unbind, but there is already an onConnected job waiting to run. That job runs and then makes meshService != null again I think I've fixed this by cancelling connectionJob. We'll see! */</ID>
|
<ID>MaxLineLength:MainActivity.kt$MainActivity$/* This problem can occur if we unbind, but there is already an onConnected job waiting to run. That job runs and then makes meshService != null again I think I've fixed this by cancelling connectionJob. We'll see! */</ID>
|
||||||
<ID>MaxLineLength:MainActivity.kt$MainActivity$// Old samsung phones have a race condition andthis might rarely fail. Which is probably find because the bind will be sufficient most of the time</ID>
|
<ID>MaxLineLength:MainActivity.kt$MainActivity$// Old samsung phones have a race condition andthis might rarely fail. Which is probably find because the bind will be sufficient most of the time</ID>
|
||||||
<ID>MaxLineLength:MainActivity.kt$MainActivity$// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel</ID>
|
<ID>MaxLineLength:MainActivity.kt$MainActivity$// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel</ID>
|
||||||
<ID>MaxLineLength:MainActivity.kt$MainActivity$// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops</ID>
|
|
||||||
<ID>MaxLineLength:MainActivity.kt$MainActivity$MeshService.ConnectionState.DEVICE_SLEEP -> R.drawable.ic_twotone_cloud_upload_24 to R.string.device_sleeping</ID>
|
|
||||||
<ID>MaxLineLength:MainActivity.kt$MainActivity$debug("Asked to open a channel URL - ask user if they want to switch to that channel. If so send the config to the radio")</ID>
|
<ID>MaxLineLength:MainActivity.kt$MainActivity$debug("Asked to open a channel URL - ask user if they want to switch to that channel. If so send the config to the radio")</ID>
|
||||||
<ID>MaxLineLength:MeshService.kt$MeshService$*</ID>
|
<ID>MaxLineLength:MeshService.kt$MeshService$*</ID>
|
||||||
<ID>MaxLineLength:MeshService.kt$MeshService$* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException</ID>
|
<ID>MaxLineLength:MeshService.kt$MeshService$* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException</ID>
|
||||||
|
@ -480,8 +470,6 @@
|
||||||
<ID>MultiLineIfElse:BuildUtils.kt$BuildUtils$return false</ID>
|
<ID>MultiLineIfElse:BuildUtils.kt$BuildUtils$return false</ID>
|
||||||
<ID>MultiLineIfElse:Channel.kt$Channel$"Custom"</ID>
|
<ID>MultiLineIfElse:Channel.kt$Channel$"Custom"</ID>
|
||||||
<ID>MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" ModemPreset.MEDIUM_SLOW -> "MediumSlow" ModemPreset.LONG_FAST -> "LongFast" ModemPreset.LONG_SLOW -> "LongSlow" ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" }</ID>
|
<ID>MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" ModemPreset.MEDIUM_SLOW -> "MediumSlow" ModemPreset.LONG_FAST -> "LongFast" ModemPreset.LONG_SLOW -> "LongSlow" ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" }</ID>
|
||||||
<ID>MultiLineIfElse:ChannelFragment.kt$channelSet = copy { settings.add(it) }</ID>
|
|
||||||
<ID>MultiLineIfElse:ChannelFragment.kt$channelSet = copy { settings[index] = it }</ID>
|
|
||||||
<ID>MultiLineIfElse:ChannelOption.kt$when (bandwidth) { 31 -> .03125f 62 -> .0625f 200 -> .203125f 400 -> .40625f 800 -> .8125f 1600 -> 1.6250f else -> bandwidth / 1000f }</ID>
|
<ID>MultiLineIfElse:ChannelOption.kt$when (bandwidth) { 31 -> .03125f 62 -> .0625f 200 -> .203125f 400 -> .40625f 800 -> .8125f 1600 -> 1.6250f else -> bandwidth / 1000f }</ID>
|
||||||
<ID>MultiLineIfElse:ContextServices.kt$MaterialAlertDialogBuilder(this) .setTitle(title) .setMessage(rationale) .setNeutralButton(R.string.cancel) { _, _ -> } .setPositiveButton(R.string.accept) { _, _ -> invokeFun() } .show()</ID>
|
<ID>MultiLineIfElse:ContextServices.kt$MaterialAlertDialogBuilder(this) .setTitle(title) .setMessage(rationale) .setNeutralButton(R.string.cancel) { _, _ -> } .setPositiveButton(R.string.accept) { _, _ -> invokeFun() } .show()</ID>
|
||||||
<ID>MultiLineIfElse:ContextServices.kt$invokeFun()</ID>
|
<ID>MultiLineIfElse:ContextServices.kt$invokeFun()</ID>
|
||||||
|
@ -514,7 +502,6 @@
|
||||||
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$hwModel.name.replace('_', '-').replace('p', '.').lowercase()</ID>
|
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$hwModel.name.replace('_', '-').replace('p', '.').lowercase()</ID>
|
||||||
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$null</ID>
|
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$null</ID>
|
||||||
<ID>MultiLineIfElse:RadioConfigScreen.kt$AlertDialog( onDismissRequest = {}, shape = RoundedCornerShape(16.dp), backgroundColor = MaterialTheme.colors.background, title = { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, ) { Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(end = 8.dp) ) Text( text = "${stringResource(title)}?\n") Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(start = 8.dp) ) } }, buttons = { Row( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { TextButton( modifier = Modifier.weight(1f), onClick = { showDialog = false }, ) { Text(stringResource(R.string.cancel)) } Button( modifier = Modifier.weight(1f), onClick = { showDialog = false onClick() }, ) { Text(stringResource(R.string.send)) } } } )</ID>
|
<ID>MultiLineIfElse:RadioConfigScreen.kt$AlertDialog( onDismissRequest = {}, shape = RoundedCornerShape(16.dp), backgroundColor = MaterialTheme.colors.background, title = { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, ) { Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(end = 8.dp) ) Text( text = "${stringResource(title)}?\n") Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(start = 8.dp) ) } }, buttons = { Row( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { TextButton( modifier = Modifier.weight(1f), onClick = { showDialog = false }, ) { Text(stringResource(R.string.cancel)) } Button( modifier = Modifier.weight(1f), onClick = { showDialog = false onClick() }, ) { Text(stringResource(R.string.send)) } } } )</ID>
|
||||||
<ID>MultiLineIfElse:RadioConfigViewModel.kt$RadioConfigViewModel$try { setChannels(channelUrl) } catch (ex: Exception) { errormsg("DeviceProfile channel import error", ex) setResponseStateError(ex.customMessage) }</ID>
|
|
||||||
<ID>MultiLineIfElse:RadioConfigViewModel.kt$RadioConfigViewModel$viewModelScope.launch { radioConfigRepository.replaceAllSettings(new) }</ID>
|
<ID>MultiLineIfElse:RadioConfigViewModel.kt$RadioConfigViewModel$viewModelScope.launch { radioConfigRepository.replaceAllSettings(new) }</ID>
|
||||||
<ID>MultiLineIfElse:RadioInterfaceService.kt$RadioInterfaceService$startInterface()</ID>
|
<ID>MultiLineIfElse:RadioInterfaceService.kt$RadioInterfaceService$startInterface()</ID>
|
||||||
<ID>MultiLineIfElse:SafeBluetooth.kt$SafeBluetooth$cb</ID>
|
<ID>MultiLineIfElse:SafeBluetooth.kt$SafeBluetooth$cb</ID>
|
||||||
|
@ -582,7 +569,6 @@
|
||||||
<ID>NewLineAtEndOfFile:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID>
|
<ID>NewLineAtEndOfFile:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID>
|
||||||
<ID>NewLineAtEndOfFile:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID>
|
<ID>NewLineAtEndOfFile:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID>
|
||||||
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
|
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
|
||||||
<ID>NewLineAtEndOfFile:Theme.kt$com.geeksville.mesh.ui.theme.Theme.kt</ID>
|
|
||||||
<ID>NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
|
<ID>NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
|
||||||
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
|
||||||
<ID>NoBlankLineBeforeRbrace:BluetoothInterface.kt$BluetoothInterface$ </ID>
|
<ID>NoBlankLineBeforeRbrace:BluetoothInterface.kt$BluetoothInterface$ </ID>
|
||||||
|
@ -617,8 +603,6 @@
|
||||||
<ID>NoWildcardImports:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID>
|
<ID>NoWildcardImports:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID>
|
||||||
<ID>NoWildcardImports:DeviceVersionTest.kt$import org.junit.Assert.*</ID>
|
<ID>NoWildcardImports:DeviceVersionTest.kt$import org.junit.Assert.*</ID>
|
||||||
<ID>NoWildcardImports:ExampleUnitTest.kt$import org.junit.Assert.*</ID>
|
<ID>NoWildcardImports:ExampleUnitTest.kt$import org.junit.Assert.*</ID>
|
||||||
<ID>NoWildcardImports:MeshService.kt$import com.geeksville.mesh.*</ID>
|
|
||||||
<ID>NoWildcardImports:MeshService.kt$import com.geeksville.mesh.util.*</ID>
|
|
||||||
<ID>NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.*</ID>
|
<ID>NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.*</ID>
|
||||||
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
|
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
|
||||||
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.material.*</ID>
|
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.material.*</ID>
|
||||||
|
@ -635,7 +619,6 @@
|
||||||
<ID>ParameterListWrapping:SafeBluetooth.kt$SafeBluetooth$( c: BluetoothGattDescriptor, cont: Continuation<BluetoothGattDescriptor>, timeout: Long = 0 )</ID>
|
<ID>ParameterListWrapping:SafeBluetooth.kt$SafeBluetooth$( c: BluetoothGattDescriptor, cont: Continuation<BluetoothGattDescriptor>, timeout: Long = 0 )</ID>
|
||||||
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
|
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
|
||||||
<ID>ReturnCount:ChannelOption.kt$internal fun LoRaConfig.radioFreq(channelNum: Int): Float</ID>
|
<ID>ReturnCount:ChannelOption.kt$internal fun LoRaConfig.radioFreq(channelNum: Int): Float</ID>
|
||||||
<ID>ReturnCount:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
|
|
||||||
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||||
<ID>SpacingAroundColon:PreviewParameterProviders.kt$NodeInfoPreviewParameterProvider$:</ID>
|
<ID>SpacingAroundColon:PreviewParameterProviders.kt$NodeInfoPreviewParameterProvider$:</ID>
|
||||||
<ID>SpacingAroundCurly:AppPrefs.kt$FloatPref$}</ID>
|
<ID>SpacingAroundCurly:AppPrefs.kt$FloatPref$}</ID>
|
||||||
|
@ -650,7 +633,6 @@
|
||||||
<ID>SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
|
<ID>SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
|
||||||
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
|
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
|
||||||
<ID>SwallowedException:MainActivity.kt$MainActivity$ex: BindFailedException</ID>
|
<ID>SwallowedException:MainActivity.kt$MainActivity$ex: BindFailedException</ID>
|
||||||
<ID>SwallowedException:MainActivity.kt$MainActivity$ex: IllegalStateException</ID>
|
|
||||||
<ID>SwallowedException:MeshLog.kt$MeshLog$e: IOException</ID>
|
<ID>SwallowedException:MeshLog.kt$MeshLog$e: IOException</ID>
|
||||||
<ID>SwallowedException:MeshService.kt$MeshService$e: Exception</ID>
|
<ID>SwallowedException:MeshService.kt$MeshService$e: Exception</ID>
|
||||||
<ID>SwallowedException:MeshService.kt$MeshService$e: TimeoutException</ID>
|
<ID>SwallowedException:MeshService.kt$MeshService$e: TimeoutException</ID>
|
||||||
|
@ -664,7 +646,7 @@
|
||||||
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
|
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
|
||||||
<ID>TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable</ID>
|
<ID>TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception</ID>
|
<ID>TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:ChannelFragment.kt$ex: Exception</ID>
|
<ID>TooGenericExceptionCaught:Channel.kt$ex: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable</ID>
|
<ID>TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable</ID>
|
||||||
<ID>TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
|
<ID>TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
|
||||||
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
|
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
|
||||||
|
@ -732,8 +714,6 @@
|
||||||
<ID>WildcardImport:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID>
|
<ID>WildcardImport:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID>
|
||||||
<ID>WildcardImport:DeviceVersionTest.kt$import org.junit.Assert.*</ID>
|
<ID>WildcardImport:DeviceVersionTest.kt$import org.junit.Assert.*</ID>
|
||||||
<ID>WildcardImport:ExampleUnitTest.kt$import org.junit.Assert.*</ID>
|
<ID>WildcardImport:ExampleUnitTest.kt$import org.junit.Assert.*</ID>
|
||||||
<ID>WildcardImport:MeshService.kt$import com.geeksville.mesh.*</ID>
|
|
||||||
<ID>WildcardImport:MeshService.kt$import com.geeksville.mesh.util.*</ID>
|
|
||||||
<ID>WildcardImport:MockInterface.kt$import com.geeksville.mesh.*</ID>
|
<ID>WildcardImport:MockInterface.kt$import com.geeksville.mesh.*</ID>
|
||||||
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
|
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
|
||||||
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.material.*</ID>
|
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.material.*</ID>
|
||||||
|
|
Ładowanie…
Reference in New Issue