Restructured SettingsScreen layout, tweaked components

This commit is contained in:
Arty Bishop committed 2025-12-27 15:02:15 +00:00
1 parent bfbe437ab1
commit a68e466040
16 files changed
+236 -214

No files matched your search

-1
View File
@@ -26,7 +26,6 @@ android {
buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release {
isMinifyEnabled = true
@@ -17,7 +17,6 @@ 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
@@ -27,7 +26,6 @@ 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
@@ -58,10 +56,6 @@ class MainContainer(private val context: Context) {
return AddToCalendar(context)
}
fun provideOpenWebUrl(): IOpenWebUrl {
return OpenWebUrl(context)
}
fun provideShowToast(): IShowToast {
return ShowToast(context)
}
@@ -115,8 +115,8 @@ private val darkScheme = darkColorScheme(
private val shapes = Shapes(
extraSmall = RoundedCornerShape(4.dp), // menus, snackbars, text fields
small = RoundedCornerShape(6.dp), // buttons, chips
medium = RoundedCornerShape(8.dp), // cards, small FABs
small = RoundedCornerShape(8.dp), // buttons, chips
medium = RoundedCornerShape(12.dp), // cards, small FABs
large = RoundedCornerShape(24.dp), // extended FABs, FABs, nav drawers
extraLarge = RoundedCornerShape(32.dp) // large FABs
)
@@ -16,8 +16,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
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
@@ -207,6 +207,19 @@ fun IconCard(action: () -> Unit, resId: Int, modifier: Modifier = Modifier) {
}
}
@Composable
fun PrimaryIconCard(modifier: Modifier = Modifier, resId: Int, onClick: () -> Unit) {
val clickableMod = modifier.clickable { onClick() }
ElevatedCard(
modifier = Modifier.size(102.dp, 48.dp),
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Box(modifier = clickableMod.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(painter = painterResource(id = resId), contentDescription = null)
}
}
}
@Composable
fun CardLoadingIndicator() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
@@ -320,7 +333,6 @@ fun ScreenColumn(
floatingBar: @Composable () -> Unit = {},
content: @Composable (Boolean) -> Unit = {}
) {
val surfaceCorners = LocalSpacing.current.large
val isVerticalLayout = isVerticalLayout()
Surface(color = MaterialTheme.colorScheme.background) {
Box(modifier = Modifier.layoutPadding(), contentAlignment = Alignment.BottomCenter) {
@@ -328,7 +340,6 @@ fun ScreenColumn(
topBar(isVerticalLayout)
Surface(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(topStart = surfaceCorners, topEnd = surfaceCorners),
color = MaterialTheme.colorScheme.surfaceContainer
) { content(isVerticalLayout) }
}
@@ -3,19 +3,16 @@ package com.rtbishop.look4sat.presentation.satellites
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@@ -49,6 +46,7 @@ import com.rtbishop.look4sat.presentation.common.CardLoadingIndicator
import com.rtbishop.look4sat.presentation.common.EmptyListCard
import com.rtbishop.look4sat.presentation.common.IconCard
import com.rtbishop.look4sat.presentation.common.InfoDialog
import com.rtbishop.look4sat.presentation.common.PrimaryIconCard
import com.rtbishop.look4sat.presentation.common.TopBar
import com.rtbishop.look4sat.presentation.common.infiniteMarquee
import com.rtbishop.look4sat.presentation.common.isVerticalLayout
@@ -88,7 +86,7 @@ private fun SatellitesScreen(uiState: SatellitesState, navigateUp: () -> Unit) {
if (isVerticalLayout()) {
TopBar {
TypeCard(types = uiState.currentTypes, toggleDialog, modifier = Modifier.weight(1f))
SaveButton(saveSelection = saveSelection, modifier = Modifier.height(48.dp))
PrimaryIconCard(onClick = saveSelection, resId = R.drawable.ic_done)
}
TopBar {
SearchBar(setQuery = { setQuery(it) }, modifier = Modifier.weight(1f))
@@ -97,7 +95,7 @@ private fun SatellitesScreen(uiState: SatellitesState, navigateUp: () -> Unit) {
}
} else {
TopBar {
SaveButton(saveSelection = saveSelection, modifier = Modifier.height(48.dp))
PrimaryIconCard(onClick = saveSelection, resId = R.drawable.ic_done)
TypeCard(types = uiState.currentTypes, toggleDialog, modifier = Modifier.weight(1f))
SearchBar(setQuery = { setQuery(it) }, modifier = Modifier.weight(1f))
IconCard(action = unselectAll, resId = R.drawable.ic_check_off)
@@ -163,19 +161,6 @@ private fun SearchBar(setQuery: (String) -> Unit, modifier: Modifier = Modifier)
}
}
@Composable
private fun SaveButton(saveSelection: () -> Unit, modifier: Modifier = Modifier) {
val clickableModifier = modifier.clickable { saveSelection() }
ElevatedCard(
modifier = Modifier.width(102.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Box(modifier = clickableModifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(painter = painterResource(id = R.drawable.ic_done), contentDescription = null)
}
}
}
@Composable
private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifier = Modifier) {
val typesText = if (types.isEmpty()) "All" else types.joinToString(", ")
@@ -187,7 +172,11 @@ private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifie
.height(48.dp)
.clickable { onClick() }) {
Spacer(Modifier)
Icon(painter = painterResource(id = R.drawable.ic_tags), contentDescription = null)
Icon(
painter = painterResource(id = R.drawable.ic_tags),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "Tags: $typesText",
fontSize = 16.sp,
@@ -4,6 +4,7 @@ import android.Manifest
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -11,7 +12,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
@@ -27,6 +27,7 @@ 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.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -45,7 +46,11 @@ import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.common.CardButton
import com.rtbishop.look4sat.presentation.common.layoutPadding
import com.rtbishop.look4sat.presentation.common.IconCard
import com.rtbishop.look4sat.presentation.common.PrimaryIconCard
import com.rtbishop.look4sat.presentation.common.ScreenColumn
import com.rtbishop.look4sat.presentation.common.TopBar
import com.rtbishop.look4sat.presentation.common.infiniteMarquee
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -61,9 +66,6 @@ fun NavGraphBuilder.settingsDestination() {
}
}
private const val POLICY_URL = "https://sites.google.com/view/look4sat-privacy-policy/home"
private const val LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html"
@Composable
private fun SettingsScreen(uiState: SettingsState) {
// Permissions setup
@@ -127,99 +129,64 @@ private fun SettingsScreen(uiState: SettingsState) {
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))
}
val uriHandler = LocalUriHandler.current
// Screen setup
LazyVerticalGrid(
columns = GridCells.Adaptive(320.dp),
modifier = Modifier.layoutPadding(),
horizontalArrangement = Arrangement.spacedBy(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 { CardCredits() }
}
}
@Preview(showBackground = true)
@Composable
private fun CardAboutPreview() = MainTheme { CardAbout(version = "4.0.0") }
@Composable
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)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.ic_satellites),
tint = MaterialTheme.colorScheme.primary,
contentDescription = null,
modifier = Modifier.size(72.dp)
)
Column {
Text(
text = stringResource(id = R.string.app_name),
fontSize = 48.sp,
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(id = R.string.app_version, version), fontSize = 20.sp
)
ScreenColumn(
topBar = { isVerticalLayout ->
if (isVerticalLayout) {
TopBar {
TopCard(onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=com.rtbishop.look4sat") }, version = uiState.appVersionName, modifier = Modifier.weight(1f))
PrimaryIconCard(onClick = { uriHandler.openUri("https://ko-fi.com/rt_bishop") }, resId = R.drawable.ic_pound)
}
TopBar {
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
BotCard(onClick = { uriHandler.openUri("https://www.gnu.org/licenses/gpl-3.0.html") }, R.drawable.ic_license, "Licence", modifier = Modifier.weight(1f))
BotCard(onClick = { uriHandler.openUri("https://sites.google.com/view/look4sat-privacy-policy/home") }, R.drawable.ic_policy, "Privacy", modifier = Modifier.weight(1f))
}
IconCard(action = { uriHandler.openUri("https://f-droid.org/en/packages/com.rtbishop.look4sat/") }, resId = R.drawable.ic_fdroid)
IconCard(action = { uriHandler.openUri("https://github.com/rt-bishop/Look4Sat/") }, resId = R.drawable.ic_github)
}
} else {
TopBar {
PrimaryIconCard(onClick = { uriHandler.openUri("https://ko-fi.com/rt_bishop") }, resId = R.drawable.ic_pound)
TopCard(onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=com.rtbishop.look4sat") }, version = uiState.appVersionName, modifier = Modifier.weight(1f))
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
BotCard(onClick = { uriHandler.openUri("https://www.gnu.org/licenses/gpl-3.0.html") }, R.drawable.ic_license, "Licence", modifier = Modifier.weight(1f))
BotCard(onClick = { uriHandler.openUri("https://sites.google.com/view/look4sat-privacy-policy/home") }, R.drawable.ic_policy, "Privacy", modifier = Modifier.weight(1f))
}
IconCard(action = { uriHandler.openUri("https://f-droid.org/en/packages/com.rtbishop.look4sat/") }, resId = R.drawable.ic_fdroid)
IconCard(action = { uriHandler.openUri("https://github.com/rt-bishop/Look4Sat/") }, resId = R.drawable.ic_github)
}
}
Text(
text = stringResource(id = R.string.app_subtitle),
fontSize = 20.sp,
modifier = Modifier.padding(top = 4.dp, bottom = 2.dp)
)
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
CardButton(
onClick = { sendUrlAction(SystemAction.OpenGitHub) },
text = stringResource(id = R.string.btn_github),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = { sendUrlAction(SystemAction.OpenDonate) },
text = stringResource(id = R.string.btn_donate),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = { sendUrlAction(SystemAction.OpenFDroid) },
text = stringResource(id = R.string.btn_fdroid),
modifier = Modifier.weight(1f)
}
) { _ ->
LazyVerticalGrid(
columns = GridCells.Adaptive(320.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.clip(MaterialTheme.shapes.medium)
) {
item {
LocationCard(
positionSettings,
setGpsPos,
showPosDialog,
showLocDialog,
dismissPos,
uiState.sendSystemAction
)
}
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) }
item {
OtherCard(
otherSettings,
toggleUtc,
toggleUpdate,
toggleSweep,
toggleSensor
)
}
item { CardCredits() }
}
}
}
@@ -419,7 +386,7 @@ private fun OtherCardPreview() = MainTheme {
shouldSeeWarning = false,
shouldSeeWelcome = false
)
OtherCard(settings = values, {}, {}, {}, {}, {})
OtherCard(settings = values, {}, {}, {}, {})
}
@Composable
@@ -428,8 +395,7 @@ private fun OtherCard(
toggleUtc: (Boolean) -> Unit,
toggleUpdate: (Boolean) -> Unit,
toggleSweep: (Boolean) -> Unit,
toggleSensor: (Boolean) -> Unit,
toggleLightTheme: (Boolean) -> Unit
toggleSensor: (Boolean) -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
@@ -465,17 +431,6 @@ private fun OtherCard(
Text(text = stringResource(id = R.string.other_switch_sensors))
Switch(checked = settings.stateOfSensors, onCheckedChange = { toggleSensor(it) })
}
// Row(
// horizontalArrangement = Arrangement.SpaceBetween,
// verticalAlignment = Alignment.CenterVertically,
// modifier = Modifier.fillMaxWidth()
// ) {
// Text(text = stringResource(id = R.string.other_switch_light_theme))
// Switch(
// checked = settings.stateOfLightTheme,
// onCheckedChange = { toggleLightTheme(it) }
// )
// }
}
}
}
@@ -508,7 +463,6 @@ private fun CardCreditsPreview() = MainTheme { CardCredits() }
@Composable
private fun CardCredits(modifier: Modifier = Modifier) {
val uriHandler = LocalUriHandler.current
ElevatedCard(modifier = modifier.fillMaxWidth()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -533,19 +487,58 @@ private fun CardCredits(modifier: Modifier = Modifier) {
color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(6.dp)
)
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
CardButton(
onClick = { uriHandler.openUri(LICENSE_URL) },
text = stringResource(id = R.string.btn_license),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = { uriHandler.openUri(POLICY_URL) },
text = stringResource(id = R.string.btn_privacy),
modifier = Modifier.weight(1f)
)
}
}
}
}
@Composable
private fun TopCard(onClick: () -> Unit, modifier: Modifier = Modifier, version: String) {
ElevatedCard(modifier = modifier) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(48.dp)
.clickable { onClick() }) {
Spacer(Modifier)
Icon(
painter = painterResource(id = R.drawable.ic_satellites),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "Look4Sat v$version",
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier
.weight(1f)
.padding(end = 6.dp)
.infiniteMarquee()
)
}
}
}
@Composable
private fun BotCard(onClick: () -> Unit, resId: Int, text: String, modifier: Modifier = Modifier) {
ElevatedCard(modifier = modifier) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(48.dp)
.clickable { onClick() }) {
Spacer(Modifier)
Icon(painter = painterResource(id = resId), contentDescription = null)
Text(
text = text,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier
.weight(1f)
.padding(end = 6.dp)
.infiniteMarquee()
)
}
}
}
@@ -2,6 +2,7 @@ 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.OrbitalPass
data class PositionSettings(
val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int
@@ -22,6 +23,9 @@ data class RCSettings(
)
data class SettingsState(
val nextPass: OrbitalPass,
val nextTime: String,
val isNextTimeAos: Boolean,
val appVersionName: String,
val positionSettings: PositionSettings,
val dataSettings: DataSettings,
@@ -24,26 +24,31 @@ 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.predict.OrbitalPass
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.usecase.IOpenWebUrl
import com.rtbishop.look4sat.domain.usecase.IShowToast
import com.rtbishop.look4sat.domain.utility.toTimerString
import com.rtbishop.look4sat.presentation.common.getDefaultPass
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class SettingsViewModel(
private val databaseRepo: IDatabaseRepo,
private val satelliteRepo: ISatelliteRepo,
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 defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val defaultRCSettings = RCSettings(
@@ -57,6 +62,9 @@ class SettingsViewModel(
)
private val _uiState = MutableStateFlow(
SettingsState(
nextTime = "00:00:00",
isNextTimeAos = true,
nextPass = getDefaultPass(),
appVersionName = appVersionName,
positionSettings = defaultPosSettings,
dataSettings = defaultDataSettings,
@@ -67,9 +75,24 @@ class SettingsViewModel(
sendSystemAction = ::handleAction
)
)
private var processing: Job? = null
val uiState: StateFlow<SettingsState> = _uiState
init {
viewModelScope.launch {
satelliteRepo.passes.collectLatest { passes ->
processing?.cancelAndJoin()
processing = viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val newPasses = satelliteRepo.processPasses(passes, timeNow)
setPassInfo(newPasses, timeNow)
delay(1000)
}
}
}
}
viewModelScope.launch {
settingsRepo.stationPosition.collect { geoPos ->
val newPosSettings = _uiState.value.positionSettings.copy(
@@ -127,15 +150,13 @@ class SettingsViewModel(
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)
else -> {}
}
}
private fun setGpsPosition() {
if (settingsRepo.setStationPositionGps()) {
if (settingsRepo.setStationPosition()) {
val messageResId = R.string.location_success
val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = true, messageResId = messageResId
@@ -151,7 +172,7 @@ class SettingsViewModel(
}
private fun setGeoPosition(latitude: Double, longitude: Double) {
if (settingsRepo.setStationPositionGeo(latitude, longitude, 0.0)) {
if (settingsRepo.setStationPosition(latitude, longitude, 0.0)) {
val messageResId = R.string.location_success
val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = messageResId
@@ -167,7 +188,7 @@ class SettingsViewModel(
}
private fun setQthPosition(locator: String) {
if (settingsRepo.setStationPositionQth(locator)) {
if (settingsRepo.setStationPosition(locator)) {
val messageResId = R.string.location_success
val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = messageResId
@@ -213,6 +234,19 @@ class SettingsViewModel(
}
}
private fun setPassInfo(passes: List<OrbitalPass>, timeNow: Long) {
if (passes.isEmpty()) return
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val time = nextPass.aosTime.minus(timeNow).toTimerString()
_uiState.update { it.copy(nextPass = nextPass, nextTime = time, isNextTimeAos = true) }
} catch (_: NoSuchElementException) {
val lastPass = passes.last()
val time = lastPass.losTime.minus(timeNow).toTimerString()
_uiState.update { it.copy(nextPass = lastPass, nextTime = time, isNextTimeAos = false) }
}
}
private fun clearAllData() = viewModelScope.launch { databaseRepo.clearAllData() }
companion object {
@@ -222,8 +256,8 @@ class SettingsViewModel(
val container = (this[applicationKey] as MainApplication).container
SettingsViewModel(
container.databaseRepo,
container.satelliteRepo,
container.settingsRepo,
container.provideOpenWebUrl(),
container.provideShowToast(),
container.provideAppVersionName()
)
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,520Q430,520 395,485Q360,450 360,400Q360,350 395,315Q430,280 480,280Q530,280 565,315Q600,350 600,400Q600,450 565,485Q530,520 480,520ZM240,920L240,611Q202,569 181,515Q160,461 160,400Q160,266 253,173Q346,80 480,80Q614,80 707,173Q800,266 800,400Q800,461 779,515Q758,569 720,611L720,920L480,840L240,920ZM480,640Q580,640 650,570Q720,500 720,400Q720,300 650,230Q580,160 480,160Q380,160 310,230Q240,300 240,400Q240,500 310,570Q380,640 480,640ZM320,801L480,760L640,801L640,677Q605,697 564.5,708.5Q524,720 480,720Q436,720 395.5,708.5Q355,697 320,677L320,801ZM480,739L480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739Q480,739 480,739L480,739L480,739Z" />
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,529 771,607.5Q742,686 688,746L560,618Q542,629 521.5,634.5Q501,640 480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,502 634.5,522.5Q629,543 618,562L678,622Q698,581 709,536Q720,491 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796Q506,788 529.5,775.5Q553,763 576,746L632,802Q599,829 560.5,849Q522,869 480,880ZM480,560Q513,560 536.5,536.5Q560,513 560,480Q560,447 536.5,423.5Q513,400 480,400Q447,400 423.5,423.5Q400,447 400,480Q400,513 423.5,536.5Q447,560 480,560ZM488,483L488,483Q488,483 488,483Q488,483 488,483L488,483Q488,483 488,483Q488,483 488,483L488,483L488,483L488,483L488,483Q488,483 488,483Q488,483 488,483Q488,483 488,483Q488,483 488,483Z" />
</vector>
-2
View File
@@ -192,8 +192,6 @@
<a href="https://celestrak.com/">Celestrak</a> website that provides access to the TLE data.
\n<a href="https://libre.space/">Libre Space Foundation</a> for their
<a href="https://db.satnogs.org/">SatNOGS</a> API and DB providing a huge amount of data.
\n<a href="https://appoftheday.downloadastro.com/app/look4sat-satellite-tracker/">DownloadAstro</a>
team for their interest to the app and the interview published.
</string>
<string name="outro_license">The app comes with no warranty.</string>
</resources>
@@ -18,9 +18,10 @@
package com.rtbishop.look4sat.data.repository
import android.content.SharedPreferences
import android.location.Location
import android.location.LocationManager
import android.os.CancellationSignal
import androidx.core.content.edit
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.OtherSettings
@@ -30,11 +31,13 @@ import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.positionToQth
import com.rtbishop.look4sat.domain.utility.qthToPosition
import com.rtbishop.look4sat.domain.utility.round
import java.util.concurrent.Executors
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class SettingsRepo(private val manager: LocationManager, private val preferences: SharedPreferences) : ISettingsRepo {
class SettingsRepo(
private val locationManager: LocationManager,
private val preferences: SharedPreferences
) : ISettingsRepo, LocationListenerCompat {
private val keyBluetoothAddress = "bluetoothAddress"
private val keyBluetoothName = "bluetoothName"
@@ -118,31 +121,30 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
//region # Station position settings
private val _stationPosition = MutableStateFlow(getStationPosition())
private val executor = Executors.newSingleThreadExecutor()
private val providerDef = LocationManager.PASSIVE_PROVIDER
private val providerGps = LocationManager.GPS_PROVIDER
private val providerNet = LocationManager.NETWORK_PROVIDER
private val timeoutSignal = CancellationSignal().apply {
setOnCancelListener { _stationPosition.value = getStationPosition() }
}
override val stationPosition: StateFlow<GeoPos> = _stationPosition
override fun setStationPositionGeo(latitude: Double, longitude: Double, altitude: Double): Boolean {
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean {
val newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = positionToQth(latitude, newLongitude) ?: return false
setStationPosition(latitude, newLongitude, altitude, locator)
return true
}
override fun setStationPositionGps(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false
override fun setStationPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
try {
val hasProviderGps = LocationManagerCompat.hasProvider(manager, providerGps)
val hasProviderNet = LocationManagerCompat.hasProvider(manager, providerNet)
val provider = if (hasProviderGps) providerGps else if (hasProviderNet) providerNet else providerDef
println("Requesting location for $provider provider")
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) {
it?.let { setStationPositionGeo(it.latitude, it.longitude, it.altitude) }
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet)
val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef
val location = locationManager.getLastKnownLocation(providerDef)
if (location == null || System.currentTimeMillis() - location.time > 600_000L) {
println("Requesting location for $provider provider")
locationManager.requestLocationUpdates(provider, 0L, 0f, this)
} else {
setStationPosition(location.latitude, location.longitude, location.altitude)
}
} catch (exception: SecurityException) {
println("No permissions were given - $exception")
@@ -150,7 +152,7 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
return true
}
override fun setStationPositionQth(locator: String): Boolean {
override fun setStationPosition(locator: String): Boolean {
val position = qthToPosition(locator) ?: return false
setStationPosition(position.latitude, position.longitude, 0.0, locator)
return true
@@ -277,7 +279,7 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
override fun getBluetoothAddress(): String = preferences.getString(keyBluetoothAddress, null) ?: "00:0C:BF:13:80:5D"
override fun setBluetoothAddress(value: String) = preferences.edit { putString(keyBluetoothAddress, value) }
override fun getBluetoothFormat(): String = preferences.getString(keyBluetoothFormat, null) ?: "W\$AZ \$EL"
override fun getBluetoothFormat(): String = preferences.getString(keyBluetoothFormat, null) ?: $$"W$AZ $EL"
override fun setBluetoothFormat(value: String) = preferences.edit { putString(keyBluetoothFormat, value) }
override fun getBluetoothName(): String = preferences.getString(keyBluetoothName, null) ?: "Default"
@@ -294,5 +296,8 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
override fun getRotatorState(): Boolean = preferences.getBoolean(keyRotatorState, false)
override fun setRotatorState(value: Boolean) = preferences.edit { putBoolean(keyRotatorState, value) }
override fun onLocationChanged(location: Location) {
setStationPosition(location.latitude, location.longitude, location.altitude)
}
//endregion
}
@@ -1,18 +0,0 @@
package com.rtbishop.look4sat.data.usecase
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import com.rtbishop.look4sat.domain.usecase.IOpenWebUrl
import androidx.core.net.toUri
class OpenWebUrl(private val context: Context) : IOpenWebUrl {
override fun invoke(url: String) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} catch (_: ActivityNotFoundException) {
}
}
}
@@ -39,9 +39,9 @@ interface ISettingsRepo {
//region # Station position settings
val stationPosition: StateFlow<GeoPos>
fun setStationPositionGeo(latitude: Double, longitude: Double, altitude: Double): Boolean
fun setStationPositionGps(): Boolean
fun setStationPositionQth(locator: String): Boolean
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
fun setStationPosition(): Boolean
fun setStationPosition(locator: String): Boolean
//endregion
//region # Database update settings
@@ -1,5 +0,0 @@
package com.rtbishop.look4sat.domain.usecase
interface IOpenWebUrl {
operator fun invoke(url: String)
}
+4 -4
View File
@@ -1,16 +1,16 @@
[versions]
android-gradle-plugin = "8.13.0"
android-gradle-plugin = "8.13.1"
google-ksp = "2.3.0"
kotlin = "2.2.21"
androidx-core-ktx = "1.17.0"
androidx-core-splashscreen = "1.0.1"
androidx-core-splashscreen = "1.2.0"
androidx-room = "2.8.3"
compose-bom = "2025.10.01"
compose-bom = "2025.11.00"
compose-activity = "1.11.0"
compose-lifecycle = "2.9.4"
compose-navigation = "2.9.5"
compose-navigation = "2.9.6"
other-coroutines = "1.10.2"
other-json = "20250517"