diff --git a/.cursor/rules/app-recreation-core.mdc b/.cursor/rules/app-recreation-core.mdc index b767883..5250a45 100644 --- a/.cursor/rules/app-recreation-core.mdc +++ b/.cursor/rules/app-recreation-core.mdc @@ -5,13 +5,22 @@ alwaysApply: true # AIRMQ Recreation Core Rules +## CRITICAL: Path check before any edit + +**BEFORE editing any file, verify its path.** + +- Path contains `airmq-android` but **NOT** `airmq-android-2026` → **NEVER EDIT.** Read-only reference. +- Path contains `airmq-android-2026` → OK to edit. + +Example: `C:\Users\sysop\Desktop\airmq-android\app\...` → **DO NOT MODIFY.** + ## Hard repository constraint ALL CHANGES ONLY IN `airmq-android-2026` REPO; NO CHANGES EVER IN `airmq-android`. ## Repository boundaries -1. Do not modify anything under `C:\Users\sysop\Desktop\airmq-android`. +1. **NEVER** modify anything under `C:\Users\sysop\Desktop\airmq-android`. 2. Treat `airmq-android` as read-only reference only. 3. Create and apply all code/config/build changes only under `C:\Users\sysop\Desktop\airmq-android-2026`. diff --git a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt index 9f0053d..97f0281 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreen.kt @@ -2,6 +2,7 @@ package org.db3.airmq.features.manage import androidx.compose.foundation.background import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,9 +18,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -38,6 +36,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.flow.collectLatest import org.db3.airmq.R @@ -48,6 +47,7 @@ import org.db3.airmq.features.manage.ManageScreenContract.DeviceItem import org.db3.airmq.features.manage.ManageScreenContract.Event import org.db3.airmq.features.manage.ManageScreenContract.State import org.db3.airmq.ui.theme.AirMQTheme +import org.db3.airmq.ui.theme.LegacyBackground import org.db3.airmq.ui.theme.LegacyNavGradientEnd import org.db3.airmq.ui.theme.LegacyNavGradientStart @@ -58,8 +58,7 @@ fun ManageScreen( onOpenSettings: () -> Unit, onOpenLogin: () -> Unit, onOpenLocation: () -> Unit, - onOpenWidgetConstructor: () -> Unit, - onBackToDashboard: () -> Unit, + onOpenAddLocation: () -> Unit, viewModel: ManageViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -71,23 +70,20 @@ fun ManageScreen( Action.OpenSetup -> onOpenSetup() is Action.OpenDevice -> onOpenDevice() is Action.OpenLocation -> onOpenLocation() + is Action.OpenAddLocation -> onOpenAddLocation() } } } ManageScreenContent( uiState = uiState, - onEvent = viewModel::onEvent, - onOpenWidgetConstructor = onOpenWidgetConstructor, - onBackToDashboard = onBackToDashboard + onEvent = viewModel::onEvent ) } @Composable private fun ManageScreenContent( uiState: State, - onEvent: (Event) -> Unit, - onOpenWidgetConstructor: () -> Unit, - onBackToDashboard: () -> Unit + onEvent: (Event) -> Unit ) { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Column( @@ -104,33 +100,31 @@ private fun ManageScreenContent( when (uiState.isAuthorized) { false -> AnonymousContent( modifier = Modifier.weight(1f), - devicesLabel = uiState.devicesLabel, - onSignIn = { onEvent(Event.SignInClicked) } + devicesLabel = uiState.devicesLabel ) true -> AuthorizedContent( - devicesLabel = uiState.devicesLabel, + modifier = Modifier.weight(1f), devices = uiState.devices, - onOpenSetup = { onEvent(Event.SetupClicked) }, onOpenDevice = { onEvent(Event.DeviceClicked(it)) }, - onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) } + onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) }, + onAddLocation = { onEvent.invoke(Event.AddDeviceLocationClicked(it))} ) } - if (uiState.isAuthorized) { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { AirMQButton( - text = stringResource(id = R.string.manage_open_widget_constructor), - onClick = onOpenWidgetConstructor, + text = if (uiState.isAuthorized) stringResource(id = R.string.button_setup) + else stringResource(id = R.string.button_sign_in), + onClick = if (uiState.isAuthorized) { { onEvent(Event.SetupClicked) } } + else { { onEvent(Event.SignInClicked) } }, + style = AirMQButtonStyle.Gradient, + leadingIconRes = if (uiState.isAuthorized) null else R.drawable.ic_account, modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) - AirMQButton( - text = stringResource(id = R.string.back_to_dashboard), - onClick = onBackToDashboard, - style = AirMQButtonStyle.Outlined, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(0.46f) + .padding(bottom = 16.dp) ) } } @@ -222,8 +216,7 @@ private fun ProfileHeader( @Composable private fun AnonymousContent( modifier: Modifier = Modifier, - devicesLabel: String, - onSignIn: () -> Unit + devicesLabel: String ) { Box( modifier = modifier @@ -236,94 +229,143 @@ private fun AnonymousContent( color = Color(0xFFBDBDBD), modifier = Modifier.align(Alignment.Center) ) - AirMQButton( - text = stringResource(id = R.string.button_sign_in), - onClick = onSignIn, - style = AirMQButtonStyle.Gradient, - leadingIconRes = R.drawable.ic_account, - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(0.46f) - .padding(bottom = 16.dp) - ) } } @Composable private fun AuthorizedContent( - devicesLabel: String, + modifier: Modifier = Modifier, devices: List, - onOpenSetup: () -> Unit, onOpenDevice: (String) -> Unit, - onOpenLocation: (String) -> Unit + onOpenLocation: (String) -> Unit, + onAddLocation: (String) -> Unit ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .padding(16.dp) + .background(LegacyBackground) ) { - Text(text = devicesLabel, style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(12.dp)) - AirMQButton( - text = stringResource(id = R.string.button_setup), - onClick = onOpenSetup, - style = AirMQButtonStyle.Gradient, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(12.dp)) - LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) { - items(devices, key = { it.id }) { device -> - DeviceRow( - item = device, - onOpenDevice = { onOpenDevice(device.id) }, - onOpenLocation = { onOpenLocation(device.id) } + if (devices.isEmpty()) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + text = stringResource(id = R.string.text_nothing_to_show), + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Light), + color = Color(0xFF757575), + modifier = Modifier.align(Alignment.Center) ) } + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(top = 8.dp), + verticalArrangement = Arrangement.Top + ) { + item(key = "header") { + DeviceListHeader() + } + items(devices, key = { it.id }) { device -> + DeviceRow( + item = device, + onOpenDevice = { onOpenDevice(device.id) }, + onOpenLocation = { onOpenLocation(device.id) }, + onAddLocation = { onAddLocation(device.id) } + ) + } + item(key = "footer") { + DeviceListFooter() + } + } } } } +private val Black38 = Color(0x61000000) + +@Composable +private fun DeviceListHeader() { + Text( + text = stringResource(id = R.string.text_your_devices), + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 22.dp), + fontSize = 14.sp, + color = Black38, + fontWeight = FontWeight.Medium + ) +} + +@Composable +private fun DeviceListFooter() { + Spacer(modifier = Modifier.height(72.dp)) +} + @Composable private fun DeviceRow( item: DeviceItem, onOpenDevice: () -> Unit, - onOpenLocation: () -> Unit + onOpenLocation: () -> Unit, + onAddLocation: () -> Unit ) { - Card( - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + val isOnline = item.status.equals(stringResource(id = R.string.map_status_online), ignoreCase = true) + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.dp) + .clickable(onClick = onOpenDevice) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - ) { - Text(text = item.name, style = MaterialTheme.typography.titleMedium) + Image( + painter = painterResource(id = R.drawable.ic_chip), + contentDescription = stringResource(id = R.string.content_device_icon), + modifier = Modifier.size(40.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f, fill = true)) { Text( - text = item.status, - style = MaterialTheme.typography.bodyMedium, - color = Color.Gray + text = item.name, + fontSize = 16.sp, + color = Color.Black + ) + Text( + text = item.extra, + fontSize = 14.sp, + color = Black38 + ) + } + Spacer(modifier = Modifier.width(16.dp)) + Image( + painter = painterResource( + id = if (isOnline) R.drawable.device_chip_online else R.drawable.device_chip_offline + ), + contentDescription = item.status, + modifier = Modifier + .align(Alignment.Top) + .padding(top = 18.dp) + ) + val trailingIcon = if (item.hasLocation) R.drawable.ic_go_to_location else R.drawable.ic_warning + IconButton( + onClick = { + if (item.hasLocation) { + onOpenLocation.invoke() + } else { + onAddLocation.invoke() + } + }, + modifier = Modifier.size(48.dp) + ) { + Icon( + painter = painterResource(id = trailingIcon), + contentDescription = null, + tint = Black38 ) - Spacer(modifier = Modifier.height(8.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - AirMQButton( - text = stringResource(id = R.string.button_open), - onClick = onOpenDevice, - style = AirMQButtonStyle.Contained, - modifier = Modifier.weight(1f) - ) - AirMQButton( - text = if (item.hasLocation) { - stringResource(id = R.string.button_view_on_map) - } else { - stringResource(id = R.string.manage_set_location) - }, - onClick = onOpenLocation, - style = AirMQButtonStyle.Outlined, - modifier = Modifier.weight(1f) - ) - } } } } @@ -339,9 +381,7 @@ private fun ManageScreenAnonymousPreview() { userEmail = "Your preferences are not being synced, please sign in", devicesLabel = "Sign in to add devices" ), - onEvent = {}, - onOpenWidgetConstructor = {}, - onBackToDashboard = {} + onEvent = {} ) } } @@ -353,17 +393,14 @@ private fun ManageScreenAuthorizedPreview() { ManageScreenContent( uiState = State( isAuthorized = true, - userName = "Anton Betsun", - userEmail = "messbees@gmail.com", - devicesLabel = "My devices", + userName = "User", + userEmail = "user@example.com", devices = listOf( - DeviceItem("1", "AirMQ #1", "Online", true), - DeviceItem("2", "AirMQ #2", "Offline", false) + DeviceItem("1", "AirMQ #1", "mobile", "Online", true), + DeviceItem("2", "AirMQ #2", "mobile", "Offline", false) ) ), - onEvent = {}, - onOpenWidgetConstructor = {}, - onBackToDashboard = {} + onEvent = {} ) } } diff --git a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreenContract.kt b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreenContract.kt index 9cd5898..bcd2edd 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreenContract.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageScreenContract.kt @@ -4,6 +4,7 @@ object ManageScreenContract { data class DeviceItem( val id: String, val name: String, + val extra: String, val status: String, val hasLocation: Boolean ) @@ -22,6 +23,7 @@ object ManageScreenContract { data object OpenLogin : Action data class OpenDevice(val deviceId: String) : Action data class OpenLocation(val deviceId: String) : Action + data class OpenAddLocation(val deviceId: String) : Action } sealed interface Event { @@ -30,5 +32,6 @@ object ManageScreenContract { data object SignInClicked : Event data class DeviceClicked(val deviceId: String) : Event data class DeviceLocationClicked(val deviceId: String) : Event + data class AddDeviceLocationClicked(val deviceId: String) : Event } } diff --git a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageViewModel.kt b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageViewModel.kt index b9fa094..a16aeb0 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/manage/ManageViewModel.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/manage/ManageViewModel.kt @@ -44,6 +44,7 @@ class ManageViewModel @Inject constructor( Event.SignInClicked -> _actions.tryEmit(Action.OpenLogin) is Event.DeviceClicked -> _actions.tryEmit(Action.OpenDevice(event.deviceId)) is Event.DeviceLocationClicked -> _actions.tryEmit(Action.OpenLocation(event.deviceId)) + is Event.AddDeviceLocationClicked -> _actions.tryEmit(Action.OpenAddLocation(event.deviceId)) } } @@ -71,17 +72,19 @@ class ManageViewModel @Inject constructor( isAuthorized = true, userName = user.displayName ?: appContext.getString(R.string.text_anonymous_user), userEmail = user.email ?: "", - devicesLabel = appContext.getString(R.string.text_your_devices), + devicesLabel = "", devices = listOf( DeviceItem( id = "device-1", name = appContext.getString(R.string.mock_device_name_42), + extra = "mobile", status = appContext.getString(R.string.map_status_online), hasLocation = true ), DeviceItem( id = "device-2", name = appContext.getString(R.string.mock_device_name_17), + extra = "mobile", status = appContext.getString(R.string.map_status_offline), hasLocation = false ) diff --git a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt index 92c948a..d73e1f4 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt @@ -152,8 +152,7 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) { onOpenSettings = { navController.navigate(AirMqRoutes.SETTINGS) }, onOpenLogin = { navController.navigate(AirMqRoutes.LOGIN) }, onOpenLocation = { navController.navigate(AirMqRoutes.LOCATION) }, - onOpenWidgetConstructor = { navController.navigate(AirMqRoutes.WIDGET_CONSTRUCTOR) }, - onBackToDashboard = { navController.navigate(AirMqRoutes.DASHBOARD) } + onOpenAddLocation = { /* TODO */ } ) } composable( diff --git a/app/src/main/res/drawable/device_chip_offline.xml b/app/src/main/res/drawable/device_chip_offline.xml new file mode 100644 index 0000000..aeb1f85 --- /dev/null +++ b/app/src/main/res/drawable/device_chip_offline.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/device_chip_online.xml b/app/src/main/res/drawable/device_chip_online.xml new file mode 100644 index 0000000..bafd412 --- /dev/null +++ b/app/src/main/res/drawable/device_chip_online.xml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_chip.xml b/app/src/main/res/drawable/ic_chip.xml new file mode 100644 index 0000000..728c7be --- /dev/null +++ b/app/src/main/res/drawable/ic_chip.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_go_to_location.xml b/app/src/main/res/drawable/ic_go_to_location.xml new file mode 100644 index 0000000..a176cc6 --- /dev/null +++ b/app/src/main/res/drawable/ic_go_to_location.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml new file mode 100644 index 0000000..1ac26f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/values-be-rBY/strings.xml b/app/src/main/res/values-be-rBY/strings.xml index dea8c14..66437c3 100644 --- a/app/src/main/res/values-be-rBY/strings.xml +++ b/app/src/main/res/values-be-rBY/strings.xml @@ -270,8 +270,8 @@ Канструктар графіка Канструктар навін Назад да навін - Anton Betsun - messbees@gmail.com + User + user@example.com AirMQ #42 AirMQ #17 \ No newline at end of file diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 5f957f7..3e3e4d0 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -270,8 +270,8 @@ Конструктор графика Конструктор новостей Назад к новостям - Anton Betsun - messbees@gmail.com + User + user@example.com AirMQ #42 AirMQ #17 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5210eac..7fde1f2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -14,4 +14,6 @@ #FFF44336 #FFEC407A #FF8E24AA + #61000000 + #FAFAFA \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f06260..7022e1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Enable WiFi Are you sure? About - App by Anton Betsun \nDesign by Alexander Prokhorov + App by AirMQ\nDesign by Alexander Prokhorov Continue anonymously? You will not be able to back up your preferences and add new AirMQ devices Select device @@ -326,8 +326,8 @@ Chart Constructor News Constructor Back to News - Anton Betsun - messbees@gmail.com + User + user@example.com AirMQ #42 AirMQ #17 \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 25b5420..2511db9 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,8 @@ - \ No newline at end of file