diff --git a/app/src/main/java/com/rtbishop/look4sat/MainContainer.kt b/app/src/main/java/com/rtbishop/look4sat/MainContainer.kt index 23a972fc..f8454e4f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainContainer.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainContainer.kt @@ -17,6 +17,8 @@ import com.rtbishop.look4sat.data.repository.SettingsRepo import com.rtbishop.look4sat.data.source.LocalSource import com.rtbishop.look4sat.data.source.RemoteSource 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.ISatelliteRepo 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.IRemoteSource 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 kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -36,17 +40,32 @@ import org.osmdroid.config.Configuration class MainContainer(private val context: Context) { 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 settingsRepo = provideSettingsRepo() val selectionRepo = provideSelectionRepo() val satelliteRepo = provideSatelliteRepo() val databaseRepo = provideDatabaseRepo() + fun provideAppVersionName(): String { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + return packageInfo.versionName ?: "4.0.0" + } + fun provideAddToCalendar(): IAddToCalendar { return AddToCalendar(context) } + fun provideOpenWebUrl(): IOpenWebUrl { + return OpenWebUrl(context) + } + + fun provideShowToast(): IShowToast { + return ShowToast(context) + } + fun provideBluetoothReporter(): BluetoothReporter { val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager return BluetoothReporter(manager, CoroutineScope(Dispatchers.IO)) @@ -63,15 +82,15 @@ class MainContainer(private val context: Context) { } private fun provideDatabaseRepo(): IDatabaseRepo { - val dataParser = DataParser(Dispatchers.Default) + val dbDispatcher = Dispatchers.Default + val dataParser = DataParser(dbDispatcher) val remoteSource = provideRemoteSource() - return DatabaseRepo(Dispatchers.Default, dataParser, localSource, remoteSource, settingsRepo) + return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo) } private fun provideLocalSource(): ILocalSource { - val database = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv313").apply { - fallbackToDestructiveMigration() - }.build() + val builder = Room.databaseBuilder(context, Look4SatDb::class.java, "Look4SatDBv313") + val database = builder.apply { fallbackToDestructiveMigration() }.build() return LocalSource(database.look4SatDao()) } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt index 0f9fdacc..628938ae 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/components/Common.kt @@ -3,7 +3,6 @@ package com.rtbishop.look4sat.presentation.components import android.content.Context import android.content.Intent import android.net.Uri -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.width import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedButton 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 fun CardLoadingIndicator() { 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) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt index 50c5fcac..78002649 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsScreen.kt @@ -1,6 +1,7 @@ package com.rtbishop.look4sat.presentation.settings import android.Manifest +import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -25,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource 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.Screen 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 java.text.SimpleDateFormat import java.util.Date 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() { - 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 -private fun SettingsScreen() { - val viewModel = viewModel(SettingsViewModel::class.java, factory = SettingsViewModel.Factory) - val context = LocalContext.current - +private fun SettingsScreen(uiState: SettingsState) { // Permissions setup -// val bluetoothContract = ActivityResultContracts.RequestPermission() -// val bluetoothError = stringResource(R.string.BTremote_perm_error) -// val bluetoothPerm = when { -// Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH -// else -> Manifest.permission.BLUETOOTH_CONNECT -// } -// val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted -> -// viewModel.setBTEnabled(isGranted) -// if (!isGranted) showToast(context, bluetoothError) -// } + val bluetoothContract = ActivityResultContracts.RequestPermission() + val bluetoothError = stringResource(R.string.BTremote_perm_error) + val bluetoothPerm = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH + else -> Manifest.permission.BLUETOOTH_CONNECT + } + val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted -> + uiState.sendRCAction(RCAction.SetBluetoothState(isGranted)) + if (!isGranted) uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError)) + } val locationContract = ActivityResultContracts.RequestMultiplePermissions() val locationError = stringResource(R.string.location_gps_error) val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION val locationPermFine = Manifest.permission.ACCESS_FINE_LOCATION val locationRequest = rememberLauncherForActivityResult(locationContract) { permissions -> when { - permissions[locationPermFine] == true -> viewModel.setGpsPosition() - permissions[locationPermCoarse] == true -> viewModel.setGpsPosition() - else -> showToast(context, locationError) + permissions[locationPermFine] == true -> uiState.sendAction(SettingsAction.SetGpsPosition) + permissions[locationPermCoarse] == true -> uiState.sendAction(SettingsAction.SetGpsPosition) + else -> uiState.sendSystemAction(SystemAction.ShowToast(locationError)) } } val contentContract = ActivityResultContracts.GetContent() val contentRequest = rememberLauncherForActivityResult(contentContract) { uri -> - uri?.let { viewModel.updateFromFile(uri.toString()) } + uri?.let { uiState.sendAction(SettingsAction.UpdateFromFile(uri.toString())) } } // Position settings - val positionSettings = viewModel.positionSettings.collectAsStateWithLifecycle().value + val positionSettings = uiState.positionSettings val stationPos = positionSettings.stationPos val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) } - val setGeoPos = { lat: Double, lon: Double -> viewModel.setGeoPosition(lat, lon) } - val setQthPos = { locator: String -> viewModel.setQthPosition(locator) } - val dismissPos = { viewModel.dismissPosMessage() } + val setGeoPos = { lat: Double, lon: Double -> + uiState.sendAction(SettingsAction.SetGeoPosition(lat, lon)) + } + val setQthPos = { locator: String -> + uiState.sendAction(SettingsAction.SetQthPosition(locator)) + } + val dismissPos = { uiState.sendAction(SettingsAction.DismissPosMessages) } val posDialogState = rememberSaveable { mutableStateOf(false) } val showPosDialog = { posDialogState.value = posDialogState.value.not() } if (posDialogState.value) { @@ -108,26 +110,48 @@ private fun SettingsScreen() { } // Data settings - val dataSettings = viewModel.dataSettings.collectAsStateWithLifecycle().value - val updateFromWeb: () -> Unit = { viewModel.updateFromWeb() } + val dataSettings = uiState.dataSettings + val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) } val updateFromFile = { contentRequest.launch("*/*") } - val clearAllData: () -> Unit = { viewModel.clearAllData() } + val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) } // Other settings - val otherSettings = viewModel.otherSettings.collectAsStateWithLifecycle().value - val toggleUtc = { value: Boolean -> viewModel.toggleUtc(value) } - val toggleUpdate = { value: Boolean -> viewModel.toggleUpdate(value) } - val toggleSweep = { value: Boolean -> viewModel.toggleSweep(value) } - val toggleSensor = { value: Boolean -> viewModel.toggleSensor(value) } - val toggleLightTheme = { value: Boolean -> viewModel.toggleLightTheme(value)} + val otherSettings = uiState.otherSettings + val toggleUtc = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleUtc(value)) } + val toggleUpdate = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleUpdate(value)) } + val toggleSweep = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSweep(value)) } + val toggleSensor = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSensor(value)) } + val toggleLightTheme = { value: Boolean -> + uiState.sendAction(SettingsAction.ToggleLightTheme(value)) + } // Screen setup - val versionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.0" - LazyColumn(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { - item { CardAbout(versionName) } - item { LocationCard(positionSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos) } + LazyColumn( + modifier = Modifier.padding(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + item { CardAbout(uiState.appVersionName, uiState.sendSystemAction) } + item { + LocationCard( + positionSettings, + setGpsPos, + showPosDialog, + showLocDialog, + dismissPos, + uiState.sendSystemAction + ) + } 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") } @Composable -private fun CardAbout(version: String, modifier: Modifier = Modifier) { - val context = LocalContext.current - ElevatedCard(modifier = modifier.fillMaxWidth()) { +private fun CardAbout(version: String, sendUrlAction: (SystemAction) -> Unit = {}) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) @@ -167,23 +190,23 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) { Text( text = stringResource(id = R.string.app_subtitle), 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) { CardButton( - onClick = { gotoUrl(context, GITHUB_URL) }, + onClick = { sendUrlAction(SystemAction.OpenGitHub) }, text = stringResource(id = R.string.btn_github), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(6.dp)) CardButton( - onClick = { gotoUrl(context, DONATE_URL) }, + onClick = { sendUrlAction(SystemAction.OpenDonate) }, text = stringResource(id = R.string.btn_donate), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(6.dp)) CardButton( - onClick = { gotoUrl(context, FDROID_URL) }, + onClick = { sendUrlAction(SystemAction.OpenFDroid) }, text = stringResource(id = R.string.btn_fdroid), modifier = Modifier.weight(1f) ) @@ -197,7 +220,7 @@ private fun CardAbout(version: String, modifier: Modifier = Modifier) { private fun LocationCardPreview() = MainTheme { val stationPos = GeoPos(0.0, 0.0, 0.0, "IO91vl", 0L) val settings = PositionSettings(true, stationPos, 0) - LocationCard(settings = settings, setGpsPos = {}, showPosDialog = {}, {}) {} + LocationCard(settings = settings, setGpsPos = {}, showPosDialog = {}, {}, {}) {} } @Composable @@ -206,7 +229,8 @@ private fun LocationCard( setGpsPos: () -> Unit, showPosDialog: () -> Unit, showLocDialog: () -> Unit, - dismissPosMessage: () -> Unit + dismissPosMessage: () -> Unit, + sendSystemAction: (SystemAction) -> Unit ) { ElevatedCard(modifier = Modifier.fillMaxWidth()) { Column( @@ -263,10 +287,9 @@ private fun LocationCard( } } if (settings.messageResId != 0) { - val context = LocalContext.current val errorString = stringResource(id = settings.messageResId) LaunchedEffect(key1 = settings.messageResId) { - showToast(context, errorString) + sendSystemAction(SystemAction.ShowToast(errorString)) dismissPosMessage() } } @@ -383,8 +406,9 @@ private fun OtherCardPreview() = MainTheme { stateOfSensors = true, stateOfSweep = true, stateOfUtc = false, - stateOfLightTheme = false) - OtherCard(settings = values, {}, {}, {}, {},{}) + stateOfLightTheme = false + ) + OtherCard(settings = values, {}, {}, {}, {}, {}) } @Composable @@ -437,7 +461,10 @@ private fun OtherCard( modifier = Modifier.fillMaxWidth() ) { Text(text = stringResource(id = R.string.other_switch_light_theme)) - Switch(checked = settings.stateOfLightTheme, onCheckedChange = { toggleLightTheme(it) }) + Switch( + checked = settings.stateOfLightTheme, + onCheckedChange = { toggleLightTheme(it) } + ) } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt index 35906ff0..da107a41 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsState.kt @@ -1,7 +1,65 @@ package com.rtbishop.look4sat.presentation.settings +import com.rtbishop.look4sat.domain.model.OtherSettings 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() +} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt index fc8025e9..32461c69 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settings/SettingsViewModel.kt @@ -24,134 +24,209 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import com.rtbishop.look4sat.MainApplication 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.ISettingsRepo +import com.rtbishop.look4sat.domain.usecase.IOpenWebUrl +import com.rtbishop.look4sat.domain.usecase.IShowToast import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch 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() { + 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 _positionSettings = MutableStateFlow(defaultPosSettings) - val positionSettings: StateFlow = _positionSettings - private val defaultDataSettings = DataSettings(false, 0, 0, 0L) - private val _dataSettings = MutableStateFlow(defaultDataSettings) - val dataSettings: StateFlow = _dataSettings - - private val _otherSettings = MutableStateFlow(settingsRepo.otherSettings.value) - val otherSettings: StateFlow = _otherSettings + private val defaultRCSettings = RCSettings( + rotatorState = settingsRepo.getRotatorState(), + rotatorAddress = settingsRepo.getRotatorAddress(), + rotatorPort = settingsRepo.getRotatorPort(), + 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 = _uiState init { viewModelScope.launch { 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 { settingsRepo.databaseState.collect { state -> - _dataSettings.update { - it.copy( - isUpdating = false, - entriesTotal = state.numberOfSatellites, - radiosTotal = state.numberOfRadios, - timestamp = state.updateTimestamp - ) - } + val newDataSettings = _uiState.value.dataSettings.copy( + isUpdating = false, + entriesTotal = state.numberOfSatellites, + radiosTotal = state.numberOfRadios, + timestamp = state.updateTimestamp + ) + _uiState.update { it.copy(dataSettings = newDataSettings) } } } 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()) { 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 { 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)) { 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 { 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)) { 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 { 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() { - _positionSettings.update { it.copy(messageResId = 0) } + private fun dismissPosMessage() { + 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 { - _dataSettings.update { it.copy(isUpdating = true) } + val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true) + _uiState.update { it.copy(dataSettings = newDataSettings) } databaseRepo.updateFromRemote() } 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) } } - fun updateFromFile(uri: String) = viewModelScope.launch { + private fun updateFromFile(uri: String) = viewModelScope.launch { try { - _dataSettings.update { it.copy(isUpdating = true) } + val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true) + _uiState.update { it.copy(dataSettings = newDataSettings) } databaseRepo.updateFromFile(uri) } 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) } } - 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) + private fun clearAllData() = viewModelScope.launch { databaseRepo.clearAllData() } companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY initializer { val container = (this[applicationKey] as MainApplication).container - SettingsViewModel(container.databaseRepo, container.settingsRepo) + SettingsViewModel( + container.databaseRepo, + container.settingsRepo, + container.provideOpenWebUrl(), + container.provideShowToast(), + container.provideAppVersionName() + ) } } } diff --git a/data/src/main/java/com/rtbishop/look4sat/data/usecase/OpenWebUrl.kt b/data/src/main/java/com/rtbishop/look4sat/data/usecase/OpenWebUrl.kt new file mode 100644 index 00000000..eb235285 --- /dev/null +++ b/data/src/main/java/com/rtbishop/look4sat/data/usecase/OpenWebUrl.kt @@ -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) { + } + } +} diff --git a/data/src/main/java/com/rtbishop/look4sat/data/usecase/ShowToast.kt b/data/src/main/java/com/rtbishop/look4sat/data/usecase/ShowToast.kt new file mode 100644 index 00000000..ede568b4 --- /dev/null +++ b/data/src/main/java/com/rtbishop/look4sat/data/usecase/ShowToast.kt @@ -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() + } +} diff --git a/domain/src/main/java/com/rtbishop/look4sat/domain/usecase/IOpenWebUrl.kt b/domain/src/main/java/com/rtbishop/look4sat/domain/usecase/IOpenWebUrl.kt new file mode 100644 index 00000000..332dfa89 --- /dev/null +++ b/domain/src/main/java/com/rtbishop/look4sat/domain/usecase/IOpenWebUrl.kt @@ -0,0 +1,5 @@ +package com.rtbishop.look4sat.domain.usecase + +interface IOpenWebUrl { + operator fun invoke(url: String) +} diff --git a/domain/src/main/java/com/rtbishop/look4sat/domain/usecase/IShowToast.kt b/domain/src/main/java/com/rtbishop/look4sat/domain/usecase/IShowToast.kt new file mode 100644 index 00000000..a67e5426 --- /dev/null +++ b/domain/src/main/java/com/rtbishop/look4sat/domain/usecase/IShowToast.kt @@ -0,0 +1,5 @@ +package com.rtbishop.look4sat.domain.usecase + +interface IShowToast { + operator fun invoke(message: String) +}