Device screen was implemented
This commit is contained in:
@@ -2,6 +2,7 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.devtools.ksp").version("1.6.10-1.0.4")
|
||||
id("org.jlleitschuh.gradle.ktlint").version("12.0.3")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -64,6 +65,7 @@ dependencies {
|
||||
implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
|
||||
implementation("androidx.navigation:navigation-compose:2.6.0")
|
||||
implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
|
||||
BIN
app/src/main/ic_helicopter-playstore.png
Normal file
BIN
app/src/main/ic_helicopter-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -15,10 +15,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.helible.pilot.components.BluetoothScannerScreen
|
||||
import com.helible.pilot.components.FlightControlScreen
|
||||
import com.helible.pilot.components.AppPreferences
|
||||
import com.helible.pilot.components.SavedPreferencesImpl
|
||||
import com.helible.pilot.components.scannerScreen.BluetoothScannerScreen
|
||||
import com.helible.pilot.components.deviceScreen.DeviceControlScreen
|
||||
import com.helible.pilot.viewmodels.AppPreferences
|
||||
import com.helible.pilot.viewmodels.SavedPreferencesImpl
|
||||
import com.helible.pilot.components.deviceScreen.defaultDeviceActionsList
|
||||
import com.helible.pilot.permissions.PermissionsLauncher
|
||||
import com.helible.pilot.permissions.PermissionsRequest
|
||||
import com.helible.pilot.permissions.RequestHardwareFeatures
|
||||
@@ -31,8 +32,10 @@ import com.helible.pilot.viewmodels.PreferencesViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
// TODO: device screen logic
|
||||
// TODO: constrain text size
|
||||
// TODO: add Bluetooth telemetry...
|
||||
// TODO: move text strings to resources
|
||||
// TODO: review permissions logic
|
||||
|
||||
private val preferences by lazy {
|
||||
SavedPreferencesImpl(getSharedPreferences(packageName, MODE_PRIVATE))
|
||||
@@ -127,33 +130,49 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
composable("device")
|
||||
{
|
||||
FlightControlScreen(
|
||||
DeviceControlScreen(
|
||||
bluetoothUiState = bluetoothState,
|
||||
getPreferences = { preferencesViewModel.preferences },
|
||||
navigateToScanner = { navController.navigate("scanner") },
|
||||
navigateToPage = { page -> navController.navigate(page) },
|
||||
connectToDevice = { device ->
|
||||
bluetoothViewModel.connectToDevice(
|
||||
device
|
||||
)
|
||||
},
|
||||
sendRotorsState = { message ->
|
||||
bluetoothViewModel.sendRotorsDutySpeed(
|
||||
message
|
||||
)
|
||||
},
|
||||
disconnectFromDevice = { bluetoothViewModel.disconnectFromDevice() },
|
||||
sendEmergStop = { bluetoothViewModel.sendEmergStop() },
|
||||
sendAlarm = { message -> bluetoothViewModel.sendAlarmState(message) },
|
||||
sendR3Duty = { duty -> bluetoothViewModel.sendR3Duty(duty) }
|
||||
deviceActionsList = defaultDeviceActionsList()
|
||||
)
|
||||
if (preferencesViewModel.preferences != null) BackHandler {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
composable("console")
|
||||
{
|
||||
|
||||
}
|
||||
composable("codeblocks")
|
||||
{
|
||||
|
||||
}
|
||||
composable("imu_calibration")
|
||||
{
|
||||
|
||||
}
|
||||
composable("motor_test")
|
||||
{
|
||||
|
||||
}
|
||||
composable("pid_settings")
|
||||
{
|
||||
|
||||
}
|
||||
composable("reports")
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.helible.pilot.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DeviceScreen() {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Text(text = "")
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package com.helible.pilot.components
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.dataclasses.AlarmStateMessage
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.dataclasses.RotorsSpeedMessage
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@Composable
|
||||
fun FlightControlScreen(
|
||||
bluetoothUiState: BluetoothUiState,
|
||||
getPreferences: () -> AppPreferences?,
|
||||
navigateToScanner: () -> Unit,
|
||||
connectToDevice: (String) -> Unit,
|
||||
disconnectFromDevice: () -> Unit,
|
||||
sendRotorsState: (RotorsSpeedMessage) -> Unit,
|
||||
sendAlarm: (AlarmStateMessage) -> Unit,
|
||||
sendEmergStop: () -> Unit,
|
||||
sendR3Duty: (Int) -> Unit,
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
val preferences: AppPreferences? = getPreferences()
|
||||
if (preferences == null) {
|
||||
navigateToScanner()
|
||||
} else {
|
||||
connectToDevice(preferences.deviceAddress)
|
||||
}
|
||||
}
|
||||
|
||||
var rotor1Duty by remember { mutableStateOf(0f) }
|
||||
var rotor2Duty by remember { mutableStateOf(0f) }
|
||||
var rotor3Duty by remember { mutableStateOf(0f) }
|
||||
|
||||
BackHandler {
|
||||
disconnectFromDevice()
|
||||
Log.i("FlightScreen", "Disconnected from the device")
|
||||
navigateToScanner()
|
||||
}
|
||||
when {
|
||||
bluetoothUiState.isConnecting -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(text = "Подключение...", textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
text = "Device name: ${getPreferences()?.deviceName ?: "(устройство отключено)"}",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(text = "Rotor 1 value: $rotor1Duty", textAlign = TextAlign.Center)
|
||||
Slider(
|
||||
value = rotor1Duty,
|
||||
onValueChange = { rotor1Duty = it.roundToInt().toFloat() },
|
||||
valueRange = 0f..1000f,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
)
|
||||
Text(text = "Rotor 2 value: $rotor2Duty", textAlign = TextAlign.Center)
|
||||
Slider(
|
||||
value = rotor2Duty,
|
||||
onValueChange = { rotor2Duty = it.roundToInt().toFloat() },
|
||||
valueRange = 0f..1000f,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
)
|
||||
Text(text = "Rotor 3 value: $rotor1Duty", textAlign = TextAlign.Center)
|
||||
Slider(
|
||||
value = rotor3Duty,
|
||||
onValueChange = { rotor3Duty = it.roundToInt().toFloat() },
|
||||
valueRange = 0f..1000f,
|
||||
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp)
|
||||
)
|
||||
FilledIconButton(
|
||||
onClick = { sendEmergStop() },
|
||||
modifier = Modifier.padding(10.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(3.dp)
|
||||
)
|
||||
Text(
|
||||
text = "СТОП",
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,6 @@ fun Title(text: String, modifier: Modifier = Modifier) {
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier,
|
||||
fontSize = 23.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.helible.pilot.components.deviceScreen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.R
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.viewmodels.AppPreferences
|
||||
|
||||
@Composable
|
||||
fun DeviceBadge(
|
||||
bluetoothUiState: BluetoothUiState,
|
||||
tryToReconnect: () -> Unit,
|
||||
getPreferences: () -> AppPreferences?
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 15.dp),
|
||||
shape = RoundedCornerShape(15)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 15.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(modifier = Modifier
|
||||
.size(60.dp)
|
||||
.graphicsLayer {
|
||||
clip = true
|
||||
shape = RoundedCornerShape(15)
|
||||
}
|
||||
.fillMaxSize()) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.helicopter),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
Text(
|
||||
text = getPreferences()?.deviceName ?: "null",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
DeviceConnectionStatus(bluetoothUiState)
|
||||
Text(text = "Заряд батареи: 79%")
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier
|
||||
.padding(2.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Refresh,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.requiredSize(Icons.Default.Refresh.defaultWidth)
|
||||
.clickable { tryToReconnect() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceBadgePreview() {
|
||||
DeviceBadge(
|
||||
bluetoothUiState = BluetoothUiState(isConnected = true),
|
||||
tryToReconnect = {},
|
||||
getPreferences = {AppPreferences("Helicopter", "AA:BB:CC:FF:DD")}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.helible.pilot.components.deviceScreen
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.R
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
|
||||
@Composable
|
||||
fun DeviceConnectionStatus(bluetoothState: BluetoothUiState) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (bluetoothState.isConnected) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = Color(56, 200, 35),
|
||||
modifier = Modifier
|
||||
.requiredSize(Icons.Default.CheckCircle.defaultWidth)
|
||||
.padding(2.dp)
|
||||
)
|
||||
Text ("На связи")
|
||||
}
|
||||
else if (bluetoothState.errorMessage != null) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.cancel),
|
||||
contentDescription = null,
|
||||
tint = Color(255, 24, 35),
|
||||
modifier = Modifier
|
||||
.requiredSize(R.drawable.cancel.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
Text ("Ошибка: ${bluetoothState.errorMessage}")
|
||||
}
|
||||
else if (bluetoothState.isConnecting) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.sync),
|
||||
contentDescription = null,
|
||||
tint = Color(40, 123, 207),
|
||||
modifier = Modifier
|
||||
.requiredSize(R.drawable.sync.dp)
|
||||
.padding(2.dp)
|
||||
)
|
||||
Text ("Подключение...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceConnectionStatusPreview() {
|
||||
Surface {
|
||||
DeviceConnectionStatus(bluetoothState = BluetoothUiState(isConnecting = true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.helible.pilot.components.deviceScreen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.R
|
||||
import com.helible.pilot.components.Title
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.viewmodels.AppPreferences
|
||||
|
||||
@Composable
|
||||
fun DeviceControlScreen(
|
||||
bluetoothUiState: BluetoothUiState,
|
||||
getPreferences: () -> AppPreferences?,
|
||||
navigateToPage: (String) -> Unit,
|
||||
connectToDevice: (String) -> Unit,
|
||||
disconnectFromDevice: () -> Unit,
|
||||
deviceActionsList: Map<String, Array<Pair<String, Pair<Pair<Int, Color>, String>>>>,
|
||||
scannerPageName: String = "scanner",
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
val preferences: AppPreferences? = getPreferences()
|
||||
if (preferences == null) {
|
||||
navigateToPage(scannerPageName)
|
||||
} else {
|
||||
connectToDevice(preferences.deviceAddress)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = bluetoothUiState.isEnabled) {
|
||||
/* Trying to reconnect, when bluetooth is turned on */
|
||||
val preferences = getPreferences()
|
||||
if(preferences != null && bluetoothUiState.isEnabled)
|
||||
connectToDevice(preferences.deviceAddress)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = bluetoothUiState.isLocationEnabled) {
|
||||
/* Trying to reconnect, when location is turned on */
|
||||
val preferences = getPreferences()
|
||||
if(preferences != null && bluetoothUiState.isLocationEnabled)
|
||||
connectToDevice(preferences.deviceAddress)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(5.dp)
|
||||
) {
|
||||
Title(
|
||||
text = "Ваше устройство",
|
||||
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||
)
|
||||
DeviceBadge(
|
||||
bluetoothUiState = bluetoothUiState,
|
||||
tryToReconnect = {
|
||||
/* Trying to reconnect, when error occurred */
|
||||
val preferences = getPreferences()
|
||||
if(preferences != null)
|
||||
connectToDevice(preferences.deviceAddress)
|
||||
},
|
||||
getPreferences = getPreferences
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 3.dp)) {
|
||||
for (section in deviceActionsList) {
|
||||
Text(section.key,
|
||||
color = Color.Gray,
|
||||
fontWeight = FontWeight.Light,
|
||||
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||
)
|
||||
for (action in section.value) {
|
||||
TextButton(onClick = { /* TODO */}) {
|
||||
Icon(
|
||||
painter = painterResource(id = action.second.first.first),
|
||||
tint = action.second.first.second,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(25.dp)
|
||||
)
|
||||
Text(
|
||||
text = action.second.second,
|
||||
color = MaterialTheme.colorScheme.inverseSurface,
|
||||
modifier = Modifier.padding(horizontal = 5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = {
|
||||
disconnectFromDevice()
|
||||
navigateToPage(scannerPageName)
|
||||
}, modifier = Modifier.padding(vertical = 10.dp)) {
|
||||
Icon(painterResource(id = R.drawable.logout), contentDescription = null)
|
||||
Text(
|
||||
text = "Отвязать устройство",
|
||||
color = MaterialTheme.colorScheme.inverseSurface,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceControlScreenPreview() {
|
||||
Surface {
|
||||
DeviceControlScreen(
|
||||
bluetoothUiState = BluetoothUiState(isConnected = true),
|
||||
getPreferences = { AppPreferences("Helicopter", "AA:BB:CC:DD:FF") },
|
||||
navigateToPage = { /*TODO*/ },
|
||||
connectToDevice = {},
|
||||
disconnectFromDevice = { /*TODO*/ },
|
||||
deviceActionsList = defaultDeviceActionsList()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.helible.pilot.components.deviceScreen
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.helible.pilot.R
|
||||
|
||||
@Composable
|
||||
fun defaultDeviceActionsList(): Map<String, Array<Pair<String, Pair<Pair<Int, Color>, String>>>> {
|
||||
return mapOf(
|
||||
Pair(
|
||||
"Управление",
|
||||
arrayOf(
|
||||
Pair(
|
||||
"console",
|
||||
Pair(
|
||||
Pair(R.drawable.joystick, MaterialTheme.colorScheme.primary),
|
||||
"Пульт управления"
|
||||
)
|
||||
),
|
||||
Pair(
|
||||
"codeblocks",
|
||||
Pair(
|
||||
Pair(R.drawable.code_blocks, MaterialTheme.colorScheme.primary),
|
||||
"Палитра команд"
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Pair(
|
||||
"Настройки",
|
||||
arrayOf(
|
||||
Pair(
|
||||
"imu_calibration",
|
||||
Pair(
|
||||
Pair(R.drawable.tune, MaterialTheme.colorScheme.primary),
|
||||
"Калибровка гироскопа и акселерометра"
|
||||
)
|
||||
),
|
||||
Pair(
|
||||
"motor_test",
|
||||
Pair(
|
||||
Pair(R.drawable.helicopter_icon, MaterialTheme.colorScheme.primary),
|
||||
"Тестирование двигателей"
|
||||
)
|
||||
),
|
||||
Pair(
|
||||
"pid_settings",
|
||||
Pair(
|
||||
Pair(R.drawable.controller_gen, MaterialTheme.colorScheme.primary),
|
||||
"Настройки ПИД регуляторов"
|
||||
)
|
||||
),
|
||||
Pair(
|
||||
"reports",
|
||||
Pair(
|
||||
Pair(R.drawable.construction, MaterialTheme.colorScheme.primary),
|
||||
"Отчеты о полётах"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.helible.pilot.components
|
||||
package com.helible.pilot.components.scannerScreen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import com.helible.pilot.components.Title
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||
|
||||
@@ -103,7 +104,6 @@ fun BluetoothScannerScreen(
|
||||
Text(text = "Далее")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.helible.pilot.components
|
||||
package com.helible.pilot.components.scannerScreen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||
import com.helible.pilot.R
|
||||
@@ -42,7 +44,7 @@ fun DeviceItem(
|
||||
)
|
||||
) {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
|
||||
Text(
|
||||
text = deviceInfo.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@@ -75,3 +77,14 @@ fun getSignalIconForRssiValue(rssi: Short): Int {
|
||||
else if (rssi >= -100) return R.drawable.signal_icon2
|
||||
return R.drawable.signal_icon1
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DeviceItemPreview() {
|
||||
DeviceItem(
|
||||
BluetoothDevice("Helicopter", "AA:BB:CC:DD:FF", -90, true),
|
||||
null,
|
||||
{_ -> },
|
||||
modifier = Modifier.size(500.dp, 60.dp)
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.helible.pilot.components
|
||||
package com.helible.pilot.components.scannerScreen
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.helible.pilot.components.scannerScreen.DeviceItem
|
||||
import com.helible.pilot.dataclasses.BluetoothUiState
|
||||
import com.helible.pilot.dataclasses.BluetoothDevice
|
||||
|
||||
@@ -57,24 +57,24 @@ class BluetoothViewModel(
|
||||
it.copy(errorMessage = error)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
bluetoothController.isScanning.onEach { result ->
|
||||
bluetoothController.isScanning.onEach { isDiscovering ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
isDiscovering = result,
|
||||
isDiscovering = isDiscovering,
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
bluetoothController.isEnabled.onEach { result ->
|
||||
bluetoothController.isEnabled.onEach { isEnabled ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
isEnabled = result,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
bluetoothController.isLocationEnabled.onEach { result ->
|
||||
bluetoothController.isLocationEnabled.onEach { isLocationEnabled ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLocationEnabled = result
|
||||
isLocationEnabled = isLocationEnabled
|
||||
)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
@@ -123,6 +123,9 @@ class BluetoothViewModel(
|
||||
private var deviceConnectionJob: Job? = null
|
||||
|
||||
fun connectToDevice(device: String) {
|
||||
if(_state.value.isConnected) {
|
||||
return
|
||||
}
|
||||
_state.update { it.copy(isConnecting = true) }
|
||||
deviceConnectionJob = bluetoothController
|
||||
.connectToDevice(device)
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.helible.pilot.viewmodels
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.helible.pilot.components.AppPreferences
|
||||
import com.helible.pilot.components.SavedPreferences
|
||||
|
||||
class PermissionDialogViewModel : ViewModel() {
|
||||
val visiblePermissionDialogQueue = mutableStateListOf<String>()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.helible.pilot.components
|
||||
package com.helible.pilot.viewmodels
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
10
app/src/main/res/drawable/cancel.xml
Normal file
10
app/src/main/res/drawable/cancel.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M336,680L480,536L624,680L680,624L536,480L680,336L624,280L480,424L336,280L280,336L424,480L280,624L336,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/code_blocks.xml
Normal file
10
app/src/main/res/drawable/code_blocks.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M384,624L440,567L353,480L440,393L384,336L240,480L384,624ZM576,624L720,480L576,336L520,393L607,480L520,567L576,624ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/construction.xml
Normal file
10
app/src/main/res/drawable/construction.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M756,840L537,621L621,537L840,756L756,840ZM204,840L120,756L396,480L328,412L300,440L249,389L249,471L221,499L100,378L128,350L210,350L160,300L302,158Q322,138 345,129Q368,120 392,120Q416,120 439,129Q462,138 482,158L390,250L440,300L412,328L480,396L570,306Q566,295 563.5,283Q561,271 561,259Q561,200 601.5,159.5Q642,119 701,119Q716,119 729.5,122Q743,125 757,131L658,230L730,302L829,203Q836,217 838.5,230.5Q841,244 841,259Q841,318 800.5,358.5Q760,399 701,399Q689,399 677,397Q665,395 654,390L204,840Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/controller_gen.xml
Normal file
10
app/src/main/res/drawable/controller_gen.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,720Q580,720 650,650Q720,580 720,480Q720,380 650,310Q580,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720ZM480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,546 593,593Q546,640 480,640ZM452,508Q463,519 480,519Q497,519 508,508L564,452Q575,441 575,424Q575,407 564,396Q553,385 536,385Q519,385 508,396L452,452Q441,463 441,480Q441,497 452,508ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/energy.xml
Normal file
10
app/src/main/res/drawable/energy.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M360,880L360,840Q360,808 384,786.5Q408,765 440,760L440,760L440,474Q418,466 401.5,451Q385,436 374,416L298,436Q262,444 229.5,430Q197,416 188,382L362,338Q370,297 400,270Q430,243 472,240L496,150Q506,115 534.5,94.5Q563,74 598,82L548,262Q573,278 586.5,304Q600,330 600,360Q600,373 597,385.5Q594,398 590,410L644,466Q669,492 673.5,527Q678,562 654,586L534,466Q531,469 527.5,470.5Q524,472 520,474L520,760L520,760Q552,765 576,786.5Q600,808 600,840L600,880L360,880ZM480,420Q505,420 522.5,402.5Q540,385 540,360Q540,335 522.5,317.5Q505,300 480,300Q455,300 437.5,317.5Q420,335 420,360Q420,385 437.5,402.5Q455,420 480,420Z"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/helicopter.png
Normal file
BIN
app/src/main/res/drawable/helicopter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
10
app/src/main/res/drawable/helicopter_icon.xml
Normal file
10
app/src/main/res/drawable/helicopter_icon.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M360,520L360,280L360,280Q260,280 190,350Q120,420 120,520L120,520L360,520ZM520,680L520,280L440,280L440,600L120,600L120,680Q120,680 120,680Q120,680 120,680L520,680ZM600,552L840,528L840,480L600,480L600,552ZM520,880L120,880L120,800L520,800L520,880ZM600,760L120,760Q87,760 63.5,736.5Q40,713 40,680L40,520Q40,386 133,293Q226,200 360,200L600,200L600,400L800,400L840,320L920,320L920,600L600,632L600,760ZM760,160L120,160L120,80L760,80L760,160ZM600,552L600,480L600,480L600,552L600,552ZM520,680L520,680Q520,680 520,680Q520,680 520,680L520,680L520,680L520,680L520,680L520,680Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/instant_mix.xml
Normal file
10
app/src/main/res/drawable/instant_mix.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,800L200,520L120,520L120,440L360,440L360,520L280,520L280,800L200,800ZM200,360L200,160L280,160L280,360L200,360ZM360,360L360,280L440,280L440,160L520,160L520,280L600,280L600,360L360,360ZM440,800L440,440L520,440L520,800L440,800ZM680,800L680,680L600,680L600,600L840,600L840,680L760,680L760,800L680,800ZM680,520L680,160L760,160L760,520L680,520Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/joystick.xml
Normal file
10
app/src/main/res/drawable/joystick.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/holo_orange_light"
|
||||
android:pathData="M272,520L480,640Q480,640 480,640Q480,640 480,640L688,520L520,423L520,560L440,560L440,423L272,520ZM440,331L440,314Q396,301 368,264.5Q340,228 340,180Q340,122 381,81Q422,40 480,40Q538,40 579,81Q620,122 620,180Q620,228 592,264.5Q564,301 520,314L520,331L800,492Q819,503 829.5,521.5Q840,540 840,562L840,638Q840,660 829.5,678.5Q819,697 800,708L520,869Q501,880 480,880Q459,880 440,869L160,708Q141,697 130.5,678.5Q120,660 120,638L120,562Q120,540 130.5,521.5Q141,503 160,492L440,331ZM440,709L200,571L200,638Q200,638 200,638Q200,638 200,638L480,800Q480,800 480,800Q480,800 480,800L760,638Q760,638 760,638Q760,638 760,638L760,571L520,709Q501,720 480,720Q459,720 440,709ZM480,240Q505,240 522.5,222.5Q540,205 540,180Q540,155 522.5,137.5Q505,120 480,120Q455,120 437.5,137.5Q420,155 420,180Q420,205 437.5,222.5Q455,240 480,240ZM480,800Q480,800 480,800Q480,800 480,800L480,800L480,800Q480,800 480,800Q480,800 480,800L480,800Q480,800 480,800Q480,800 480,800L480,800Q480,800 480,800Q480,800 480,800L480,800L480,800Z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/logout.xml
Normal file
11
app/src/main/res/drawable/logout.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L480,120L480,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L480,760L480,840L200,840ZM640,680L585,622L687,520L360,520L360,440L687,440L585,338L640,280L840,480L640,680Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/sync.xml
Normal file
10
app/src/main/res/drawable/sync.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M160,800L160,720L270,720L254,706Q202,660 181,601Q160,542 160,482Q160,371 226.5,284.5Q293,198 400,170L400,254Q328,280 284,342.5Q240,405 240,482Q240,527 257,569.5Q274,612 310,648L320,658L320,560L400,560L400,800L160,800ZM560,790L560,706Q632,680 676,617.5Q720,555 720,478Q720,433 703,390.5Q686,348 650,312L640,302L640,400L560,400L560,160L800,160L800,240L690,240L706,254Q755,303 777.5,360.5Q800,418 800,478Q800,589 733.5,675.5Q667,762 560,790Z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/tune.xml
Normal file
10
app/src/main/res/drawable/tune.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?android:colorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M440,840L440,600L520,600L520,680L840,680L840,760L520,760L520,840L440,840ZM120,760L120,680L360,680L360,760L120,760ZM280,600L280,520L120,520L120,440L280,440L280,360L360,360L360,600L280,600ZM440,520L440,440L840,440L840,520L440,520ZM600,360L600,120L680,120L680,200L840,200L840,280L680,280L680,360L600,360ZM120,280L120,200L520,200L520,280L120,280Z"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user