Port manage/settings flows to Compose and wire settings persistence.
Add contract-driven state/action/event view models for manage and settings, migrate settings UI toward legacy preference rows (with anonymous stub behavior), and back SettingsServiceImpl with SharedPreferences for real toggle/city storage. Made-with: Cursor
This commit is contained in:
@@ -1,8 +1,47 @@
|
|||||||
package org.db3.airmq.features.manage
|
package org.db3.airmq.features.manage
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import org.db3.airmq.features.common.MockScreenScaffold
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import org.db3.airmq.features.common.ScreenAction
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import org.db3.airmq.features.common.AirMqButton
|
||||||
|
import org.db3.airmq.features.common.AirMqButtonStyle
|
||||||
|
import org.db3.airmq.features.manage.ManageScreenContract.Action
|
||||||
|
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.features.manage.ManageScreenContract.UserMode
|
||||||
|
import org.db3.airmq.ui.theme.AirMQTheme
|
||||||
|
import org.db3.airmq.ui.theme.LegacyNavGradientEnd
|
||||||
|
import org.db3.airmq.ui.theme.LegacyNavGradientStart
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ManageScreen(
|
fun ManageScreen(
|
||||||
@@ -12,19 +51,276 @@ fun ManageScreen(
|
|||||||
onOpenLogin: () -> Unit,
|
onOpenLogin: () -> Unit,
|
||||||
onOpenLocation: () -> Unit,
|
onOpenLocation: () -> Unit,
|
||||||
onOpenWidgetConstructor: () -> Unit,
|
onOpenWidgetConstructor: () -> Unit,
|
||||||
onBackToDashboard: () -> Unit
|
onBackToDashboard: () -> Unit,
|
||||||
|
viewModel: ManageViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
MockScreenScaffold(
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
title = "Manage",
|
LaunchedEffect(viewModel) {
|
||||||
subtitle = "Bottom-tab equivalent: manage",
|
viewModel.actions.collectLatest { action ->
|
||||||
actions = listOf(
|
when (action) {
|
||||||
ScreenAction("Open Device", onOpenDevice),
|
Action.OpenLogin -> onOpenLogin()
|
||||||
ScreenAction("Start Setup", onOpenSetup),
|
Action.OpenSettings -> onOpenSettings()
|
||||||
ScreenAction("Open Settings", onOpenSettings),
|
Action.OpenSetup -> onOpenSetup()
|
||||||
ScreenAction("Open Login", onOpenLogin),
|
is Action.OpenDevice -> onOpenDevice()
|
||||||
ScreenAction("Select Location", onOpenLocation),
|
is Action.OpenLocation -> onOpenLocation()
|
||||||
ScreenAction("Open Widget Constructor", onOpenWidgetConstructor),
|
}
|
||||||
ScreenAction("Back to Dashboard", onBackToDashboard)
|
}
|
||||||
)
|
}
|
||||||
|
ManageScreenContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onEvent = viewModel::onEvent,
|
||||||
|
onOpenWidgetConstructor = onOpenWidgetConstructor,
|
||||||
|
onBackToDashboard = onBackToDashboard
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ManageScreenContent(
|
||||||
|
uiState: State,
|
||||||
|
onEvent: (Event) -> Unit,
|
||||||
|
onOpenWidgetConstructor: () -> Unit,
|
||||||
|
onBackToDashboard: () -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
) {
|
||||||
|
ProfileHeader(
|
||||||
|
name = uiState.userName,
|
||||||
|
email = uiState.userEmail,
|
||||||
|
onSettingsClick = { onEvent(Event.SettingsClicked) }
|
||||||
|
)
|
||||||
|
when (uiState.userMode) {
|
||||||
|
UserMode.ANONYMOUS -> AnonymousContent(
|
||||||
|
devicesLabel = uiState.devicesLabel,
|
||||||
|
onSignIn = { onEvent(Event.SignInClicked) }
|
||||||
|
)
|
||||||
|
UserMode.AUTHORIZED -> AuthorizedContent(
|
||||||
|
devicesLabel = uiState.devicesLabel,
|
||||||
|
devices = uiState.devices,
|
||||||
|
onOpenSetup = { onEvent(Event.SetupClicked) },
|
||||||
|
onOpenDevice = { onEvent(Event.DeviceClicked(it)) },
|
||||||
|
onOpenLocation = { onEvent(Event.DeviceLocationClicked(it)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
AirMqButton(
|
||||||
|
text = "Open Widget Constructor",
|
||||||
|
onClick = onOpenWidgetConstructor,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
AirMqButton(
|
||||||
|
text = "Back to Dashboard",
|
||||||
|
onClick = onBackToDashboard,
|
||||||
|
style = AirMqButtonStyle.Outlined,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileHeader(
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
onSettingsClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(LegacyNavGradientEnd, LegacyNavGradientStart)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Manage",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
AirMqButton(
|
||||||
|
text = "Settings",
|
||||||
|
onClick = onSettingsClick,
|
||||||
|
style = AirMqButtonStyle.Text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = Color.White.copy(alpha = 0.25f), shape = CircleShape)
|
||||||
|
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name.firstOrNull()?.uppercase() ?: "A",
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = email,
|
||||||
|
color = Color.White.copy(alpha = 0.8f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AnonymousContent(
|
||||||
|
devicesLabel: String,
|
||||||
|
onSignIn: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = devicesLabel,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
AirMqButton(
|
||||||
|
text = "Sign in",
|
||||||
|
onClick = onSignIn,
|
||||||
|
style = AirMqButtonStyle.Gradient,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AuthorizedContent(
|
||||||
|
devicesLabel: String,
|
||||||
|
devices: List<DeviceItem>,
|
||||||
|
onOpenSetup: () -> Unit,
|
||||||
|
onOpenDevice: (String) -> Unit,
|
||||||
|
onOpenLocation: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = devicesLabel, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
AirMqButton(
|
||||||
|
text = "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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeviceRow(
|
||||||
|
item: DeviceItem,
|
||||||
|
onOpenDevice: () -> Unit,
|
||||||
|
onOpenLocation: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Text(text = item.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = item.status,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
AirMqButton(
|
||||||
|
text = "Open",
|
||||||
|
onClick = onOpenDevice,
|
||||||
|
style = AirMqButtonStyle.Contained,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
AirMqButton(
|
||||||
|
text = if (item.hasLocation) "Show on map" else "Set location",
|
||||||
|
onClick = onOpenLocation,
|
||||||
|
style = AirMqButtonStyle.Outlined,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ManageScreenAnonymousPreview() {
|
||||||
|
AirMQTheme {
|
||||||
|
ManageScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
userMode = UserMode.ANONYMOUS
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
onOpenWidgetConstructor = {},
|
||||||
|
onBackToDashboard = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ManageScreenAuthorizedPreview() {
|
||||||
|
AirMQTheme {
|
||||||
|
ManageScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
userMode = UserMode.AUTHORIZED,
|
||||||
|
userName = "Anton Betsun",
|
||||||
|
userEmail = "messbees@gmail.com",
|
||||||
|
devicesLabel = "My devices",
|
||||||
|
devices = listOf(
|
||||||
|
DeviceItem("1", "AirMQ #1", "Online", true),
|
||||||
|
DeviceItem("2", "AirMQ #2", "Offline", false)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onEvent = {},
|
||||||
|
onOpenWidgetConstructor = {},
|
||||||
|
onBackToDashboard = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.db3.airmq.features.manage
|
||||||
|
|
||||||
|
object ManageScreenContract {
|
||||||
|
|
||||||
|
enum class UserMode {
|
||||||
|
ANONYMOUS,
|
||||||
|
AUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceItem(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val status: String,
|
||||||
|
val hasLocation: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val userMode: UserMode = UserMode.ANONYMOUS,
|
||||||
|
val userName: String = "Anonymous",
|
||||||
|
val userEmail: String = "Please sign in to access your devices.",
|
||||||
|
val devicesLabel: String = "Sign in to see your devices",
|
||||||
|
val devices: List<DeviceItem> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface Action {
|
||||||
|
data object OpenSettings : Action
|
||||||
|
data object OpenSetup : Action
|
||||||
|
data object OpenLogin : Action
|
||||||
|
data class OpenDevice(val deviceId: String) : Action
|
||||||
|
data class OpenLocation(val deviceId: String) : Action
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Event {
|
||||||
|
data object SettingsClicked : Event
|
||||||
|
data object SetupClicked : Event
|
||||||
|
data object SignInClicked : Event
|
||||||
|
data class DeviceClicked(val deviceId: String) : Event
|
||||||
|
data class DeviceLocationClicked(val deviceId: String) : Event
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package org.db3.airmq.features.manage
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.db3.airmq.features.manage.ManageScreenContract.Action
|
||||||
|
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.features.manage.ManageScreenContract.UserMode
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ManageViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
|
// Temporary migration stub: keep screen in anonymous mode.
|
||||||
|
private val forceAnonymous = true
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(initialState())
|
||||||
|
val uiState: StateFlow<State> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
|
||||||
|
val actions: SharedFlow<Action> = _actions.asSharedFlow()
|
||||||
|
|
||||||
|
fun onEvent(event: Event) {
|
||||||
|
when (event) {
|
||||||
|
Event.SettingsClicked -> _actions.tryEmit(Action.OpenSettings)
|
||||||
|
Event.SetupClicked -> _actions.tryEmit(Action.OpenSetup)
|
||||||
|
Event.SignInClicked -> _actions.tryEmit(Action.OpenLogin)
|
||||||
|
is Event.DeviceClicked -> _actions.tryEmit(Action.OpenDevice(event.deviceId))
|
||||||
|
is Event.DeviceLocationClicked -> _actions.tryEmit(Action.OpenLocation(event.deviceId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialState(): State {
|
||||||
|
val mode = if (forceAnonymous) {
|
||||||
|
UserMode.ANONYMOUS
|
||||||
|
} else {
|
||||||
|
UserMode.AUTHORIZED
|
||||||
|
}
|
||||||
|
return when (mode) {
|
||||||
|
UserMode.ANONYMOUS -> State(
|
||||||
|
userMode = UserMode.ANONYMOUS
|
||||||
|
)
|
||||||
|
UserMode.AUTHORIZED -> State(
|
||||||
|
userMode = UserMode.AUTHORIZED,
|
||||||
|
userName = "Anton Betsun",
|
||||||
|
userEmail = "messbees@gmail.com",
|
||||||
|
devicesLabel = "My devices",
|
||||||
|
devices = listOf(
|
||||||
|
DeviceItem(
|
||||||
|
id = "device-1",
|
||||||
|
name = "AirMQ #42",
|
||||||
|
status = "Online",
|
||||||
|
hasLocation = true
|
||||||
|
),
|
||||||
|
DeviceItem(
|
||||||
|
id = "device-2",
|
||||||
|
name = "AirMQ #17",
|
||||||
|
status = "Offline",
|
||||||
|
hasLocation = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,281 @@
|
|||||||
package org.db3.airmq.features.settings
|
package org.db3.airmq.features.settings
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import org.db3.airmq.features.common.MockScreenScaffold
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import org.db3.airmq.features.common.ScreenAction
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.Action
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.Event
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.State
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.UserMode
|
||||||
|
import org.db3.airmq.ui.theme.AirMQTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
onOpenDebug: () -> Unit,
|
onOpenDebug: () -> Unit,
|
||||||
onOpenCity: () -> Unit,
|
onOpenCity: () -> Unit,
|
||||||
onLogOutToManage: () -> Unit
|
onLogOutToManage: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
MockScreenScaffold(
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
title = "Settings",
|
val context = LocalContext.current
|
||||||
subtitle = "Settings and account actions.",
|
|
||||||
actions = listOf(
|
LaunchedEffect(viewModel) {
|
||||||
ScreenAction("Open Debug", onOpenDebug),
|
viewModel.actions.collectLatest { action ->
|
||||||
ScreenAction("Open City", onOpenCity),
|
when (action) {
|
||||||
ScreenAction("Log Out to Manage", onLogOutToManage)
|
Action.OpenDebug -> onOpenDebug()
|
||||||
)
|
Action.LogOutToManage -> onLogOutToManage()
|
||||||
|
is Action.ShowMessage -> Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsScreenContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onEvent = viewModel::onEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsScreenContent(
|
||||||
|
uiState: State,
|
||||||
|
onEvent: (Event) -> Unit
|
||||||
|
) {
|
||||||
|
val screenHorizontalPadding = 16.dp
|
||||||
|
val iconSize = 20.dp
|
||||||
|
val iconTextGap = 12.dp
|
||||||
|
val headerTextStart = iconSize + iconTextGap
|
||||||
|
|
||||||
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
return@Scaffold
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(horizontal = screenHorizontalPadding, vertical = 12.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Application",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = Color(0xFF607D8B),
|
||||||
|
modifier = Modifier.padding(start = headerTextStart, top = 4.dp, bottom = 4.dp)
|
||||||
|
)
|
||||||
|
PreferenceRow(
|
||||||
|
iconName = "ic_pref_city",
|
||||||
|
iconFallbackRes = android.R.drawable.ic_menu_myplaces,
|
||||||
|
title = "City in dashboard",
|
||||||
|
summary = uiState.city,
|
||||||
|
iconSize = iconSize,
|
||||||
|
iconTextGap = iconTextGap,
|
||||||
|
onClick = { onEvent(Event.CityClicked) }
|
||||||
|
)
|
||||||
|
PreferenceCheckRow(
|
||||||
|
iconName = "ic_pref_notifications",
|
||||||
|
iconFallbackRes = android.R.drawable.ic_dialog_info,
|
||||||
|
title = "Notify when device becomes offline",
|
||||||
|
checked = uiState.deviceStatusNotificationsEnabled,
|
||||||
|
iconSize = iconSize,
|
||||||
|
iconTextGap = iconTextGap,
|
||||||
|
onToggle = { onEvent(Event.DeviceStatusNotificationsChanged(it)) }
|
||||||
|
)
|
||||||
|
PreferenceCheckRow(
|
||||||
|
iconName = "ic_pref_offline_devices",
|
||||||
|
iconFallbackRes = android.R.drawable.ic_menu_mylocation,
|
||||||
|
title = "Show offline devices",
|
||||||
|
checked = uiState.offlineDevicesVisible,
|
||||||
|
iconSize = iconSize,
|
||||||
|
iconTextGap = iconTextGap,
|
||||||
|
onToggle = { onEvent(Event.OfflineDevicesVisibilityChanged(it)) }
|
||||||
|
)
|
||||||
|
PreferenceRow(
|
||||||
|
iconName = "ic_pref_info",
|
||||||
|
iconFallbackRes = android.R.drawable.ic_dialog_info,
|
||||||
|
title = "About",
|
||||||
|
iconSize = iconSize,
|
||||||
|
iconTextGap = iconTextGap,
|
||||||
|
onClick = { onEvent(Event.AboutClicked) }
|
||||||
|
)
|
||||||
|
if (uiState.userMode == UserMode.AUTHORIZED) {
|
||||||
|
PreferenceRow(
|
||||||
|
iconName = "ic_pref_debug",
|
||||||
|
iconFallbackRes = android.R.drawable.ic_lock_power_off,
|
||||||
|
title = "Log out",
|
||||||
|
iconSize = iconSize,
|
||||||
|
iconTextGap = iconTextGap,
|
||||||
|
onClick = { onEvent(Event.LogOutClicked) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreferenceRow(
|
||||||
|
iconName: String,
|
||||||
|
iconFallbackRes: Int,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
iconSize: androidx.compose.ui.unit.Dp,
|
||||||
|
iconTextGap: androidx.compose.ui.unit.Dp,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val iconRes = remember(iconName, iconFallbackRes) {
|
||||||
|
context.resources.getIdentifier(iconName, "drawable", context.packageName)
|
||||||
|
.takeIf { it != 0 } ?: iconFallbackRes
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 44.dp)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(iconTextGap)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
tint = Color(0x99333333)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(text = title, style = MaterialTheme.typography.bodyLarge, color = Color(0xFF222222))
|
||||||
|
if (summary != null) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color(0xFF6B6B6B)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreferenceCheckRow(
|
||||||
|
iconName: String,
|
||||||
|
iconFallbackRes: Int,
|
||||||
|
title: String,
|
||||||
|
checked: Boolean,
|
||||||
|
iconSize: androidx.compose.ui.unit.Dp,
|
||||||
|
iconTextGap: androidx.compose.ui.unit.Dp,
|
||||||
|
onToggle: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val iconRes = remember(iconName, iconFallbackRes) {
|
||||||
|
context.resources.getIdentifier(iconName, "drawable", context.packageName)
|
||||||
|
.takeIf { it != 0 } ?: iconFallbackRes
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 44.dp)
|
||||||
|
.clickable { onToggle(!checked) }
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = iconRes),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
tint = Color(0x99333333)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(iconTextGap))
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = Color(0xFF222222),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onToggle,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = Color(0xFF2F6FA8),
|
||||||
|
uncheckedColor = Color(0xFF8C8C8C),
|
||||||
|
checkmarkColor = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Settings Anonymous", showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun SettingsScreenAnonymousPreview() {
|
||||||
|
AirMQTheme {
|
||||||
|
SettingsScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
userMode = UserMode.ANONYMOUS,
|
||||||
|
city = "Minsk",
|
||||||
|
deviceStatusNotificationsEnabled = true,
|
||||||
|
offlineDevicesVisible = false,
|
||||||
|
advancedEnabled = false,
|
||||||
|
isLoading = false
|
||||||
|
),
|
||||||
|
onEvent = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Settings Advanced", showBackground = true, showSystemUi = true)
|
||||||
|
@Composable
|
||||||
|
private fun SettingsScreenAdvancedPreview() {
|
||||||
|
AirMQTheme {
|
||||||
|
SettingsScreenContent(
|
||||||
|
uiState = State(
|
||||||
|
userMode = UserMode.ANONYMOUS,
|
||||||
|
city = "Minsk",
|
||||||
|
deviceStatusNotificationsEnabled = true,
|
||||||
|
offlineDevicesVisible = true,
|
||||||
|
advancedEnabled = true,
|
||||||
|
isLoading = false
|
||||||
|
),
|
||||||
|
onEvent = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.db3.airmq.features.settings
|
||||||
|
|
||||||
|
object SettingsScreenContract {
|
||||||
|
|
||||||
|
enum class UserMode {
|
||||||
|
ANONYMOUS,
|
||||||
|
AUTHORIZED
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val userMode: UserMode = UserMode.ANONYMOUS,
|
||||||
|
val city: String = "Minsk",
|
||||||
|
val deviceStatusNotificationsEnabled: Boolean = true,
|
||||||
|
val offlineDevicesVisible: Boolean = false,
|
||||||
|
val advancedEnabled: Boolean = false,
|
||||||
|
val isLoading: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface Action {
|
||||||
|
data object OpenDebug : Action
|
||||||
|
data object LogOutToManage : Action
|
||||||
|
data class ShowMessage(val message: String) : Action
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Event {
|
||||||
|
data object CityClicked : Event
|
||||||
|
data object AboutClicked : Event
|
||||||
|
data object DebugClicked : Event
|
||||||
|
data object LogOutClicked : Event
|
||||||
|
data class DeviceStatusNotificationsChanged(val enabled: Boolean) : Event
|
||||||
|
data class OfflineDevicesVisibilityChanged(val enabled: Boolean) : Event
|
||||||
|
data class AdvancedChanged(val enabled: Boolean) : Event
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package org.db3.airmq.features.settings
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.Action
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.Event
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.State
|
||||||
|
import org.db3.airmq.features.settings.SettingsScreenContract.UserMode
|
||||||
|
import org.db3.airmq.sdk.settings.SettingsService
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SettingsViewModel @Inject constructor(
|
||||||
|
private val settingsService: SettingsService
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
// Temporary migration stub: keep screen in anonymous mode.
|
||||||
|
private val forceAnonymous = true
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(State())
|
||||||
|
val uiState: StateFlow<State> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
|
||||||
|
val actions: SharedFlow<Action> = _actions.asSharedFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEvent(event: Event) {
|
||||||
|
when (event) {
|
||||||
|
Event.CityClicked -> _actions.tryEmit(Action.ShowMessage("Coming Soon"))
|
||||||
|
Event.AboutClicked -> _actions.tryEmit(Action.ShowMessage("Coming Soon"))
|
||||||
|
Event.DebugClicked -> _actions.tryEmit(Action.OpenDebug)
|
||||||
|
Event.LogOutClicked -> _actions.tryEmit(Action.LogOutToManage)
|
||||||
|
is Event.DeviceStatusNotificationsChanged -> {
|
||||||
|
updateToggle(
|
||||||
|
toggle = { settingsService.setDeviceStatusNotificationsEnabled(event.enabled) },
|
||||||
|
updateState = { copy(deviceStatusNotificationsEnabled = event.enabled) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Event.OfflineDevicesVisibilityChanged -> {
|
||||||
|
updateToggle(
|
||||||
|
toggle = { settingsService.setOfflineDevicesVisible(event.enabled) },
|
||||||
|
updateState = { copy(offlineDevicesVisible = event.enabled) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is Event.AdvancedChanged -> {
|
||||||
|
updateToggle(
|
||||||
|
toggle = { settingsService.setAdvancedEnabled(event.enabled) },
|
||||||
|
updateState = { copy(advancedEnabled = event.enabled) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val city = settingsService.getCity() ?: "Minsk"
|
||||||
|
val deviceStatus = settingsService.getDeviceStatusNotificationsEnabled()
|
||||||
|
val offlineVisible = settingsService.getOfflineDevicesVisible()
|
||||||
|
val advanced = settingsService.getAdvancedEnabled()
|
||||||
|
_uiState.value = _uiState.value.copy(
|
||||||
|
userMode = if (forceAnonymous) UserMode.ANONYMOUS else UserMode.AUTHORIZED,
|
||||||
|
city = city,
|
||||||
|
deviceStatusNotificationsEnabled = deviceStatus,
|
||||||
|
offlineDevicesVisible = offlineVisible,
|
||||||
|
advancedEnabled = advanced,
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateToggle(
|
||||||
|
toggle: suspend () -> Result<Unit>,
|
||||||
|
updateState: State.() -> State
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val previous = _uiState.value
|
||||||
|
_uiState.value = previous.updateState()
|
||||||
|
val result = toggle()
|
||||||
|
if (result.isFailure) {
|
||||||
|
_uiState.value = previous
|
||||||
|
_actions.tryEmit(
|
||||||
|
Action.ShowMessage(result.exceptionOrNull()?.message ?: "Failed to save settings")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/res/drawable/ic_pref_advanced.xml
Normal file
11
app/src/main/res/drawable/ic_pref_advanced.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="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12.09,2.91C10.08,0.9 7.07,0.49 4.65,1.67L8.28,5.3c0.39,0.39 0.39,1.02 0,1.41L6.69,8.3c-0.39,0.4 -1.02,0.4 -1.41,0L1.65,4.67C0.48,7.1 0.89,10.09 2.9,12.1c1.86,1.86 4.58,2.35 6.89,1.48l7.96,7.96c1.03,1.03 2.69,1.03 3.71,0 1.03,-1.03 1.03,-2.69 0,-3.71L13.54,9.9c0.92,-2.34 0.44,-5.1 -1.45,-6.99z"/>
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_pref_city.xml
Normal file
11
app/src/main/res/drawable/ic_pref_city.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15,11L15,5.83c0,-0.53 -0.21,-1.04 -0.59,-1.41L12.7,2.71c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.7,1.7C9.21,4.79 9,5.3 9,5.83L9,7L5,7c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2v-6c0,-1.1 -0.9,-2 -2,-2h-4zM7,19L5,19v-2h2v2zM7,15L5,15v-2h2v2zM7,11L5,11L5,9h2v2zM13,19h-2v-2h2v2zM13,15h-2v-2h2v2zM13,11h-2L11,9h2v2zM13,7h-2L11,5h2v2zM19,19h-2v-2h2v2zM19,15h-2v-2h2v2z" />
|
||||||
|
</vector>
|
||||||
13
app/src/main/res/drawable/ic_pref_debug.xml
Normal file
13
app/src/main/res/drawable/ic_pref_debug.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,8h-1.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96l0.93,-0.93c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.02,-0.39 -1.41,0l-1.47,1.47C12.96,5.06 12.49,5 12,5s-0.96,0.06 -1.41,0.17L9.11,3.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41l0.92,0.93C7.88,6.55 7.26,7.22 6.81,8L5,8c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L5,12c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1v1c0,0.34 0.04,0.67 0.09,1L5,16c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h1.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L19,18c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-1.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h1c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-1v-1c0,-0.34 -0.04,-0.67 -0.09,-1L19,10c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1zM13,16h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1zM13,12h-2c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h2c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"
|
||||||
|
tools:ignore="VectorPath" />
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_pref_info.xml
Normal file
11
app/src/main/res/drawable/ic_pref_info.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="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,17c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v4c0,0.55 -0.45,1 -1,1zM13,9h-2L11,7h2v2z"/>
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_pref_notifications.xml
Normal file
11
app/src/main/res/drawable/ic_pref_notifications.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-1.29,1.29c-0.63,0.63 -0.19,1.71 0.7,1.71h13.17c0.89,0 1.34,-1.08 0.71,-1.71L18,16z" />
|
||||||
|
</vector>
|
||||||
14
app/src/main/res/drawable/ic_pref_offline_devices.xml
Normal file
14
app/src/main/res/drawable/ic_pref_offline_devices.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#333333"
|
||||||
|
android:alpha="0.6">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M14,10V3.26C13.35,3.09 12.68,3 12,3c-4.2,0 -8,3.22 -8,8.2c0,3.18 2.45,6.92 7.34,11.23c0.38,0.33 0.95,0.33 1.33,0C17.55,18.12 20,14.38 20,11.2c0,-0.41 -0.04,-0.81 -0.09,-1.2H14zM12,13c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C14,12.1 13.1,13 12,13z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M22.54,2.88l-1.42,-1.42l-2.12,2.13l-2.12,-2.13l-1.42,1.42l2.13,2.12l-2.13,2.12l1.42,1.42l2.12,-2.13l2.12,2.13l1.42,-1.42l-2.13,-2.12z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,25 +1,55 @@
|
|||||||
package org.db3.airmq.sdk.settings
|
package org.db3.airmq.sdk.settings
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SettingsServiceImpl @Inject constructor() : SettingsService {
|
class SettingsServiceImpl @Inject constructor(
|
||||||
override suspend fun getCity(): String? = throw NotImplementedError("getCity is not implemented yet")
|
@ApplicationContext context: Context
|
||||||
|
) : SettingsService {
|
||||||
|
private val sharedPreferences =
|
||||||
|
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
override suspend fun setCity(city: String?): Result<Unit> = throw NotImplementedError("setCity is not implemented yet")
|
override suspend fun getCity(): String? = sharedPreferences.getString(KEY_CITY, DEFAULT_CITY)
|
||||||
|
|
||||||
override suspend fun getOfflineDevicesVisible(): Boolean = false
|
override suspend fun setCity(city: String?): Result<Unit> = runCatching {
|
||||||
|
sharedPreferences.edit().putString(KEY_CITY, city ?: DEFAULT_CITY).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getOfflineDevicesVisible(): Boolean =
|
||||||
|
sharedPreferences.getBoolean(KEY_OFFLINE_DEVICES, DEFAULT_OFFLINE_DEVICES)
|
||||||
|
|
||||||
override suspend fun setOfflineDevicesVisible(visible: Boolean): Result<Unit> =
|
override suspend fun setOfflineDevicesVisible(visible: Boolean): Result<Unit> =
|
||||||
throw NotImplementedError("setOfflineDevicesVisible is not implemented yet")
|
runCatching {
|
||||||
|
sharedPreferences.edit().putBoolean(KEY_OFFLINE_DEVICES, visible).apply()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getAdvancedEnabled(): Boolean = throw NotImplementedError("getAdvancedEnabled is not implemented yet")
|
override suspend fun getAdvancedEnabled(): Boolean =
|
||||||
|
sharedPreferences.getBoolean(KEY_SHOW_ADVANCED, DEFAULT_SHOW_ADVANCED)
|
||||||
|
|
||||||
override suspend fun setAdvancedEnabled(enabled: Boolean): Result<Unit> =
|
override suspend fun setAdvancedEnabled(enabled: Boolean): Result<Unit> =
|
||||||
throw NotImplementedError("setAdvancedEnabled is not implemented yet")
|
runCatching {
|
||||||
|
sharedPreferences.edit().putBoolean(KEY_SHOW_ADVANCED, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getDeviceStatusNotificationsEnabled(): Boolean =
|
override suspend fun getDeviceStatusNotificationsEnabled(): Boolean =
|
||||||
throw NotImplementedError("getDeviceStatusNotificationsEnabled is not implemented yet")
|
sharedPreferences.getBoolean(KEY_DEVICE_STATUS_NOTIFICATIONS, DEFAULT_DEVICE_STATUS_NOTIFICATIONS)
|
||||||
|
|
||||||
override suspend fun setDeviceStatusNotificationsEnabled(enabled: Boolean): Result<Unit> =
|
override suspend fun setDeviceStatusNotificationsEnabled(enabled: Boolean): Result<Unit> =
|
||||||
throw NotImplementedError("setDeviceStatusNotificationsEnabled is not implemented yet")
|
runCatching {
|
||||||
|
sharedPreferences.edit().putBoolean(KEY_DEVICE_STATUS_NOTIFICATIONS, enabled).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val PREFERENCES_NAME = "airmq_settings"
|
||||||
|
private const val KEY_CITY = "dashboard_city"
|
||||||
|
private const val KEY_OFFLINE_DEVICES = "offline_devices"
|
||||||
|
private const val KEY_SHOW_ADVANCED = "show_advanced"
|
||||||
|
private const val KEY_DEVICE_STATUS_NOTIFICATIONS = "device_status"
|
||||||
|
|
||||||
|
private const val DEFAULT_CITY = "Minsk"
|
||||||
|
private const val DEFAULT_OFFLINE_DEVICES = false
|
||||||
|
private const val DEFAULT_SHOW_ADVANCED = false
|
||||||
|
private const val DEFAULT_DEVICE_STATUS_NOTIFICATIONS = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user