Extracted a few UI actions into reusable use cases

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:47 +00:00
1 parent 01139f76e7
commit 11841ca3d7
9 files changed
+343 -147

No files matched your search

@@ -17,6 +17,8 @@ import com.rtbishop.look4sat.data.repository.SettingsRepo
import com.rtbishop.look4sat.data.source.LocalSource import com.rtbishop.look4sat.data.source.LocalSource
import com.rtbishop.look4sat.data.source.RemoteSource import com.rtbishop.look4sat.data.source.RemoteSource
import com.rtbishop.look4sat.data.usecase.AddToCalendar import com.rtbishop.look4sat.data.usecase.AddToCalendar
import com.rtbishop.look4sat.data.usecase.OpenWebUrl
import com.rtbishop.look4sat.data.usecase.ShowToast
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISelectionRepo import com.rtbishop.look4sat.domain.repository.ISelectionRepo
@@ -25,6 +27,8 @@ import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.source.ILocalSource import com.rtbishop.look4sat.domain.source.ILocalSource
import com.rtbishop.look4sat.domain.source.IRemoteSource import com.rtbishop.look4sat.domain.source.IRemoteSource
import com.rtbishop.look4sat.domain.usecase.IAddToCalendar import com.rtbishop.look4sat.domain.usecase.IAddToCalendar
import com.rtbishop.look4sat.domain.usecase.IOpenWebUrl
import com.rtbishop.look4sat.domain.usecase.IShowToast
import com.rtbishop.look4sat.domain.utility.DataParser import com.rtbishop.look4sat.domain.utility.DataParser
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -36,17 +40,32 @@ import org.osmdroid.config.Configuration
class MainContainer(private val context: Context) { class MainContainer(private val context: Context) {
private val localSource = provideLocalSource() private val localSource = provideLocalSource()
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") } private val mainHandler = CoroutineExceptionHandler { _, error ->
println("MainHandler: $error")
}
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler) val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
val settingsRepo = provideSettingsRepo() val settingsRepo = provideSettingsRepo()
val selectionRepo = provideSelectionRepo() val selectionRepo = provideSelectionRepo()
val satelliteRepo = provideSatelliteRepo() val satelliteRepo = provideSatelliteRepo()
val databaseRepo = provideDatabaseRepo() val databaseRepo = provideDatabaseRepo()
fun provideAppVersionName(): String {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
return packageInfo.versionName ?: "4.0.0"
}
fun provideAddToCalendar(): IAddToCalendar { fun provideAddToCalendar(): IAddToCalendar {
return AddToCalendar(context) return AddToCalendar(context)
} }
fun provideOpenWebUrl(): IOpenWebUrl {
return OpenWebUrl(context)
}
fun provideShowToast(): IShowToast {
return ShowToast(context)
}
fun provideBluetoothReporter(): BluetoothReporter { fun provideBluetoothReporter(): BluetoothReporter {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO)) return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO))
@@ -63,15 +82,15 @@ class MainContainer(private val context: Context) {
} }
private fun provideDatabaseRepo(): IDatabaseRepo { private fun provideDatabaseRepo(): IDatabaseRepo {
val dataParser = DataParser(Dispatchers.Default) val dbDispatcher = Dispatchers.Default
val dataParser = DataParser(dbDispatcher)
val remoteSource = provideRemoteSource() val remoteSource = provideRemoteSource()
return DatabaseRepo(Dispatchers.Default, dataParser, localSource, remoteSource, settingsRepo) return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo)
} }
private fun provideLocalSource(): ILocalSource { private fun provideLocalSource(): ILocalSource {
val database = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv313").apply { val builder = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv313")
fallbackToDestructiveMigration() val database = builder.apply { fallbackToDestructiveMigration() }.build()
}.build()
return LocalSource(database.look4SatDao()) return LocalSource(database.look4SatDao())
} }
@@ -3,7 +3,6 @@ package com.rtbishop.look4sat.presentation.components
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -19,7 +18,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
@@ -219,22 +217,6 @@ fun CardIcon(onClick: () -> Unit, iconId: Int, modifier: Modifier = Modifier) {
} }
} }
@Composable
fun StatusIcon(iconResId: Int, isEnabled: Boolean = false, description: String? = null, onClick: (() -> Unit)? = null) {
val cardColors = if (isEnabled) {
CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary)
} else {
CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
}
val clickableModifier = if (onClick != null) Modifier.clickable { onClick.invoke() } else Modifier
val iconTint = if (isEnabled) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.onSurface
ElevatedCard(modifier = Modifier.size(48.dp), colors = cardColors) {
Box(modifier = clickableModifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(imageVector = ImageVector.vectorResource(iconResId), tint = iconTint, contentDescription = description)
}
}
}
@Composable @Composable
fun CardLoadingIndicator() { fun CardLoadingIndicator() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
@@ -242,10 +224,6 @@ fun CardLoadingIndicator() {
} }
} }
fun showToast(context: Context, message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
fun gotoUrl(context: Context, url: String) { fun gotoUrl(context: Context, url: String) {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} }
@@ -1,6 +1,7 @@
package com.rtbishop.look4sat.presentation.settings package com.rtbishop.look4sat.presentation.settings
import android.Manifest import android.Manifest
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -25,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -42,60 +42,62 @@ import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardButton import com.rtbishop.look4sat.presentation.components.CardButton
import com.rtbishop.look4sat.presentation.components.gotoUrl
import com.rtbishop.look4sat.presentation.components.showToast
import org.osmdroid.library.BuildConfig import org.osmdroid.library.BuildConfig
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
private const val GITHUB_URL = "https://github.com/rt-bishop/Look4Sat/"
private const val DONATE_URL = "https://ko-fi.com/rt_bishop"
private const val FDROID_URL = "https://f-droid.org/en/packages/com.rtbishop.look4sat/"
fun NavGraphBuilder.settingsDestination() { fun NavGraphBuilder.settingsDestination() {
composable(Screen.Settings.route) { SettingsScreen() } composable(Screen.Settings.route) {
val viewModel = viewModel(
modelClass = SettingsViewModel::class.java,
factory = SettingsViewModel.Factory
)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
SettingsScreen(uiState)
}
} }
@Composable @Composable
private fun SettingsScreen() { private fun SettingsScreen(uiState: SettingsState) {
val viewModel = viewModel(SettingsViewModel::class.java, factory = SettingsViewModel.Factory)
val context = LocalContext.current
// Permissions setup // Permissions setup
// val bluetoothContract = ActivityResultContracts.RequestPermission() val bluetoothContract = ActivityResultContracts.RequestPermission()
// val bluetoothError = stringResource(R.string.BTremote_perm_error) val bluetoothError = stringResource(R.string.BTremote_perm_error)
// val bluetoothPerm = when { val bluetoothPerm = when {
// Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
// else -> Manifest.permission.BLUETOOTH_CONNECT else -> Manifest.permission.BLUETOOTH_CONNECT
// } }
// val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted -> val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
// viewModel.setBTEnabled(isGranted) uiState.sendRCAction(RCAction.SetBluetoothState(isGranted))
// if (!isGranted) showToast(context, bluetoothError) if (!isGranted) uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError))
// } }
val locationContract = ActivityResultContracts.RequestMultiplePermissions() val locationContract = ActivityResultContracts.RequestMultiplePermissions()
val locationError = stringResource(R.string.location_gps_error) val locationError = stringResource(R.string.location_gps_error)
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION
val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions -> val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions ->
when { when {
permissions[locationPermFine] == true -> viewModel.setGpsPosition() permissions[locationPermFine] == true -> uiState.sendAction(SettingsAction.SetGpsPosition)
permissions[locationPermCoarse] == true -> viewModel.setGpsPosition() permissions[locationPermCoarse] == true -> uiState.sendAction(SettingsAction.SetGpsPosition)
else -> showToast(context, locationError) else -> uiState.sendSystemAction(SystemAction.ShowToast(locationError))
} }
} }
val contentContract = ActivityResultContracts.GetContent() val contentContract = ActivityResultContracts.GetContent()
val contentRequest = rememberLauncherForActivityResult(contentContract) { uri -> val contentRequest = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { viewModel.updateFromFile(uri.toString()) } uri?.let { uiState.sendAction(SettingsAction.UpdateFromFile(uri.toString())) }
} }
// Position settings // Position settings
val positionSettings = viewModel.positionSettings.collectAsStateWithLifecycle().value val positionSettings = uiState.positionSettings
val stationPos = positionSettings.stationPos val stationPos = positionSettings.stationPos
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) } val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
val setGeoPos = { lat: Double, lon: Double -> viewModel.setGeoPosition(lat, lon) } val setGeoPos = { lat: Double, lon: Double ->
val setQthPos = { locator: String -> viewModel.setQthPosition(locator) } uiState.sendAction(SettingsAction.SetGeoPosition(lat, lon))
val dismissPos = { viewModel.dismissPosMessage() } }
val setQthPos = { locator: String ->
uiState.sendAction(SettingsAction.SetQthPosition(locator))
}
val dismissPos = { uiState.sendAction(SettingsAction.DismissPosMessages) }
val posDialogState = rememberSaveable { mutableStateOf(false) } val posDialogState = rememberSaveable { mutableStateOf(false) }
val showPosDialog = { posDialogState.value = posDialogState.value.not() } val showPosDialog = { posDialogState.value = posDialogState.value.not() }
if (posDialogState.value) { if (posDialogState.value) {
@@ -108,26 +110,48 @@ private fun SettingsScreen() {
} }
// Data settings // Data settings
val dataSettings = viewModel.dataSettings.collectAsStateWithLifecycle().value val dataSettings = uiState.dataSettings
val updateFromWeb: () -> Unit = { viewModel.updateFromWeb() } val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) }
val updateFromFile = { contentRequest.launch("*/*") } val updateFromFile = { contentRequest.launch("*/*") }
val clearAllData: () -> Unit = { viewModel.clearAllData() } val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) }
// Other settings // Other settings
val otherSettings = viewModel.otherSettings.collectAsStateWithLifecycle().value val otherSettings = uiState.otherSettings
val toggleUtc = { value: Boolean -> viewModel.toggleUtc(value) } val toggleUtc = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleUtc(value)) }
val toggleUpdate = { value: Boolean -> viewModel.toggleUpdate(value) } val toggleUpdate = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleUpdate(value)) }
val toggleSweep = { value: Boolean -> viewModel.toggleSweep(value) } val toggleSweep = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSweep(value)) }
val toggleSensor = { value: Boolean -> viewModel.toggleSensor(value) } val toggleSensor = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSensor(value)) }
val toggleLightTheme = { value: Boolean -> viewModel.toggleLightTheme(value)} val toggleLightTheme = { value: Boolean ->
uiState.sendAction(SettingsAction.ToggleLightTheme(value))
}
// Screen setup // Screen setup
val versionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.0" LazyColumn(
LazyColumn(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { modifier = Modifier.padding(6.dp),
item { CardAbout(versionName) } verticalArrangement = Arrangement.spacedBy(6.dp)
item { LocationCard(positionSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos) } ) {
item { CardAbout(uiState.appVersionName, uiState.sendSystemAction) }
item {
LocationCard(
positionSettings,
setGpsPos,
showPosDialog,
showLocDialog,
dismissPos,
uiState.sendSystemAction
)
}
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) } item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) }
item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor,toggleLightTheme) } item {
OtherCard(
otherSettings,
toggleUtc,
toggleUpdate,
toggleSweep,
toggleSensor,
toggleLightTheme
)
}
} }
} }
@@ -136,9 +160,8 @@ private fun SettingsScreen() {
private fun CardAboutPreview() = MainTheme { CardAbout(version = "4.0.0") } private fun CardAboutPreview() = MainTheme { CardAbout(version = "4.0.0") }
@Composable @Composable
private fun CardAbout(version: String, modifier: Modifier = Modifier) { private fun CardAbout(version: String, sendUrlAction: (SystemAction) -> Unit = {}) {
val context = LocalContext.current ElevatedCard(modifier = Modifier.fillMaxWidth()) {
ElevatedCard(modifier = modifier.fillMaxWidth()) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
@@ -167,23 +190,23 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) {
Text( Text(
text = stringResource(id = R.string.app_subtitle), text = stringResource(id = R.string.app_subtitle),
fontSize = 20.sp, fontSize = 20.sp,
modifier = modifier.padding(top = 4.dp, bottom = 2.dp) modifier = Modifier.padding(top = 4.dp, bottom = 2.dp)
) )
Row(horizontalArrangement = Arrangement.SpaceEvenly) { Row(horizontalArrangement = Arrangement.SpaceEvenly) {
CardButton( CardButton(
onClick = { gotoUrl(context, GITHUB_URL) }, onClick = { sendUrlAction(SystemAction.OpenGitHub) },
text = stringResource(id = R.string.btn_github), text = stringResource(id = R.string.btn_github),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
CardButton( CardButton(
onClick = { gotoUrl(context, DONATE_URL) }, onClick = { sendUrlAction(SystemAction.OpenDonate) },
text = stringResource(id = R.string.btn_donate), text = stringResource(id = R.string.btn_donate),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
CardButton( CardButton(
onClick = { gotoUrl(context, FDROID_URL) }, onClick = { sendUrlAction(SystemAction.OpenFDroid) },
text = stringResource(id = R.string.btn_fdroid), text = stringResource(id = R.string.btn_fdroid),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@@ -197,7 +220,7 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) {
private fun LocationCardPreview() = MainTheme { private fun LocationCardPreview() = MainTheme {
val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L) val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L)
val settings = PositionSettings(true, stationPos, 0) val settings = PositionSettings(true, stationPos, 0)
LocationCard(settings = settings, setGpsPos = {}, showPosDialog = {}, {}) {} LocationCard(settings = settings, setGpsPos = {}, showPosDialog = {}, {}, {}) {}
} }
@Composable @Composable
@@ -206,7 +229,8 @@ private fun LocationCard(
setGpsPos: () -> Unit, setGpsPos: () -> Unit,
showPosDialog: () -> Unit, showPosDialog: () -> Unit,
showLocDialog: () -> Unit, showLocDialog: () -> Unit,
dismissPosMessage: () -> Unit dismissPosMessage: () -> Unit,
sendSystemAction: (SystemAction) -> Unit
) { ) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
@@ -263,10 +287,9 @@ private fun LocationCard(
} }
} }
if (settings.messageResId != 0) { if (settings.messageResId != 0) {
val context = LocalContext.current
val errorString = stringResource(id = settings.messageResId) val errorString = stringResource(id = settings.messageResId)
LaunchedEffect(key1 = settings.messageResId) { LaunchedEffect(key1 = settings.messageResId) {
showToast(context, errorString) sendSystemAction(SystemAction.ShowToast(errorString))
dismissPosMessage() dismissPosMessage()
} }
} }
@@ -383,8 +406,9 @@ private fun OtherCardPreview() = MainTheme {
stateOfSensors = true, stateOfSensors = true,
stateOfSweep = true, stateOfSweep = true,
stateOfUtc = false, stateOfUtc = false,
stateOfLightTheme = false) stateOfLightTheme = false
OtherCard(settings = values, {}, {}, {}, {},{}) )
OtherCard(settings = values, {}, {}, {}, {}, {})
} }
@Composable @Composable
@@ -437,7 +461,10 @@ private fun OtherCard(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text(text = stringResource(id = R.string.other_switch_light_theme)) Text(text = stringResource(id = R.string.other_switch_light_theme))
Switch(checked = settings.stateOfLightTheme, onCheckedChange = { toggleLightTheme(it) }) Switch(
checked = settings.stateOfLightTheme,
onCheckedChange = { toggleLightTheme(it) }
)
} }
} }
} }
@@ -1,7 +1,65 @@
package com.rtbishop.look4sat.presentation.settings package com.rtbishop.look4sat.presentation.settings
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
data class PositionSettings(val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int) data class PositionSettings(
val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int
)
data class DataSettings(val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long) data class DataSettings(
val isUpdating: Boolean, val entriesTotal: Int, val radiosTotal: Int, val timestamp: Long
)
data class RCSettings(
val rotatorState: Boolean,
val rotatorAddress: String,
val rotatorPort: String,
val bluetoothState: Boolean,
val bluetoothFormat: String,
val bluetoothName: String,
val bluetoothAddress: String,
)
data class SettingsState(
val appVersionName: String,
val positionSettings: PositionSettings,
val dataSettings: DataSettings,
val otherSettings: OtherSettings,
val rcSettings: RCSettings,
val sendAction: (SettingsAction) -> Unit,
val sendRCAction: (RCAction) -> Unit,
val sendSystemAction: (SystemAction) -> Unit
)
sealed class SettingsAction {
data object SetGpsPosition : SettingsAction()
data class SetGeoPosition(val latitude: Double, val longitude: Double) : SettingsAction()
data class SetQthPosition(val locator: String) : SettingsAction()
data object DismissPosMessages : SettingsAction()
data object UpdateFromWeb : SettingsAction()
data class UpdateFromFile(val uri: String) : SettingsAction()
data object ClearAllData : SettingsAction()
data class ToggleUtc(val value: Boolean) : SettingsAction()
data class ToggleUpdate(val value: Boolean) : SettingsAction()
data class ToggleSweep(val value: Boolean) : SettingsAction()
data class ToggleSensor(val value: Boolean) : SettingsAction()
data class ToggleLightTheme(val value: Boolean) : SettingsAction()
}
sealed class SystemAction {
data object OpenGitHub : SystemAction()
data object OpenDonate : SystemAction()
data object OpenFDroid : SystemAction()
data class ShowToast(val message: String) : SystemAction()
}
sealed class RCAction {
data class SetRotatorState(val value: Boolean) : RCAction()
data class SetRotatorAddress(val value: String) : RCAction()
data class SetRotatorPort(val value: String) : RCAction()
data class SetBluetoothState(val value: Boolean) : RCAction()
data class SetBluetoothFormat(val value: String) : RCAction()
data class SetBluetoothName(val value: String) : RCAction()
data class SetBluetoothAddress(val value: String) : RCAction()
}
@@ -24,134 +24,209 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.usecase.IOpenWebUrl
import com.rtbishop.look4sat.domain.usecase.IShowToast
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SettingsViewModel( class SettingsViewModel(
private val databaseRepo: IDatabaseRepo, private val settingsRepo: ISettingsRepo private val databaseRepo: IDatabaseRepo,
private val settingsRepo: ISettingsRepo,
private val openWebUrl: IOpenWebUrl,
private val showToast: IShowToast,
appVersionName: String
) : ViewModel() { ) : ViewModel() {
private val githubUrl = "https://github.com/rt-bishop/Look4Sat/"
private val donateUrl = "https://ko-fi.com/rt_bishop"
private val fdroidUrl = "https://f-droid.org/en/packages/com.rtbishop.look4sat/"
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0) private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
private val _positionSettings = MutableStateFlow(defaultPosSettings)
val positionSettings: StateFlow<PositionSettings> = _positionSettings
private val defaultDataSettings = DataSettings(false, 0, 0, 0L) private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val _dataSettings = MutableStateFlow(defaultDataSettings) private val defaultRCSettings = RCSettings(
val dataSettings: StateFlow<DataSettings> = _dataSettings rotatorState = settingsRepo.getRotatorState(),
rotatorAddress = settingsRepo.getRotatorAddress(),
private val _otherSettings = MutableStateFlow(settingsRepo.otherSettings.value) rotatorPort = settingsRepo.getRotatorPort(),
val otherSettings: StateFlow<OtherSettings> = _otherSettings bluetoothState = settingsRepo.getBluetoothState(),
bluetoothFormat = settingsRepo.getBluetoothFormat(),
bluetoothName = settingsRepo.getBluetoothName(),
bluetoothAddress = settingsRepo.getBluetoothAddress()
)
private val _uiState = MutableStateFlow(
SettingsState(
appVersionName = appVersionName,
positionSettings = defaultPosSettings,
dataSettings = defaultDataSettings,
otherSettings = settingsRepo.otherSettings.value,
rcSettings = defaultRCSettings,
sendAction = ::handleAction,
sendRCAction = ::handleAction,
sendSystemAction = ::handleAction
)
)
val uiState: StateFlow<SettingsState> = _uiState
init { init {
viewModelScope.launch { viewModelScope.launch {
settingsRepo.stationPosition.collect { geoPos -> settingsRepo.stationPosition.collect { geoPos ->
_positionSettings.update { it.copy(isUpdating = false, stationPos = geoPos) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, stationPos = geoPos
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} }
} }
viewModelScope.launch { viewModelScope.launch {
settingsRepo.databaseState.collect { state -> settingsRepo.databaseState.collect { state ->
_dataSettings.update { val newDataSettings = _uiState.value.dataSettings.copy(
it.copy( isUpdating = false,
isUpdating = false, entriesTotal = state.numberOfSatellites,
entriesTotal = state.numberOfSatellites, radiosTotal = state.numberOfRadios,
radiosTotal = state.numberOfRadios, timestamp = state.updateTimestamp
timestamp = state.updateTimestamp )
) _uiState.update { it.copy(dataSettings = newDataSettings) }
}
} }
} }
viewModelScope.launch { viewModelScope.launch {
settingsRepo.otherSettings.collect { settings -> _otherSettings.update { settings } } settingsRepo.otherSettings.collect { settings ->
_uiState.update { it.copy(otherSettings = settings) }
}
} }
} }
fun setGpsPosition() { private fun handleAction(action: SettingsAction) {
when (action) {
SettingsAction.SetGpsPosition -> setGpsPosition()
is SettingsAction.SetGeoPosition -> setGeoPosition(action.latitude, action.longitude)
is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
SettingsAction.DismissPosMessages -> dismissPosMessage()
SettingsAction.UpdateFromWeb -> updateFromWeb()
is SettingsAction.UpdateFromFile -> updateFromFile(action.uri)
SettingsAction.ClearAllData -> clearAllData()
is SettingsAction.ToggleUtc -> settingsRepo.setStateOfUtc(action.value)
is SettingsAction.ToggleUpdate -> settingsRepo.setStateOfAutoUpdate(action.value)
is SettingsAction.ToggleSweep -> settingsRepo.setStateOfSweep(action.value)
is SettingsAction.ToggleSensor -> settingsRepo.setStateOfSensors(action.value)
is SettingsAction.ToggleLightTheme -> settingsRepo.setStateOfLightTheme(action.value)
}
}
private fun handleAction(action: RCAction) {
when (action) {
is RCAction.SetRotatorState -> settingsRepo.setRotatorState(action.value)
is RCAction.SetRotatorAddress -> settingsRepo.setRotatorAddress(action.value)
is RCAction.SetRotatorPort -> settingsRepo.setRotatorPort(action.value)
is RCAction.SetBluetoothState -> settingsRepo.setBluetoothState(action.value)
is RCAction.SetBluetoothAddress -> settingsRepo.setBluetoothAddress(action.value)
is RCAction.SetBluetoothFormat -> settingsRepo.setBluetoothFormat(action.value)
is RCAction.SetBluetoothName -> settingsRepo.setBluetoothName(action.value)
}
}
private fun handleAction(action: SystemAction) {
when (action) {
SystemAction.OpenGitHub -> openWebUrl(githubUrl)
SystemAction.OpenDonate -> openWebUrl(donateUrl)
SystemAction.OpenFDroid -> openWebUrl(fdroidUrl)
is SystemAction.ShowToast -> showToast(action.message)
}
}
private fun setGpsPosition() {
if (settingsRepo.setStationPositionGps()) { if (settingsRepo.setStationPositionGps()) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
_positionSettings.update { it.copy(isUpdating = true, messageResId = messageResId) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = true, messageResId = messageResId
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} else { } else {
val errorResId = R.string.location_gps_error val errorResId = R.string.location_gps_error
_positionSettings.update { it.copy(messageResId = errorResId) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = errorResId
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} }
} }
fun setGeoPosition(latitude: Double, longitude: Double) { private fun setGeoPosition(latitude: Double, longitude: Double) {
if (settingsRepo.setStationPositionGeo(latitude, longitude, 0.0)) { if (settingsRepo.setStationPositionGeo(latitude, longitude, 0.0)) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
_positionSettings.update { it.copy(messageResId = messageResId) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = messageResId
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} else { } else {
val errorResId = R.string.location_manual_error val errorResId = R.string.location_manual_error
_positionSettings.update { it.copy(messageResId = errorResId) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = errorResId
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} }
} }
fun setQthPosition(locator: String) { private fun setQthPosition(locator: String) {
if (settingsRepo.setStationPositionQth(locator)) { if (settingsRepo.setStationPositionQth(locator)) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
_positionSettings.update { it.copy(messageResId = messageResId) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = messageResId
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} else { } else {
val errorResId = R.string.location_qth_error val errorResId = R.string.location_qth_error
_positionSettings.update { it.copy(messageResId = errorResId) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = errorResId
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} }
} }
fun dismissPosMessage() { private fun dismissPosMessage() {
_positionSettings.update { it.copy(messageResId = 0) } val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = 0
)
_uiState.update { it.copy(positionSettings = newPosSettings) }
} }
fun updateFromWeb() = viewModelScope.launch { private fun updateFromWeb() = viewModelScope.launch {
try { try {
_dataSettings.update { it.copy(isUpdating = true) } val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) }
databaseRepo.updateFromRemote() databaseRepo.updateFromRemote()
} catch (exception: Exception) { } catch (exception: Exception) {
_dataSettings.update { it.copy(isUpdating = false) } val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) }
println(exception) println(exception)
} }
} }
fun updateFromFile(uri: String) = viewModelScope.launch { private fun updateFromFile(uri: String) = viewModelScope.launch {
try { try {
_dataSettings.update { it.copy(isUpdating = true) } val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) }
databaseRepo.updateFromFile(uri) databaseRepo.updateFromFile(uri)
} catch (exception: Exception) { } catch (exception: Exception) {
_dataSettings.update { it.copy(isUpdating = false) } val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) }
println(exception) println(exception)
} }
} }
fun clearAllData() = viewModelScope.launch { databaseRepo.clearAllData() } private fun clearAllData() = viewModelScope.launch { databaseRepo.clearAllData() }
fun toggleUtc(value: Boolean) = settingsRepo.setStateOfUtc(value)
fun toggleUpdate(value: Boolean) = settingsRepo.setStateOfAutoUpdate(value)
fun toggleSweep(value: Boolean) = settingsRepo.setStateOfSweep(value)
fun toggleSensor(value: Boolean) = settingsRepo.setStateOfSensors(value)
fun toggleLightTheme(value: Boolean) = settingsRepo.setStateOfLightTheme(value)
// fun getRotatorEnabled(): Boolean = settings.getRotatorEnabled()
// fun setRotatorEnabled(value: Boolean) = settings.setRotatorEnabled(value)
// fun getRotatorServer(): String = settings.getRotatorServer()
// fun setRotatorServer(value: String) = settings.setRotatorServer(value)
// fun getRotatorPort(): String = settings.getRotatorPort()
// fun setRotatorPort(value: String) = settings.setRotatorPort(value)
// fun getBTEnabled(): Boolean = settings.getBTEnabled()
// fun setBTEnabled(value: Boolean) = settings.setBTEnabled(value)
// fun getBTFormat(): String = settings.getBTFormat()
// fun setBTFormat(value: String) = settings.setBTFormat(value)
// fun getBTDeviceName(): String = settings.getBTDeviceName()
// fun setBTDeviceName(value: String) = settings.setBTDeviceName(value)
// fun getBTDeviceAddr(): String = settings.getBTDeviceAddr()
// fun setBTDeviceAddr(value: String) = settings.setBTDeviceAddr(value)
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
SettingsViewModel(container.databaseRepo, container.settingsRepo) SettingsViewModel(
container.databaseRepo,
container.settingsRepo,
container.provideOpenWebUrl(),
container.provideShowToast(),
container.provideAppVersionName()
)
} }
} }
} }
@@ -0,0 +1,18 @@
package com.rtbishop.look4sat.data.usecase
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.rtbishop.look4sat.domain.usecase.IOpenWebUrl
class OpenWebUrl(private val context: Context) : IOpenWebUrl {
override fun invoke(url: String) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} catch (_: ActivityNotFoundException) {
}
}
}
@@ -0,0 +1,11 @@
package com.rtbishop.look4sat.data.usecase
import android.content.Context
import android.widget.Toast
import com.rtbishop.look4sat.domain.usecase.IShowToast
class ShowToast(private val context: Context) : IShowToast {
override fun invoke(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
@@ -0,0 +1,5 @@
package com.rtbishop.look4sat.domain.usecase
interface IOpenWebUrl {
operator fun invoke(url: String)
}
@@ -0,0 +1,5 @@
package com.rtbishop.look4sat.domain.usecase
interface IShowToast {
operator fun invoke(message: String)
}