|
@ -31,8 +31,8 @@ android {
|
||||||
applicationId "com.geeksville.mesh"
|
applicationId "com.geeksville.mesh"
|
||||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 20137 // format is Mmmss (where M is 1+the numeric major number
|
versionCode 20138 // format is Mmmss (where M is 1+the numeric major number
|
||||||
versionName "1.1.37"
|
versionName "1.1.38"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 18 KiB Po Szerokość: | Wysokość: | Rozmiar: 13 KiB |
|
@ -606,7 +606,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
||||||
|
|
||||||
/// Called when we gain/lose a connection to our mesh radio
|
/// Called when we gain/lose a connection to our mesh radio
|
||||||
private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) {
|
private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) {
|
||||||
debug("connchange ${model.isConnected.value}")
|
debug("connchange ${model.isConnected.value} -> $connected")
|
||||||
|
|
||||||
if (connected == MeshService.ConnectionState.CONNECTED) {
|
if (connected == MeshService.ConnectionState.CONNECTED) {
|
||||||
model.meshService?.let { service ->
|
model.meshService?.let { service ->
|
||||||
|
|
|
@ -2,13 +2,13 @@ package com.geeksville.mesh.model
|
||||||
|
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.geeksville.android.BuildUtils.isEmulator
|
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.mesh.DataPacket
|
import com.geeksville.mesh.DataPacket
|
||||||
import com.geeksville.mesh.MessageStatus
|
import com.geeksville.mesh.MessageStatus
|
||||||
|
|
||||||
|
|
||||||
class MessagesState(private val ui: UIViewModel) : Logging {
|
class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
|
/* We now provide fake messages a via MockInterface
|
||||||
private val testTexts = listOf(
|
private val testTexts = listOf(
|
||||||
DataPacket(
|
DataPacket(
|
||||||
"+16508765310",
|
"+16508765310",
|
||||||
|
@ -18,10 +18,10 @@ class MessagesState(private val ui: UIViewModel) : Logging {
|
||||||
"+16508765311",
|
"+16508765311",
|
||||||
"Help! I've fallen and I can't get up."
|
"Help! I've fallen and I can't get up."
|
||||||
)
|
)
|
||||||
)
|
) */
|
||||||
|
|
||||||
/// This is the inner storage for messages
|
/// This is the inner storage for messages
|
||||||
private val messagesList = (if (isEmulator) testTexts else emptyList()).toMutableList()
|
private val messagesList = emptyList<DataPacket>().toMutableList()
|
||||||
|
|
||||||
// If the following (unused otherwise) line is commented out, the IDE preview window works.
|
// If the following (unused otherwise) line is commented out, the IDE preview window works.
|
||||||
// if left in the preview always renders as empty.
|
// if left in the preview always renders as empty.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.geeksville.mesh.model
|
package com.geeksville.mesh.model
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.geeksville.android.BuildUtils.isEmulator
|
|
||||||
import com.geeksville.mesh.MeshUser
|
import com.geeksville.mesh.MeshUser
|
||||||
import com.geeksville.mesh.NodeInfo
|
import com.geeksville.mesh.NodeInfo
|
||||||
import com.geeksville.mesh.Position
|
import com.geeksville.mesh.Position
|
||||||
|
@ -43,15 +42,15 @@ class NodeDB(private val ui: UIViewModel) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val seedWithTestNodes = isEmulator
|
private val seedWithTestNodes = false
|
||||||
|
|
||||||
/// The unique ID of our node
|
/// The unique ID of our node
|
||||||
val myId = object : MutableLiveData<String?>(if (isEmulator) "+16508765309" else null) {}
|
val myId = object : MutableLiveData<String?>(if (seedWithTestNodes) "+16508765309" else null) {}
|
||||||
|
|
||||||
/// A map from nodeid to to nodeinfo
|
/// A map from nodeid to to nodeinfo
|
||||||
val nodes =
|
val nodes =
|
||||||
object :
|
object :
|
||||||
MutableLiveData<Map<String, NodeInfo>>(mapOf(*(if (isEmulator) testNodes else listOf()).map { it.user!!.id to it }
|
MutableLiveData<Map<String, NodeInfo>>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it }
|
||||||
.toTypedArray())) {}
|
.toTypedArray())) {}
|
||||||
|
|
||||||
/// Could be null if we haven't received our node DB yet
|
/// Could be null if we haven't received our node DB yet
|
||||||
|
|
|
@ -11,7 +11,6 @@ import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.geeksville.android.BuildUtils.isEmulator
|
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.mesh.IMeshService
|
import com.geeksville.mesh.IMeshService
|
||||||
import com.geeksville.mesh.MeshProtos
|
import com.geeksville.mesh.MeshProtos
|
||||||
|
@ -77,10 +76,7 @@ class UIViewModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
fun getChannel(c: MeshProtos.RadioConfig?): Channel? {
|
fun getChannel(c: MeshProtos.RadioConfig?): Channel? {
|
||||||
val channel = c?.channelSettings?.let { Channel(it) }
|
val channel = c?.channelSettings?.let { Channel(it) }
|
||||||
|
|
||||||
return if (channel == null && isEmulator)
|
return channel
|
||||||
Channel.emulated
|
|
||||||
else
|
|
||||||
channel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPreferences(context: Context): SharedPreferences =
|
fun getPreferences(context: Context): SharedPreferences =
|
||||||
|
|
|
@ -97,6 +97,8 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
||||||
return bluetoothManager.adapter
|
return bluetoothManager.adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toInterfaceName(deviceName: String) = "x$deviceName"
|
||||||
|
|
||||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||||
fun addressValid(context: Context, address: String): Boolean {
|
fun addressValid(context: Context, address: String): Boolean {
|
||||||
val allPaired =
|
val allPaired =
|
||||||
|
|
|
@ -6,11 +6,3 @@ interface IRadioInterface : Closeable {
|
||||||
fun handleSendToRadio(p: ByteArray)
|
fun handleSendToRadio(p: ByteArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
class NopInterface : IRadioInterface {
|
|
||||||
override fun handleSendToRadio(p: ByteArray) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1196,8 +1196,8 @@ class MeshService : Service(), Logging {
|
||||||
if (newMyNodeInfo == null || newNodes.isEmpty())
|
if (newMyNodeInfo == null || newNodes.isEmpty())
|
||||||
errormsg("Did not receive a valid config")
|
errormsg("Did not receive a valid config")
|
||||||
else {
|
else {
|
||||||
debug("Installing new node DB")
|
|
||||||
discardNodeDB()
|
discardNodeDB()
|
||||||
|
debug("Installing new node DB")
|
||||||
myNodeInfo = newMyNodeInfo
|
myNodeInfo = newMyNodeInfo
|
||||||
|
|
||||||
newNodes.forEach(::installNodeInfo)
|
newNodes.forEach(::installNodeInfo)
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
package com.geeksville.mesh.service
|
||||||
|
|
||||||
|
import com.geeksville.android.Logging
|
||||||
|
import com.geeksville.mesh.MeshProtos
|
||||||
|
import com.geeksville.mesh.Portnums
|
||||||
|
import com.geeksville.mesh.Position
|
||||||
|
import com.geeksville.mesh.R
|
||||||
|
import com.geeksville.mesh.model.getInitials
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import okhttp3.internal.toHexString
|
||||||
|
|
||||||
|
/** A simulated interface that is used for testing in the simulator */
|
||||||
|
class MockInterface(private val service: RadioInterfaceService) : Logging, IRadioInterface {
|
||||||
|
companion object : Logging {
|
||||||
|
|
||||||
|
const val interfaceName = "m"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var messageCount = 50
|
||||||
|
|
||||||
|
// an infinite sequence of ints
|
||||||
|
private val messageNumSequence = generateSequence { messageCount++ }.iterator()
|
||||||
|
|
||||||
|
init {
|
||||||
|
info("Starting the mock interface")
|
||||||
|
service.onConnect() // Tell clients they can use the API
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSendToRadio(p: ByteArray) {
|
||||||
|
val pr = MeshProtos.ToRadio.parseFrom(p)
|
||||||
|
|
||||||
|
when {
|
||||||
|
pr.wantConfigId != 0 -> sendConfigResponse(pr.wantConfigId)
|
||||||
|
pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr)
|
||||||
|
else -> info("Ignoring data sent to mock interface $pr")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
info("Closing the mock interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a fake text message from a node
|
||||||
|
private fun makeTextMessage(numIn: Int) =
|
||||||
|
MeshProtos.FromRadio.newBuilder().apply {
|
||||||
|
packet = MeshProtos.MeshPacket.newBuilder().apply {
|
||||||
|
id = messageNumSequence.next()
|
||||||
|
from = numIn
|
||||||
|
to = 0xffffffff.toInt() // ugly way of saying broadcast
|
||||||
|
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||||
|
rxSnr = 1.5f
|
||||||
|
decoded = MeshProtos.SubPacket.newBuilder().apply {
|
||||||
|
data = MeshProtos.Data.newBuilder().apply {
|
||||||
|
portnum = Portnums.PortNum.TEXT_MESSAGE_APP
|
||||||
|
payload = ByteString.copyFromUtf8("This simulated node sends Hi!")
|
||||||
|
}.build()
|
||||||
|
}.build()
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) =
|
||||||
|
MeshProtos.FromRadio.newBuilder().apply {
|
||||||
|
packet = MeshProtos.MeshPacket.newBuilder().apply {
|
||||||
|
id = messageNumSequence.next()
|
||||||
|
from = fromIn
|
||||||
|
to = toIn
|
||||||
|
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||||
|
rxSnr = 1.5f
|
||||||
|
decoded = MeshProtos.SubPacket.newBuilder().apply {
|
||||||
|
data = MeshProtos.Data.newBuilder().apply {
|
||||||
|
successId = msgId
|
||||||
|
}.build()
|
||||||
|
}.build()
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a fake ack packet back if the sender asked for want_ack
|
||||||
|
private fun sendFakeAck(pr: MeshProtos.ToRadio) {
|
||||||
|
service.handleFromRadio(makeAck(pr.packet.to, pr.packet.from, pr.packet.id).build().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendConfigResponse(configId: Int) {
|
||||||
|
debug("Sending mock config response")
|
||||||
|
|
||||||
|
/// Generate a fake node info entry
|
||||||
|
fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) =
|
||||||
|
MeshProtos.FromRadio.newBuilder().apply {
|
||||||
|
nodeInfo = MeshProtos.NodeInfo.newBuilder().apply {
|
||||||
|
num = numIn
|
||||||
|
user = MeshProtos.User.newBuilder().apply {
|
||||||
|
id = "!0x" + num.toHexString()
|
||||||
|
longName = "Sim " + num.toHexString()
|
||||||
|
shortName = getInitials(longName)
|
||||||
|
}.build()
|
||||||
|
position = MeshProtos.Position.newBuilder().apply {
|
||||||
|
latitudeI = Position.degI(lat)
|
||||||
|
longitudeI = Position.degI(lon)
|
||||||
|
batteryLevel = 42
|
||||||
|
altitude = 35
|
||||||
|
time = (System.currentTimeMillis() / 1000).toInt()
|
||||||
|
}.build()
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulated network data to feed to our app
|
||||||
|
val MY_NODE = 0x42424242
|
||||||
|
val packets = arrayOf(
|
||||||
|
// MyNodeInfo
|
||||||
|
MeshProtos.FromRadio.newBuilder().apply {
|
||||||
|
myInfo = MeshProtos.MyNodeInfo.newBuilder().apply {
|
||||||
|
myNodeNum = MY_NODE
|
||||||
|
region = "TW"
|
||||||
|
numChannels = 7
|
||||||
|
hwModel = "Sim"
|
||||||
|
packetIdBits = 32
|
||||||
|
nodeNumBits = 32
|
||||||
|
currentPacketId = 1
|
||||||
|
messageTimeoutMsec = 5 * 60 * 1000
|
||||||
|
firmwareVersion = service.getString(R.string.cur_firmware_version)
|
||||||
|
}.build()
|
||||||
|
},
|
||||||
|
|
||||||
|
// RadioConfig
|
||||||
|
MeshProtos.FromRadio.newBuilder().apply {
|
||||||
|
radio = MeshProtos.RadioConfig.newBuilder().apply {
|
||||||
|
|
||||||
|
preferences = MeshProtos.RadioConfig.UserPreferences.newBuilder().apply {
|
||||||
|
region = MeshProtos.RegionCode.TW
|
||||||
|
// FIXME set critical times?
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
channel = MeshProtos.ChannelSettings.newBuilder().apply {
|
||||||
|
// we just have an empty listing so that the default channel works
|
||||||
|
}.build()
|
||||||
|
}.build()
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fake NodeDB
|
||||||
|
makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas
|
||||||
|
makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
|
||||||
|
|
||||||
|
MeshProtos.FromRadio.newBuilder().apply {
|
||||||
|
configCompleteId = configId
|
||||||
|
},
|
||||||
|
|
||||||
|
// Done with config response, now pretend to receive some text messages
|
||||||
|
|
||||||
|
makeTextMessage(MY_NODE + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
packets.forEach { p ->
|
||||||
|
service.handleFromRadio(p.build().toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.geeksville.mesh.service
|
||||||
|
|
||||||
|
class NopInterface : IRadioInterface {
|
||||||
|
override fun handleSendToRadio(p: ByteArray) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import android.content.SharedPreferences
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.geeksville.android.BinaryLogFile
|
import com.geeksville.android.BinaryLogFile
|
||||||
|
import com.geeksville.android.BuildUtils.isEmulator
|
||||||
import com.geeksville.android.GeeksvilleApplication
|
import com.geeksville.android.GeeksvilleApplication
|
||||||
import com.geeksville.android.Logging
|
import com.geeksville.android.Logging
|
||||||
import com.geeksville.concurrent.handledLaunch
|
import com.geeksville.concurrent.handledLaunch
|
||||||
|
@ -81,12 +82,20 @@ class RadioInterfaceService : Service(), Logging {
|
||||||
rest = null
|
rest = null
|
||||||
|
|
||||||
if (rest != null)
|
if (rest != null)
|
||||||
address = "x$rest" // Add the bluetooth prefix
|
address = BluetoothInterface.toInterfaceName(rest) // Add the bluetooth prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
|
||||||
|
if(address == null && isMockInterfaceAvailable(context))
|
||||||
|
address = MockInterface.interfaceName
|
||||||
|
|
||||||
return address
|
return address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** return true if we should show the mock interface on this device
|
||||||
|
* (ie are we in an emulator or in testlab
|
||||||
|
*/
|
||||||
|
fun isMockInterfaceAvailable(context: Context) = isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)
|
||||||
|
|
||||||
/** Like getDeviceAddress, but filtered to return only devices we are currently bonded with
|
/** Like getDeviceAddress, but filtered to return only devices we are currently bonded with
|
||||||
*
|
*
|
||||||
|
@ -108,6 +117,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||||
'x' -> BluetoothInterface.addressValid(context, rest)
|
'x' -> BluetoothInterface.addressValid(context, rest)
|
||||||
's' -> SerialInterface.addressValid(context, rest)
|
's' -> SerialInterface.addressValid(context, rest)
|
||||||
'n' -> true
|
'n' -> true
|
||||||
|
'm' -> true
|
||||||
else -> TODO("Unexpected interface type $c")
|
else -> TODO("Unexpected interface type $c")
|
||||||
}
|
}
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
|
@ -236,6 +246,7 @@ class RadioInterfaceService : Service(), Logging {
|
||||||
radioIf = when (c) {
|
radioIf = when (c) {
|
||||||
'x' -> BluetoothInterface(this, rest)
|
'x' -> BluetoothInterface(this, rest)
|
||||||
's' -> SerialInterface(this, rest)
|
's' -> SerialInterface(this, rest)
|
||||||
|
'm' -> MockInterface(this)
|
||||||
'n' -> nopIf
|
'n' -> nopIf
|
||||||
else -> {
|
else -> {
|
||||||
errormsg("Unexpected radio interface type")
|
errormsg("Unexpected radio interface type")
|
||||||
|
|
|
@ -265,10 +265,11 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||||
debug("BTScan component active")
|
debug("BTScan component active")
|
||||||
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
|
selectedAddress = RadioInterfaceService.getDeviceAddress(context)
|
||||||
|
|
||||||
return if (bluetoothAdapter == null) {
|
return if (bluetoothAdapter == null || RadioInterfaceService.isMockInterfaceAvailable(context)) {
|
||||||
warn("No bluetooth adapter. Running under emulation?")
|
warn("No bluetooth adapter. Running under emulation?")
|
||||||
|
|
||||||
val testnodes = listOf(
|
val testnodes = listOf(
|
||||||
|
DeviceListEntry("Simulated interface", "m", true),
|
||||||
DeviceListEntry("Meshtastic_ab12", "xaa", false),
|
DeviceListEntry("Meshtastic_ab12", "xaa", false),
|
||||||
DeviceListEntry("Meshtastic_32ac", "xbb", true)
|
DeviceListEntry("Meshtastic_32ac", "xbb", true)
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
android:translateX="18.36"
|
android:translateX="18.36"
|
||||||
android:translateY="18.36">
|
android:translateY="18.36">
|
||||||
<path
|
<path
|
||||||
android:pathData="m16.9229,9.3914 l-4.8076,13.3628 -1.0141,-1.3106 5.3136,-14.7693c0.1142,-0.3174 0.3038,-0.5075 0.5065,-0.5079 0.2028,-0.0003 0.3925,0.1891 0.507,0.5062l5.3259,14.7461 -1.0128,1.3141z"
|
android:pathData="m17.5564,11.8482 l-5.208,7.6376 -1.5217,-1.0377 5.9674,-8.7512c0.1714,-0.2513 0.4558,-0.4019 0.76,-0.4022 0.3042,-0.0003 0.5889,0.1497 0.7608,0.4008l5.9811,8.7374 -1.5199,1.0404z"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:fillColor="#2c2d3c"
|
android:fillColor="#2c2d3c"
|
||||||
android:fillType="evenOdd"/>
|
android:fillType="evenOdd"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="m6.8349,22.7403 l5.6159,-15.6089 -1.0141,-1.3106 -5.6159,15.6089z"
|
android:pathData="m5.854,19.4956 l6.3707,-9.3423 -1.5749,-1.0739 -6.3707,9.3423z"
|
||||||
android:strokeLineJoin="round"
|
android:strokeLineJoin="round"
|
||||||
android:fillColor="#2c2d3c"
|
android:fillColor="#2c2d3c"
|
||||||
android:fillType="evenOdd"/>
|
android:fillType="evenOdd"/>
|
||||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 1.8 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.5 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 3.8 KiB Po Szerokość: | Wysokość: | Rozmiar: 3.5 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 1.3 KiB Po Szerokość: | Wysokość: | Rozmiar: 1.1 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 2.4 KiB Po Szerokość: | Wysokość: | Rozmiar: 2.1 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 2.5 KiB Po Szerokość: | Wysokość: | Rozmiar: 2.1 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 5.4 KiB Po Szerokość: | Wysokość: | Rozmiar: 4.9 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 3.9 KiB Po Szerokość: | Wysokość: | Rozmiar: 3.3 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 8.3 KiB Po Szerokość: | Wysokość: | Rozmiar: 7.6 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 5.3 KiB Po Szerokość: | Wysokość: | Rozmiar: 4.5 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 12 KiB Po Szerokość: | Wysokość: | Rozmiar: 11 KiB |
Przed Szerokość: | Wysokość: | Rozmiar: 156 KiB Po Szerokość: | Wysokość: | Rozmiar: 156 KiB |
2
design
|
@ -1 +1 @@
|
||||||
Subproject commit 9f340a53463ad38516c372adac80e966ccf7a21a
|
Subproject commit a81074152157fa54b0d02ccbbd6a6357cc3cedcf
|