mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Merge remote-tracking branch 'origin/master' into dev
# Conflicts: # app/src/main/java/com/geeksville/mesh/service/MeshService.kt # app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt
This commit is contained in:
commit
48ea4f50fa
19 changed files with 260 additions and 82 deletions
|
|
@ -13,6 +13,7 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
|
@ -29,6 +30,7 @@ import com.geeksville.mesh.ui.AppStatus
|
|||
import com.geeksville.mesh.ui.MeshApp
|
||||
import com.geeksville.mesh.ui.ScanState
|
||||
import com.geeksville.mesh.ui.Screen
|
||||
import com.geeksville.util.Exceptions
|
||||
import com.geeksville.util.exceptionReporter
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
|
|
@ -239,6 +241,8 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
|
||||
override fun onDestroy() {
|
||||
unregisterMeshReceiver()
|
||||
UIState.meshService =
|
||||
null // When our activity goes away make sure we don't keep a ptr around to the service
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +332,18 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return try {
|
||||
super.dispatchTouchEvent(ev)
|
||||
} catch (ex: Throwable) {
|
||||
Exceptions.report(
|
||||
ex,
|
||||
"dispatchTouchEvent"
|
||||
) // hide this Compose error from the user but report to the mothership
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private val meshServiceReceiver = object : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
|
|
@ -395,7 +411,8 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
private fun bindMeshService() {
|
||||
debug("Binding to mesh service!")
|
||||
// we bind using the well known name, to make sure 3rd party apps could also
|
||||
logAssert(UIState.meshService == null)
|
||||
if (UIState.meshService != null)
|
||||
Exceptions.reportError("meshService was supposed to be null, ignoring (but reporting a bug)")
|
||||
|
||||
MeshService.startService(this)?.let { intent ->
|
||||
// ALSO bind so we can use the api
|
||||
|
|
|
|||
|
|
@ -530,6 +530,12 @@ class MeshService : Service(), Logging {
|
|||
|
||||
else -> TODO()
|
||||
}
|
||||
|
||||
GeeksvilleApplication.analytics.track(
|
||||
"data_receive",
|
||||
DataPair("num_bytes", bytes.size),
|
||||
DataPair("type", data.typValue)
|
||||
)
|
||||
}
|
||||
|
||||
/// Update our DB of users based on someone sending out a User subpacket
|
||||
|
|
@ -686,10 +692,20 @@ class MeshService : Service(), Logging {
|
|||
try {
|
||||
reinitFromRadio()
|
||||
|
||||
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
|
||||
GeeksvilleApplication.analytics.track(
|
||||
"mesh_connect",
|
||||
DataPair("num_nodes", numNodes),
|
||||
DataPair("num_online", numOnlineNodes)
|
||||
DataPair("num_online", numOnlineNodes),
|
||||
radioModel
|
||||
)
|
||||
|
||||
// Once someone connects to hardware start tracking the approximate number of nodes in their mesh
|
||||
// this allows us to collect stats on what typical mesh size is and to tell difference between users who just
|
||||
// downloaded the app, vs has connected it to some hardware.
|
||||
GeeksvilleApplication.analytics.setUserInfo(
|
||||
DataPair("num_nodes", numNodes),
|
||||
radioModel
|
||||
)
|
||||
} catch (ex: RemoteException) {
|
||||
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which
|
||||
|
|
@ -838,6 +854,12 @@ class MeshService : Service(), Logging {
|
|||
sendToRadio(ToRadio.newBuilder().apply {
|
||||
this.packet = packet
|
||||
})
|
||||
|
||||
GeeksvilleApplication.analytics.track(
|
||||
"data_send",
|
||||
DataPair("num_bytes", payloadIn.size),
|
||||
DataPair("type", typ)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRadioConfig(): ByteArray = toRemoteExceptions {
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
|
||||
// Handle an incoming packet from the radio, broadcasts it as an android intent
|
||||
private fun handleFromRadio(p: ByteArray) {
|
||||
if(logReceives) {
|
||||
if (logReceives) {
|
||||
receivedPacketsLog.write(p)
|
||||
receivedPacketsLog.flush()
|
||||
}
|
||||
|
|
@ -279,6 +279,9 @@ class RadioInterfaceService : Service(), Logging {
|
|||
|
||||
fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)
|
||||
|
||||
// We must set this to true before broadcasting connectionChanged
|
||||
isConnected = true
|
||||
|
||||
safe!!.setNotify(fromNum, true) {
|
||||
debug("fromNum changed, so we are reading new messages")
|
||||
doReadFromRadio()
|
||||
|
|
@ -286,7 +289,6 @@ class RadioInterfaceService : Service(), Logging {
|
|||
|
||||
// Now tell clients they can (finally use the api)
|
||||
broadcastConnectionChanged(true)
|
||||
isConnected = true
|
||||
|
||||
// Immediately broadcast any queued packets sitting on the device
|
||||
doReadFromRadio()
|
||||
|
|
@ -349,7 +351,7 @@ class RadioInterfaceService : Service(), Logging {
|
|||
info("Closing radio interface service")
|
||||
if (logSends)
|
||||
sentPacketsLog.close()
|
||||
if(logReceives)
|
||||
if (logReceives)
|
||||
receivedPacketsLog.close()
|
||||
safe?.close()
|
||||
safe = null
|
||||
|
|
|
|||
19
app/src/main/java/com/geeksville/mesh/ui/Analytics.kt
Normal file
19
app/src/main/java/com/geeksville/mesh/ui/Analytics.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import androidx.compose.Composable
|
||||
import androidx.compose.onCommit
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
|
||||
/**
|
||||
* Track compose screen visibility
|
||||
*/
|
||||
@Composable
|
||||
fun analyticsScreen(name: String) {
|
||||
onCommit(AppStatus.currentScreen) {
|
||||
GeeksvilleApplication.analytics.sendScreenView(name)
|
||||
|
||||
onDispose {
|
||||
GeeksvilleApplication.analytics.endScreenView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import androidx.ui.core.Text
|
|||
import androidx.ui.foundation.shape.corner.RoundedCornerShape
|
||||
import androidx.ui.graphics.Color
|
||||
import androidx.ui.layout.*
|
||||
import androidx.ui.material.Button
|
||||
import androidx.ui.material.Divider
|
||||
import androidx.ui.material.MaterialTheme
|
||||
import androidx.ui.material.TextButton
|
||||
|
|
@ -75,9 +74,9 @@ private fun DrawerButton(
|
|||
|
||||
Surface(
|
||||
modifier = modifier + LayoutPadding(
|
||||
left = 8.dp,
|
||||
start = 8.dp,
|
||||
top = 8.dp,
|
||||
right = 8.dp,
|
||||
end = 8.dp,
|
||||
bottom = 0.dp
|
||||
),
|
||||
color = backgroundColor,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package com.geeksville.mesh.ui
|
|||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.le.*
|
||||
import android.content.Context
|
||||
import android.os.ParcelUuid
|
||||
import androidx.compose.*
|
||||
import androidx.compose.Composable
|
||||
import androidx.compose.Model
|
||||
import androidx.compose.frames.modelMapOf
|
||||
import androidx.compose.onCommit
|
||||
import androidx.ui.core.ContextAmbient
|
||||
import androidx.ui.core.LayoutModifier
|
||||
import androidx.ui.core.Text
|
||||
import androidx.ui.layout.Column
|
||||
import androidx.ui.layout.LayoutGravity
|
||||
|
|
@ -45,7 +47,7 @@ object ScanState : Logging {
|
|||
debug("stopping scan")
|
||||
try {
|
||||
scanner!!.stopScan(callback)
|
||||
} catch(ex: Throwable) {
|
||||
} catch (ex: Throwable) {
|
||||
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
|
||||
}
|
||||
callback = null
|
||||
|
|
@ -67,7 +69,9 @@ fun BTScanScreen() {
|
|||
val bluetoothAdapter =
|
||||
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
|
||||
|
||||
onActive {
|
||||
analyticsScreen(name = "settings")
|
||||
onCommit(AppStatus.currentScreen) {
|
||||
ScanState.debug("BTScan component active")
|
||||
ScanUIState.selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
|
||||
|
||||
val scanCallback = object : ScanCallback() {
|
||||
|
|
@ -117,10 +121,10 @@ fun BTScanScreen() {
|
|||
/// The following call might return null if the user doesn't have bluetooth access permissions
|
||||
val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
|
||||
|
||||
if(s == null) {
|
||||
ScanUIState.errorText = "This application requires bluetooth access. Please grant access in android settings."
|
||||
}
|
||||
else {
|
||||
if (s == null) {
|
||||
ScanUIState.errorText =
|
||||
"This application requires bluetooth access. Please grant access in android settings."
|
||||
} else {
|
||||
ScanState.debug("starting scan")
|
||||
|
||||
// filter and only accept devices that have a sw update service
|
||||
|
|
@ -138,6 +142,7 @@ fun BTScanScreen() {
|
|||
}
|
||||
|
||||
onDispose {
|
||||
ScanState.debug("BTScan component deactivated")
|
||||
ScanState.stopScan()
|
||||
}
|
||||
}
|
||||
|
|
@ -147,7 +152,10 @@ fun BTScanScreen() {
|
|||
Text("An unexpected error was encountered. Please file a bug on our github: ${ScanUIState.errorText}")
|
||||
} else {
|
||||
if (ScanUIState.devices.isEmpty()) {
|
||||
Text(text = "Looking for Meshtastic devices... (zero found)", modifier = LayoutGravity.Center)
|
||||
Text(
|
||||
text = "Looking for Meshtastic devices... (zero found)",
|
||||
modifier = LayoutGravity.Center
|
||||
)
|
||||
|
||||
CircularProgressIndicator() // Show that we are searching still
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@ package com.geeksville.mesh.ui
|
|||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.Composable
|
||||
import androidx.ui.core.ContextAmbient
|
||||
import androidx.ui.core.Text
|
||||
import androidx.ui.foundation.DrawImage
|
||||
import androidx.ui.graphics.Image
|
||||
import androidx.ui.graphics.ImageConfig
|
||||
import androidx.ui.graphics.NativeImage
|
||||
import androidx.ui.core.*
|
||||
import androidx.ui.foundation.Box
|
||||
import androidx.ui.graphics.*
|
||||
import androidx.ui.graphics.colorspace.ColorSpace
|
||||
import androidx.ui.graphics.colorspace.ColorSpaces
|
||||
import androidx.ui.graphics.painter.ImagePainter
|
||||
import androidx.ui.layout.*
|
||||
import androidx.ui.material.MaterialTheme
|
||||
import androidx.ui.material.OutlinedButton
|
||||
import androidx.ui.material.ripple.Ripple
|
||||
import androidx.ui.tooling.preview.Preview
|
||||
import androidx.ui.unit.Density
|
||||
import androidx.ui.unit.PxSize
|
||||
import androidx.ui.unit.dp
|
||||
import androidx.ui.unit.toRect
|
||||
import com.geeksville.analytics.DataPair
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.R
|
||||
|
|
@ -70,8 +72,39 @@ class AndroidImage(val bitmap: Bitmap) : Image {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Stolen from the Compose SimpleImage, replace with their real Image component someday
|
||||
// TODO(mount, malkov) : remove when RepaintBoundary is a modifier: b/149982905
|
||||
// This is class and not val because if b/149985596
|
||||
private object ClipModifier : DrawModifier {
|
||||
override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) {
|
||||
canvas.save()
|
||||
canvas.clipRect(size.toRect())
|
||||
drawContent()
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stolen from the Compose SimpleImage, replace with their real Image component someday
|
||||
@Composable
|
||||
fun ScaledImage(
|
||||
image: Image,
|
||||
modifier: Modifier = Modifier.None,
|
||||
tint: Color? = null
|
||||
) {
|
||||
with(DensityAmbient.current) {
|
||||
val imageModifier = ImagePainter(image).toModifier(
|
||||
scaleFit = ScaleFit.FillMaxDimension,
|
||||
colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) }
|
||||
)
|
||||
Box(modifier + ClipModifier + imageModifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelContent(channel: Channel = Channel("Default", 7)) {
|
||||
analyticsScreen(name = "channel")
|
||||
|
||||
val typography = MaterialTheme.typography()
|
||||
val context = ContextAmbient.current
|
||||
|
||||
|
|
@ -87,14 +120,18 @@ fun ChannelContent(channel: Channel = Channel("Default", 7)) {
|
|||
// val image = imageResource(id = R.drawable.qrcode)
|
||||
val image = AndroidImage(UIState.getChannelQR(context))
|
||||
|
||||
Container(modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)) {
|
||||
DrawImage(image = image)
|
||||
}
|
||||
ScaledImage(
|
||||
image = image,
|
||||
modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
|
||||
)
|
||||
|
||||
Ripple(bounded = false) {
|
||||
OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(left = 24.dp),
|
||||
OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(start = 24.dp),
|
||||
onClick = {
|
||||
GeeksvilleApplication.analytics.track("channel_share") // track how many times users share channels
|
||||
GeeksvilleApplication.analytics.track(
|
||||
"share",
|
||||
DataPair("content_type", "channel")
|
||||
) // track how many times users share channels
|
||||
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.geeksville.mesh.ui
|
|||
|
||||
import androidx.compose.Composable
|
||||
import androidx.compose.state
|
||||
import androidx.ui.animation.Crossfade
|
||||
import androidx.ui.core.ContextAmbient
|
||||
import androidx.ui.core.Text
|
||||
import androidx.ui.layout.Column
|
||||
|
|
@ -34,6 +33,8 @@ fun getInitials(name: String): String {
|
|||
|
||||
@Composable
|
||||
fun HomeContent() {
|
||||
analyticsScreen(name = "users")
|
||||
|
||||
Column {
|
||||
Row {
|
||||
VectorImage(
|
||||
|
|
@ -124,35 +125,30 @@ fun previewView() {
|
|||
|
||||
@Composable
|
||||
private fun AppContent(openDrawer: () -> Unit) {
|
||||
Crossfade(AppStatus.currentScreen) { screen ->
|
||||
Surface(color = (MaterialTheme.colors()).background) {
|
||||
// crossfade breaks onCommit behavior because it keeps old views around
|
||||
//Crossfade(AppStatus.currentScreen) { screen ->
|
||||
Surface(color = (MaterialTheme.colors()).background) {
|
||||
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text(text = "Meshtastic") },
|
||||
navigationIcon = {
|
||||
Container(LayoutSize(40.dp, 40.dp)) {
|
||||
VectorImageButton(R.drawable.ic_launcher_new_foreground) {
|
||||
openDrawer()
|
||||
}
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text(text = "Meshtastic") },
|
||||
navigationIcon = {
|
||||
Container(LayoutSize(40.dp, 40.dp)) {
|
||||
VectorImageButton(R.drawable.ic_launcher_new_foreground) {
|
||||
openDrawer()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// VerticalScroller breaks flexible layouts - because verticalscrollers have 'infinite' height
|
||||
// VerticalScroller(modifier = LayoutFlexible(1f)) {
|
||||
//if (screen != Screen.settings)
|
||||
// ScanState.stopScan() // Nasty hack to teardown the bt scanner
|
||||
|
||||
when (screen) {
|
||||
Screen.messages -> MessagesContent()
|
||||
Screen.settings -> SettingsContent()
|
||||
Screen.users -> HomeContent()
|
||||
Screen.channel -> ChannelContent()
|
||||
else -> TODO()
|
||||
}
|
||||
//}
|
||||
)
|
||||
|
||||
when (AppStatus.currentScreen) {
|
||||
Screen.messages -> MessagesContent()
|
||||
Screen.settings -> SettingsContent()
|
||||
Screen.users -> HomeContent()
|
||||
Screen.channel -> ChannelContent()
|
||||
else -> TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
|
|||
Row(modifier = modifier) {
|
||||
UserIcon(NodeDB.nodes[msg.from])
|
||||
|
||||
Column(modifier = LayoutPadding(left = 12.dp)) {
|
||||
Column(modifier = LayoutPadding(start = 12.dp)) {
|
||||
Row {
|
||||
val nodes = NodeDB.nodes
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
|
|||
ProvideEmphasis(emphasis = TimestampEmphasis) {
|
||||
Text(
|
||||
text = dateFormat.format(msg.date),
|
||||
modifier = LayoutPadding(left = 8.dp),
|
||||
modifier = LayoutPadding(start = 8.dp),
|
||||
style = MaterialTheme.typography().caption
|
||||
)
|
||||
}
|
||||
|
|
@ -67,6 +67,8 @@ fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
|
|||
|
||||
@Composable
|
||||
fun MessagesContent() {
|
||||
analyticsScreen(name = "messages")
|
||||
|
||||
Column(modifier = LayoutSize.Fill) {
|
||||
|
||||
val sidePad = 8.dp
|
||||
|
|
@ -79,8 +81,8 @@ fun MessagesContent() {
|
|||
messages.forEach { msg ->
|
||||
MessageCard(
|
||||
msg, modifier = LayoutPadding(
|
||||
left = sidePad,
|
||||
right = sidePad,
|
||||
start = sidePad,
|
||||
end = sidePad,
|
||||
top = topPad,
|
||||
bottom = topPad
|
||||
)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ fun NodeInfoCard(node: NodeInfo) {
|
|||
// Text("Node: ${it.user?.longName}")
|
||||
Row(modifier = LayoutPadding(16.dp)) {
|
||||
UserIcon(
|
||||
modifier = LayoutPadding(left = 0.dp, top = 0.dp, right = 0.dp, bottom = 0.dp),
|
||||
modifier = LayoutPadding(start = 0.dp, top = 0.dp, end = 0.dp, bottom = 0.dp),
|
||||
user = node
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import androidx.compose.Composable
|
||||
import androidx.compose.ambient
|
||||
import androidx.compose.state
|
||||
import androidx.ui.core.ContextAmbient
|
||||
import androidx.ui.core.Text
|
||||
|
|
|
|||
|
|
@ -2,25 +2,23 @@ package com.geeksville.mesh.ui
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.Composable
|
||||
import androidx.ui.core.DensityAmbient
|
||||
import androidx.ui.core.Modifier
|
||||
import androidx.ui.foundation.Clickable
|
||||
import androidx.ui.foundation.Icon
|
||||
import androidx.ui.graphics.Color
|
||||
import androidx.ui.graphics.vector.DrawVector
|
||||
import androidx.ui.layout.Container
|
||||
import androidx.ui.layout.LayoutSize
|
||||
import androidx.ui.material.ripple.Ripple
|
||||
import androidx.ui.material.IconButton
|
||||
import androidx.ui.res.vectorResource
|
||||
import androidx.ui.unit.dp
|
||||
|
||||
|
||||
@Composable
|
||||
fun VectorImageButton(@DrawableRes id: Int, onClick: () -> Unit) {
|
||||
Ripple(bounded = false) {
|
||||
Clickable(onClick = onClick) {
|
||||
VectorImage(id = id /* , modifier = LayoutSize(40.dp, 40.dp) */)
|
||||
}
|
||||
//Ripple(bounded = false) {
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(vectorResource(id) /* , modifier = LayoutSize(40.dp, 40.dp) */)
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
/* fun AppBarIcon(icon: Image, onClick: () -> Unit) {
|
||||
|
|
@ -40,13 +38,13 @@ fun VectorImage(
|
|||
) {
|
||||
val vector = vectorResource(id)
|
||||
// WithDensity {
|
||||
Container(
|
||||
modifier = modifier + LayoutSize(
|
||||
vector.defaultWidth,
|
||||
vector.defaultHeight
|
||||
)
|
||||
) {
|
||||
DrawVector(vector, tint)
|
||||
}
|
||||
Container(
|
||||
modifier = modifier + LayoutSize(
|
||||
vector.defaultWidth,
|
||||
vector.defaultHeight
|
||||
)
|
||||
) {
|
||||
DrawVector(vector, tint)
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e1a48b6e75c2ec3ad3995165a7b4fb64ce597e02
|
||||
Subproject commit f309ee8f9e9db37daabd7c76da683e052ef62f7a
|
||||
Loading…
Add table
Add a link
Reference in a new issue