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 { buildTypes {
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
} }
release { release {
isMinifyEnabled = true 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.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.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
@@ -27,7 +26,6 @@ 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.usecase.IShowToast
import com.rtbishop.look4sat.domain.utility.DataParser import com.rtbishop.look4sat.domain.utility.DataParser
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@@ -58,10 +56,6 @@ class MainContainer(private val context: Context) {
return AddToCalendar(context) return AddToCalendar(context)
} }
fun provideOpenWebUrl(): IOpenWebUrl {
return OpenWebUrl(context)
}
fun provideShowToast(): IShowToast { fun provideShowToast(): IShowToast {
return ShowToast(context) return ShowToast(context)
} }
@@ -115,8 +115,8 @@ private val darkScheme = darkColorScheme(
private val shapes = Shapes( private val shapes = Shapes(
extraSmall = RoundedCornerShape(4.dp), // menus, snackbars, text fields extraSmall = RoundedCornerShape(4.dp), // menus, snackbars, text fields
small = RoundedCornerShape(6.dp), // buttons, chips small = RoundedCornerShape(8.dp), // buttons, chips
medium = RoundedCornerShape(8.dp), // cards, small FABs medium = RoundedCornerShape(12.dp), // cards, small FABs
large = RoundedCornerShape(24.dp), // extended FABs, FABs, nav drawers large = RoundedCornerShape(24.dp), // extended FABs, FABs, nav drawers
extraLarge = RoundedCornerShape(32.dp) // large FABs 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.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
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
@@ -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 @Composable
fun CardLoadingIndicator() { fun CardLoadingIndicator() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
@@ -320,7 +333,6 @@ fun ScreenColumn(
floatingBar: @Composable () -> Unit = {}, floatingBar: @Composable () -> Unit = {},
content: @Composable (Boolean) -> Unit = {} content: @Composable (Boolean) -> Unit = {}
) { ) {
val surfaceCorners = LocalSpacing.current.large
val isVerticalLayout = isVerticalLayout() val isVerticalLayout = isVerticalLayout()
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
Box(modifier = Modifier.layoutPadding(), contentAlignment = Alignment.BottomCenter) { Box(modifier = Modifier.layoutPadding(), contentAlignment = Alignment.BottomCenter) {
@@ -328,7 +340,6 @@ fun ScreenColumn(
topBar(isVerticalLayout) topBar(isVerticalLayout)
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(topStart = surfaceCorners, topEnd = surfaceCorners),
color = MaterialTheme.colorScheme.surfaceContainer color = MaterialTheme.colorScheme.surfaceContainer
) { content(isVerticalLayout) } ) { content(isVerticalLayout) }
} }
@@ -3,19 +3,16 @@ package com.rtbishop.look4sat.presentation.satellites
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
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon 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.EmptyListCard
import com.rtbishop.look4sat.presentation.common.IconCard import com.rtbishop.look4sat.presentation.common.IconCard
import com.rtbishop.look4sat.presentation.common.InfoDialog 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.TopBar
import com.rtbishop.look4sat.presentation.common.infiniteMarquee import com.rtbishop.look4sat.presentation.common.infiniteMarquee
import com.rtbishop.look4sat.presentation.common.isVerticalLayout import com.rtbishop.look4sat.presentation.common.isVerticalLayout
@@ -88,7 +86,7 @@ private fun SatellitesScreen(uiState: SatellitesState, navigateUp: () -> Unit) {
if (isVerticalLayout()) { if (isVerticalLayout()) {
TopBar { TopBar {
TypeCard(types = uiState.currentTypes, toggleDialog, modifier = Modifier.weight(1f)) 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 { TopBar {
SearchBar(setQuery = { setQuery(it) }, modifier = Modifier.weight(1f)) SearchBar(setQuery = { setQuery(it) }, modifier = Modifier.weight(1f))
@@ -97,7 +95,7 @@ private fun SatellitesScreen(uiState: SatellitesState, navigateUp: () -> Unit) {
} }
} else { } else {
TopBar { 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)) TypeCard(types = uiState.currentTypes, toggleDialog, modifier = Modifier.weight(1f))
SearchBar(setQuery = { setQuery(it) }, modifier = Modifier.weight(1f)) SearchBar(setQuery = { setQuery(it) }, modifier = Modifier.weight(1f))
IconCard(action = unselectAll, resId = R.drawable.ic_check_off) 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 @Composable
private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifier = Modifier) { private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifier = Modifier) {
val typesText = if (types.isEmpty()) "All" else types.joinToString(", ") 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) .height(48.dp)
.clickable { onClick() }) { .clickable { onClick() }) {
Spacer(Modifier) 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(
text = "Tags: $typesText", text = "Tags: $typesText",
fontSize = 16.sp, fontSize = 16.sp,
@@ -4,6 +4,7 @@ import android.Manifest
import android.os.Build 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 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.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.draw.clip
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.MainTheme
import com.rtbishop.look4sat.presentation.Screen import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.common.CardButton 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.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale 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 @Composable
private fun SettingsScreen(uiState: SettingsState) { private fun SettingsScreen(uiState: SettingsState) {
// Permissions setup // Permissions setup
@@ -127,99 +129,64 @@ private fun SettingsScreen(uiState: SettingsState) {
val toggleUpdate = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleUpdate(value)) } val toggleUpdate = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleUpdate(value)) }
val toggleSweep = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSweep(value)) } val toggleSweep = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSweep(value)) }
val toggleSensor = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSensor(value)) } val toggleSensor = { value: Boolean -> uiState.sendAction(SettingsAction.ToggleSensor(value)) }
val toggleLightTheme = { value: Boolean -> val uriHandler = LocalUriHandler.current
uiState.sendAction(SettingsAction.ToggleLightTheme(value))
}
// Screen setup ScreenColumn(
LazyVerticalGrid( topBar = { isVerticalLayout ->
columns = GridCells.Adaptive(320.dp), if (isVerticalLayout) {
modifier = Modifier.layoutPadding(), TopBar {
horizontalArrangement = Arrangement.spacedBy(6.dp), TopCard(onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=com.rtbishop.look4sat") }, version = uiState.appVersionName, modifier = Modifier.weight(1f))
verticalArrangement = Arrangement.spacedBy(6.dp) PrimaryIconCard(onClick = { uriHandler.openUri("https://ko-fi.com/rt_bishop") }, resId = R.drawable.ic_pound)
) { }
item { CardAbout(uiState.appVersionName, uiState.sendSystemAction) } TopBar {
item { Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
LocationCard( BotCard(onClick = { uriHandler.openUri("https://www.gnu.org/licenses/gpl-3.0.html") }, R.drawable.ic_license, "Licence", modifier = Modifier.weight(1f))
positionSettings, BotCard(onClick = { uriHandler.openUri("https://sites.google.com/view/look4sat-privacy-policy/home") }, R.drawable.ic_policy, "Privacy", modifier = Modifier.weight(1f))
setGpsPos, }
showPosDialog, IconCard(action = { uriHandler.openUri("https://f-droid.org/en/packages/com.rtbishop.look4sat/") }, resId = R.drawable.ic_fdroid)
showLocDialog, IconCard(action = { uriHandler.openUri("https://github.com/rt-bishop/Look4Sat/") }, resId = R.drawable.ic_github)
dismissPos, }
uiState.sendSystemAction } else {
) TopBar {
} PrimaryIconCard(onClick = { uriHandler.openUri("https://ko-fi.com/rt_bishop") }, resId = R.drawable.ic_pound)
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) } TopCard(onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=com.rtbishop.look4sat") }, version = uiState.appVersionName, modifier = Modifier.weight(1f))
item { Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
OtherCard( BotCard(onClick = { uriHandler.openUri("https://www.gnu.org/licenses/gpl-3.0.html") }, R.drawable.ic_license, "Licence", modifier = Modifier.weight(1f))
otherSettings, BotCard(onClick = { uriHandler.openUri("https://sites.google.com/view/look4sat-privacy-policy/home") }, R.drawable.ic_policy, "Privacy", modifier = Modifier.weight(1f))
toggleUtc, }
toggleUpdate, IconCard(action = { uriHandler.openUri("https://f-droid.org/en/packages/com.rtbishop.look4sat/") }, resId = R.drawable.ic_fdroid)
toggleSweep, IconCard(action = { uriHandler.openUri("https://github.com/rt-bishop/Look4Sat/") }, resId = R.drawable.ic_github)
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
)
} }
} }
Text( }
text = stringResource(id = R.string.app_subtitle), ) { _ ->
fontSize = 20.sp, LazyVerticalGrid(
modifier = Modifier.padding(top = 4.dp, bottom = 2.dp) columns = GridCells.Adaptive(320.dp),
) horizontalArrangement = Arrangement.spacedBy(6.dp),
Row(horizontalArrangement = Arrangement.SpaceEvenly) { verticalArrangement = Arrangement.spacedBy(6.dp),
CardButton( modifier = Modifier.clip(MaterialTheme.shapes.medium)
onClick = { sendUrlAction(SystemAction.OpenGitHub) }, ) {
text = stringResource(id = R.string.btn_github), item {
modifier = Modifier.weight(1f) LocationCard(
) positionSettings,
Spacer(modifier = Modifier.width(6.dp)) setGpsPos,
CardButton( showPosDialog,
onClick = { sendUrlAction(SystemAction.OpenDonate) }, showLocDialog,
text = stringResource(id = R.string.btn_donate), dismissPos,
modifier = Modifier.weight(1f) uiState.sendSystemAction
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = { sendUrlAction(SystemAction.OpenFDroid) },
text = stringResource(id = R.string.btn_fdroid),
modifier = Modifier.weight(1f)
) )
} }
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, shouldSeeWarning = false,
shouldSeeWelcome = false shouldSeeWelcome = false
) )
OtherCard(settings = values, {}, {}, {}, {}, {}) OtherCard(settings = values, {}, {}, {}, {})
} }
@Composable @Composable
@@ -428,8 +395,7 @@ private fun OtherCard(
toggleUtc: (Boolean) -> Unit, toggleUtc: (Boolean) -> Unit,
toggleUpdate: (Boolean) -> Unit, toggleUpdate: (Boolean) -> Unit,
toggleSweep: (Boolean) -> Unit, toggleSweep: (Boolean) -> Unit,
toggleSensor: (Boolean) -> Unit, toggleSensor: (Boolean) -> Unit
toggleLightTheme: (Boolean) -> Unit
) { ) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) { 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)) Text(text = stringResource(id = R.string.other_switch_sensors))
Switch(checked = settings.stateOfSensors, onCheckedChange = { toggleSensor(it) }) 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 @Composable
private fun CardCredits(modifier: Modifier = Modifier) { private fun CardCredits(modifier: Modifier = Modifier) {
val uriHandler = LocalUriHandler.current
ElevatedCard(modifier = modifier.fillMaxWidth()) { ElevatedCard(modifier = modifier.fillMaxWidth()) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -533,19 +487,58 @@ private fun CardCredits(modifier: Modifier = Modifier) {
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = modifier.padding(6.dp) 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) @Composable
) private fun TopCard(onClick: () -> Unit, modifier: Modifier = Modifier, version: String) {
Spacer(modifier = Modifier.width(6.dp)) ElevatedCard(modifier = modifier) {
CardButton( Row(
onClick = { uriHandler.openUri(POLICY_URL) }, horizontalArrangement = Arrangement.spacedBy(12.dp),
text = stringResource(id = R.string.btn_privacy), verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f) 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.model.OtherSettings
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalPass
data class PositionSettings( data class PositionSettings(
val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int val isUpdating: Boolean, val stationPos: GeoPos, val messageResId: Int
@@ -22,6 +23,9 @@ data class RCSettings(
) )
data class SettingsState( data class SettingsState(
val nextPass: OrbitalPass,
val nextTime: String,
val isNextTimeAos: Boolean,
val appVersionName: String, val appVersionName: String,
val positionSettings: PositionSettings, val positionSettings: PositionSettings,
val dataSettings: DataSettings, val dataSettings: DataSettings,
@@ -24,26 +24,31 @@ 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.predict.OrbitalPass
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.ISettingsRepo 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.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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SettingsViewModel( class SettingsViewModel(
private val databaseRepo: IDatabaseRepo, private val databaseRepo: IDatabaseRepo,
private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo, private val settingsRepo: ISettingsRepo,
private val openWebUrl: IOpenWebUrl,
private val showToast: IShowToast, private val showToast: IShowToast,
appVersionName: String 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 defaultDataSettings = DataSettings(false, 0, 0, 0L) private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val defaultRCSettings = RCSettings( private val defaultRCSettings = RCSettings(
@@ -57,6 +62,9 @@ class SettingsViewModel(
) )
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
SettingsState( SettingsState(
nextTime = "00:00:00",
isNextTimeAos = true,
nextPass = getDefaultPass(),
appVersionName = appVersionName, appVersionName = appVersionName,
positionSettings = defaultPosSettings, positionSettings = defaultPosSettings,
dataSettings = defaultDataSettings, dataSettings = defaultDataSettings,
@@ -67,9 +75,24 @@ class SettingsViewModel(
sendSystemAction = ::handleAction sendSystemAction = ::handleAction
) )
) )
private var processing: Job? = null
val uiState: StateFlow<SettingsState> = _uiState val uiState: StateFlow<SettingsState> = _uiState
init { 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 { viewModelScope.launch {
settingsRepo.stationPosition.collect { geoPos -> settingsRepo.stationPosition.collect { geoPos ->
val newPosSettings = _uiState.value.positionSettings.copy( val newPosSettings = _uiState.value.positionSettings.copy(
@@ -127,15 +150,13 @@ class SettingsViewModel(
private fun handleAction(action: SystemAction) { private fun handleAction(action: SystemAction) {
when (action) { when (action) {
SystemAction.OpenGitHub -> openWebUrl(githubUrl)
SystemAction.OpenDonate -> openWebUrl(donateUrl)
SystemAction.OpenFDroid -> openWebUrl(fdroidUrl)
is SystemAction.ShowToast -> showToast(action.message) is SystemAction.ShowToast -> showToast(action.message)
else -> {}
} }
} }
private fun setGpsPosition() { private fun setGpsPosition() {
if (settingsRepo.setStationPositionGps()) { if (settingsRepo.setStationPosition()) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
val newPosSettings = _uiState.value.positionSettings.copy( val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = true, messageResId = messageResId isUpdating = true, messageResId = messageResId
@@ -151,7 +172,7 @@ class SettingsViewModel(
} }
private fun setGeoPosition(latitude: Double, longitude: Double) { 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 messageResId = R.string.location_success
val newPosSettings = _uiState.value.positionSettings.copy( val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = messageResId isUpdating = false, messageResId = messageResId
@@ -167,7 +188,7 @@ class SettingsViewModel(
} }
private fun setQthPosition(locator: String) { private fun setQthPosition(locator: String) {
if (settingsRepo.setStationPositionQth(locator)) { if (settingsRepo.setStationPosition(locator)) {
val messageResId = R.string.location_success val messageResId = R.string.location_success
val newPosSettings = _uiState.value.positionSettings.copy( val newPosSettings = _uiState.value.positionSettings.copy(
isUpdating = false, messageResId = messageResId 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() } private fun clearAllData() = viewModelScope.launch { databaseRepo.clearAllData() }
companion object { companion object {
@@ -222,8 +256,8 @@ class SettingsViewModel(
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
SettingsViewModel( SettingsViewModel(
container.databaseRepo, container.databaseRepo,
container.satelliteRepo,
container.settingsRepo, container.settingsRepo,
container.provideOpenWebUrl(),
container.provideShowToast(), container.provideShowToast(),
container.provideAppVersionName() 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. <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 \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. <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>
<string name="outro_license">The app comes with no warranty.</string> <string name="outro_license">The app comes with no warranty.</string>
</resources> </resources>
@@ -18,9 +18,10 @@
package com.rtbishop.look4sat.data.repository package com.rtbishop.look4sat.data.repository
import android.content.SharedPreferences import android.content.SharedPreferences
import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.CancellationSignal
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import com.rtbishop.look4sat.domain.model.DatabaseState import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.OtherSettings 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.positionToQth
import com.rtbishop.look4sat.domain.utility.qthToPosition import com.rtbishop.look4sat.domain.utility.qthToPosition
import com.rtbishop.look4sat.domain.utility.round import com.rtbishop.look4sat.domain.utility.round
import java.util.concurrent.Executors
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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 keyBluetoothAddress = "bluetoothAddress"
private val keyBluetoothName = "bluetoothName" private val keyBluetoothName = "bluetoothName"
@@ -118,31 +121,30 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
//region # Station position settings //region # Station position settings
private val _stationPosition = MutableStateFlow(getStationPosition()) private val _stationPosition = MutableStateFlow(getStationPosition())
private val executor = Executors.newSingleThreadExecutor()
private val providerDef = LocationManager.PASSIVE_PROVIDER private val providerDef = LocationManager.PASSIVE_PROVIDER
private val providerGps = LocationManager.GPS_PROVIDER private val providerGps = LocationManager.GPS_PROVIDER
private val providerNet = LocationManager.NETWORK_PROVIDER private val providerNet = LocationManager.NETWORK_PROVIDER
private val timeoutSignal = CancellationSignal().apply {
setOnCancelListener { _stationPosition.value = getStationPosition() }
}
override val stationPosition: StateFlow<GeoPos> = _stationPosition 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 newLongitude = if (longitude > 180.0) longitude - 180 else longitude
val locator = positionToQth(latitude, newLongitude) ?: return false val locator = positionToQth(latitude, newLongitude) ?: return false
setStationPosition(latitude, newLongitude, altitude, locator) setStationPosition(latitude, newLongitude, altitude, locator)
return true return true
} }
override fun setStationPositionGps(): Boolean { override fun setStationPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(manager)) return false if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
try { try {
val hasProviderGps = LocationManagerCompat.hasProvider(manager, providerGps) val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
val hasProviderNet = LocationManagerCompat.hasProvider(manager, providerNet) val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet)
val provider = if (hasProviderGps) providerGps else if (hasProviderNet) providerNet else providerDef val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef
println("Requesting location for $provider provider") val location = locationManager.getLastKnownLocation(providerDef)
LocationManagerCompat.getCurrentLocation(manager, provider, timeoutSignal, executor) { if (location == null || System.currentTimeMillis() - location.time > 600_000L) {
it?.let { setStationPositionGeo(it.latitude, it.longitude, it.altitude) } println("Requesting location for $provider provider")
locationManager.requestLocationUpdates(provider, 0L, 0f, this)
} else {
setStationPosition(location.latitude, location.longitude, location.altitude)
} }
} catch (exception: SecurityException) { } catch (exception: SecurityException) {
println("No permissions were given - $exception") println("No permissions were given - $exception")
@@ -150,7 +152,7 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
return true return true
} }
override fun setStationPositionQth(locator: String): Boolean { override fun setStationPosition(locator: String): Boolean {
val position = qthToPosition(locator) ?: return false val position = qthToPosition(locator) ?: return false
setStationPosition(position.latitude, position.longitude, 0.0, locator) setStationPosition(position.latitude, position.longitude, 0.0, locator)
return true 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 getBluetoothAddress(): String = preferences.getString(keyBluetoothAddress, null) ?: "00:0C:BF:13:80:5D"
override fun setBluetoothAddress(value: String) = preferences.edit { putString(keyBluetoothAddress, value) } 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 setBluetoothFormat(value: String) = preferences.edit { putString(keyBluetoothFormat, value) }
override fun getBluetoothName(): String = preferences.getString(keyBluetoothName, null) ?: "Default" 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 getRotatorState(): Boolean = preferences.getBoolean(keyRotatorState, false)
override fun setRotatorState(value: Boolean) = preferences.edit { putBoolean(keyRotatorState, value) } override fun setRotatorState(value: Boolean) = preferences.edit { putBoolean(keyRotatorState, value) }
override fun onLocationChanged(location: Location) {
setStationPosition(location.latitude, location.longitude, location.altitude)
}
//endregion //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 //region # Station position settings
val stationPosition: StateFlow<GeoPos> val stationPosition: StateFlow<GeoPos>
fun setStationPositionGeo(latitude: Double, longitude: Double, altitude: Double): Boolean fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
fun setStationPositionGps(): Boolean fun setStationPosition(): Boolean
fun setStationPositionQth(locator: String): Boolean fun setStationPosition(locator: String): Boolean
//endregion //endregion
//region # Database update settings //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] [versions]
android-gradle-plugin = "8.13.0" android-gradle-plugin = "8.13.1"
google-ksp = "2.3.0" google-ksp = "2.3.0"
kotlin = "2.2.21" kotlin = "2.2.21"
androidx-core-ktx = "1.17.0" androidx-core-ktx = "1.17.0"
androidx-core-splashscreen = "1.0.1" androidx-core-splashscreen = "1.2.0"
androidx-room = "2.8.3" androidx-room = "2.8.3"
compose-bom = "2025.10.01" compose-bom = "2025.11.00"
compose-activity = "1.11.0" compose-activity = "1.11.0"
compose-lifecycle = "2.9.4" compose-lifecycle = "2.9.4"
compose-navigation = "2.9.5" compose-navigation = "2.9.6"
other-coroutines = "1.10.2" other-coroutines = "1.10.2"
other-json = "20250517" other-json = "20250517"