mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Added the ability to fully customize data sources via import
This commit is contained in:
1 parent
24eebdef74
commit
2f4e3f5802
12 files changed
+176
-199
No files matched your search
@@ -23,7 +23,6 @@ import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
|||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
|
||||||
import com.rtbishop.look4sat.core.domain.utility.DataParser
|
import com.rtbishop.look4sat.core.domain.utility.DataParser
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -40,7 +39,6 @@ class DatabaseRepo(
|
|||||||
private val settingsRepo: ISettingsRepo
|
private val settingsRepo: ISettingsRepo
|
||||||
) : IDatabaseRepo {
|
) : IDatabaseRepo {
|
||||||
|
|
||||||
private val customSourceType = "Other"
|
|
||||||
|
|
||||||
override suspend fun updateTLEFromFile(uri: String): Int = withContext(dispatcher) {
|
override suspend fun updateTLEFromFile(uri: String): Int = withContext(dispatcher) {
|
||||||
var importedCount = 0
|
var importedCount = 0
|
||||||
@@ -65,18 +63,13 @@ class DatabaseRepo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateFromRemote() = withContext(dispatcher) {
|
override suspend fun updateFromRemote() = withContext(dispatcher) {
|
||||||
val dataSourcesSettings = settingsRepo.dataSourcesSettings.value
|
val settings = settingsRepo.dataSourcesSettings.value
|
||||||
val tleUrls = buildMap {
|
fun normalizeUrl(url: String) = if (url.startsWith("http")) url else "https://$url"
|
||||||
putAll(Sources.satelliteDataUrls)
|
val tleUrls = settings.satelliteUrls.filter { it.isNotBlank() }.map(::normalizeUrl)
|
||||||
if (dataSourcesSettings.useCustomTLE) put(customSourceType, dataSourcesSettings.tleUrl)
|
val radioUrls = settings.transceiversUrls.filter { it.isNotBlank() }.map(::normalizeUrl)
|
||||||
}.filterValues { it.isNotBlank() }
|
|
||||||
val radioUrls = buildMap {
|
|
||||||
putAll(Sources.transceiversDataUrls)
|
|
||||||
if (dataSourcesSettings.useCustomTransceivers) put(customSourceType, dataSourcesSettings.transceiversUrl)
|
|
||||||
}.filterValues { it.isNotBlank() }
|
|
||||||
// launch all network requests concurrently
|
// launch all network requests concurrently
|
||||||
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
val tleJobs = tleUrls.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
||||||
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
val radioJobs = radioUrls.map { url -> async { url to remoteSource.getNetworkStream(url) } }
|
||||||
// parse fetched data concurrently
|
// parse fetched data concurrently
|
||||||
val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) ->
|
val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) ->
|
||||||
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty()
|
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty()
|
||||||
|
|||||||
+12
-12
@@ -31,6 +31,7 @@ import com.rtbishop.look4sat.core.domain.model.RCSettings
|
|||||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
|
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||||
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
|
||||||
import com.rtbishop.look4sat.core.domain.utility.round
|
import com.rtbishop.look4sat.core.domain.utility.round
|
||||||
@@ -89,11 +90,10 @@ class SettingsRepo(
|
|||||||
private val keyHighElevation = "highElevation"
|
private val keyHighElevation = "highElevation"
|
||||||
private val keyRadarCompassOffset = "radarCompassOffset"
|
private val keyRadarCompassOffset = "radarCompassOffset"
|
||||||
private val keyRadarCompassOffsetElev = "radarCompassOffsetElev"
|
private val keyRadarCompassOffsetElev = "radarCompassOffsetElev"
|
||||||
private val keyUseCustomTle = "useCustomTle"
|
private val keySatelliteUrls = "satelliteUrls"
|
||||||
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
private val keyTransceiversUrls = "transceiversUrls"
|
||||||
private val keyTleUrl = "tleUrl"
|
|
||||||
private val keyTransceiversUrl = "transceiversUrl"
|
|
||||||
private val separatorComma = ","
|
private val separatorComma = ","
|
||||||
|
private val separatorUrl = "\n"
|
||||||
|
|
||||||
//region # Satellites selection settings
|
//region # Satellites selection settings
|
||||||
private val _satelliteSelection = MutableStateFlow(getSelectedIds())
|
private val _satelliteSelection = MutableStateFlow(getSelectedIds())
|
||||||
@@ -365,19 +365,19 @@ class SettingsRepo(
|
|||||||
|
|
||||||
override fun updateDataSourcesSettings(settings: DataSourcesSettings) {
|
override fun updateDataSourcesSettings(settings: DataSourcesSettings) {
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putBoolean(keyUseCustomTle, settings.useCustomTLE)
|
putString(keySatelliteUrls, settings.satelliteUrls.joinToString(separatorUrl))
|
||||||
putBoolean(keyUseCustomTransceivers, settings.useCustomTransceivers)
|
putString(keyTransceiversUrls, settings.transceiversUrls.joinToString(separatorUrl))
|
||||||
putString(keyTleUrl, settings.tleUrl)
|
|
||||||
putString(keyTransceiversUrl, settings.transceiversUrl)
|
|
||||||
}
|
}
|
||||||
_dataSourcesSettings.value = settings
|
_dataSourcesSettings.value = settings
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
|
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
|
||||||
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
|
satelliteUrls = preferences.getString(keySatelliteUrls, null)
|
||||||
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
|
?.split(separatorUrl)?.filter { it.isNotBlank() }
|
||||||
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
|
?: Sources.satelliteDataUrls,
|
||||||
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
|
transceiversUrls = preferences.getString(keyTransceiversUrls, null)
|
||||||
|
?.split(separatorUrl)?.filter { it.isNotBlank() }
|
||||||
|
?: Sources.transceiversDataUrls
|
||||||
)
|
)
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|||||||
+4
-8
@@ -90,10 +90,8 @@ class DatabaseRepoTest {
|
|||||||
}
|
}
|
||||||
val settingsRepo = FakeSettingsRepo(
|
val settingsRepo = FakeSettingsRepo(
|
||||||
dataSources = DataSourcesSettings(
|
dataSources = DataSourcesSettings(
|
||||||
useCustomTLE = true,
|
satelliteUrls = listOf(customCsvUrl),
|
||||||
useCustomTransceivers = false,
|
transceiversUrls = emptyList()
|
||||||
tleUrl = customCsvUrl,
|
|
||||||
transceiversUrl = ""
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||||
@@ -221,9 +219,7 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
|||||||
|
|
||||||
private fun defaultDataSourcesSettings(): DataSourcesSettings {
|
private fun defaultDataSourcesSettings(): DataSourcesSettings {
|
||||||
return DataSourcesSettings(
|
return DataSourcesSettings(
|
||||||
useCustomTLE = false,
|
satelliteUrls = emptyList(),
|
||||||
useCustomTransceivers = false,
|
transceiversUrls = emptyList()
|
||||||
tleUrl = "",
|
|
||||||
transceiversUrl = ""
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -135,7 +135,7 @@ class SelectionRepoTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = MutableStateFlow(
|
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = MutableStateFlow(
|
||||||
DataSourcesSettings(false, false, "", "")
|
DataSourcesSettings(emptyList(), emptyList())
|
||||||
)
|
)
|
||||||
|
|
||||||
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
|
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
|
||||||
|
|||||||
@@ -67,10 +67,8 @@ data class OtherSettings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class DataSourcesSettings(
|
data class DataSourcesSettings(
|
||||||
val useCustomTLE: Boolean,
|
val satelliteUrls: List<String>,
|
||||||
val useCustomTransceivers: Boolean,
|
val transceiversUrls: List<String>
|
||||||
val tleUrl: String,
|
|
||||||
val transceiversUrl: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RadioControlSettings(
|
data class RadioControlSettings(
|
||||||
|
|||||||
@@ -18,18 +18,17 @@
|
|||||||
package com.rtbishop.look4sat.core.domain.source
|
package com.rtbishop.look4sat.core.domain.source
|
||||||
|
|
||||||
object Sources {
|
object Sources {
|
||||||
val satelliteDataUrls = mapOf(
|
val satelliteDataUrls = listOf(
|
||||||
"CelesTrak" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
"celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||||
"AMSAT" to "https://amsat.org/tle/current/nasabare.txt",
|
"db.satnogs.org/api/tle/?format=3le",
|
||||||
"McCants Classified" to "https://www.mmccants.org/tles/classfd.zip",
|
"amsat.org/tle/current/nasabare.txt",
|
||||||
"McCants Integrated" to "https://www.mmccants.org/tles/inttles.zip",
|
"mmccants.org/tles/classfd.zip",
|
||||||
"R4UAB" to "https://r4uab.ru/satonline.txt",
|
"r4uab.ru/satonline.txt",
|
||||||
"SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le",
|
"live.ariss.org/iss.txt"
|
||||||
"ARISS" to "https://live.ariss.org/iss.txt",
|
|
||||||
"Other" to "" // key for sats filter
|
|
||||||
)
|
)
|
||||||
val transceiversDataUrls = mapOf(
|
val transceiversDataUrls = listOf(
|
||||||
"SatNOGS" to "https://db.satnogs.org/api/transmitters/?format=json&status=active"
|
"db.satnogs.org/api/transmitters/?format=json&status=active",
|
||||||
|
"r4uab.ru/transmitters.json"
|
||||||
)
|
)
|
||||||
val satelliteModes = listOf(
|
val satelliteModes = listOf(
|
||||||
"4FSK", "64-QAM", "AFSK", "AFSK TUBiX10", "AHRPT", "AM", "APT", "ASK", "BPSK",
|
"4FSK", "64-QAM", "AFSK", "AFSK TUBiX10", "AHRPT", "AM", "APT", "ASK", "BPSK",
|
||||||
|
|||||||
+7
-2
@@ -239,8 +239,13 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IconCard(action: () -> Unit, resId: Int, modifier: Modifier = Modifier, enabled: Boolean = true) {
|
fun IconCard(
|
||||||
ElevatedCard(modifier = Modifier.size(48.dp), enabled = enabled, onClick = action) {
|
action: () -> Unit, resId: Int, modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true, containerColor: Color = Color.Unspecified
|
||||||
|
) {
|
||||||
|
val colors = if (containerColor == Color.Unspecified) CardDefaults.elevatedCardColors()
|
||||||
|
else CardDefaults.elevatedCardColors(containerColor = containerColor)
|
||||||
|
ElevatedCard(modifier = Modifier.size(48.dp), enabled = enabled, onClick = action, colors = colors) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Icon(painter = painterResource(resId), contentDescription = null, modifier = modifier)
|
Icon(painter = painterResource(resId), contentDescription = null, modifier = modifier)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z" />
|
||||||
|
</vector>
|
||||||
@@ -172,10 +172,7 @@
|
|||||||
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
|
||||||
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
|
||||||
|
|
||||||
<string name="prefs_data_sources_title">Custom sources</string>
|
<string name="prefs_data_sources_title">Data sources</string>
|
||||||
<string name="prefs_data_sources_tle_switch">TLE URL</string>
|
|
||||||
<string name="prefs_data_sources_transceivers_switch">Transceivers URL</string>
|
|
||||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
|
||||||
|
|
||||||
<string name="prefs_data_output_title">Data output</string>
|
<string name="prefs_data_output_title">Data output</string>
|
||||||
<string name="prefs_net_output">Network</string>
|
<string name="prefs_net_output">Network</string>
|
||||||
|
|||||||
+113
-88
@@ -17,26 +17,39 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.feature.settings
|
package com.rtbishop.look4sat.feature.settings
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
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.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
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.rotate
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -46,6 +59,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||||
|
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||||
import com.rtbishop.look4sat.core.presentation.LocalSpacing
|
import com.rtbishop.look4sat.core.presentation.LocalSpacing
|
||||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||||
import com.rtbishop.look4sat.core.presentation.R
|
import com.rtbishop.look4sat.core.presentation.R
|
||||||
@@ -129,130 +143,141 @@ fun LocatorDialog(qthLocator: String, dismiss: () -> Unit, save: (String) -> Uni
|
|||||||
private fun TransceiversDialogPreview() {
|
private fun TransceiversDialogPreview() {
|
||||||
MainTheme {
|
MainTheme {
|
||||||
DataSourcesDialog(
|
DataSourcesDialog(
|
||||||
useCustomTle = true,
|
satelliteUrls = listOf(
|
||||||
useCustomTransceivers = true,
|
"celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||||
tleUrl = "https://example.com/tle.txt",
|
"amsat.org/tle/current/nasabare.txt"
|
||||||
transceiversUrl = "https://example.com/tx.json",
|
),
|
||||||
requestCustomSourcesPermission = { onGranted, _ -> onGranted() },
|
transceiversUrls = listOf(
|
||||||
|
"db.satnogs.org/api/transmitters/?format=json&status=active"
|
||||||
|
),
|
||||||
onImportTle = {},
|
onImportTle = {},
|
||||||
onImportTransceivers = {},
|
onImportTransceivers = {},
|
||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
onSave = { _, _, _, _ -> }
|
onSave = { _, _ -> }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DataSourcesDialog(
|
fun DataSourcesDialog(
|
||||||
useCustomTle: Boolean,
|
satelliteUrls: List<String>,
|
||||||
useCustomTransceivers: Boolean,
|
transceiversUrls: List<String>,
|
||||||
tleUrl: String,
|
|
||||||
transceiversUrl: String,
|
|
||||||
requestCustomSourcesPermission: (onGranted: () -> Unit, onDenied: () -> Unit) -> Unit,
|
|
||||||
onImportTle: () -> Unit,
|
onImportTle: () -> Unit,
|
||||||
onImportTransceivers: () -> Unit,
|
onImportTransceivers: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSave: (Boolean, Boolean, String, String) -> Unit
|
onSave: (List<String>, List<String>) -> Unit
|
||||||
) {
|
) {
|
||||||
val padding = LocalSpacing.current.large
|
val padding = LocalSpacing.current.large
|
||||||
val isEnabledCustomTle = rememberSaveable { mutableStateOf(useCustomTle) }
|
// Use stable Long IDs to avoid key collisions (e.g. multiple empty "" entries).
|
||||||
val isEnabledCustomTransceivers = rememberSaveable { mutableStateOf(useCustomTransceivers) }
|
val nextId = remember { mutableLongStateOf((satelliteUrls.size + transceiversUrls.size).toLong()) }
|
||||||
val urlTle = rememberSaveable { mutableStateOf(tleUrl) }
|
val satUrls = remember {
|
||||||
val urlTransceivers = rememberSaveable { mutableStateOf(transceiversUrl) }
|
satelliteUrls.mapIndexed { i, url -> i.toLong() to url }.toMutableStateList()
|
||||||
val onAccept = {
|
|
||||||
onSave(
|
|
||||||
isEnabledCustomTle.value,
|
|
||||||
isEnabledCustomTransceivers.value,
|
|
||||||
urlTle.value,
|
|
||||||
urlTransceivers.value
|
|
||||||
)
|
|
||||||
onDismiss()
|
|
||||||
}
|
}
|
||||||
val onCancel = { onDismiss() }
|
val txUrls = remember {
|
||||||
|
transceiversUrls.mapIndexed { i, url -> (satelliteUrls.size + i).toLong() to url }.toMutableStateList()
|
||||||
|
}
|
||||||
|
val onAccept = { onSave(satUrls.map { it.second }, txUrls.map { it.second }); onDismiss() }
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
title = stringResource(id = R.string.prefs_data_sources_title),
|
title = stringResource(id = R.string.prefs_data_sources_title),
|
||||||
onCancel = onCancel,
|
onCancel = onDismiss,
|
||||||
onAccept = onAccept,
|
onAccept = onAccept,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = padding)) {
|
LazyColumn(
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
modifier = Modifier
|
||||||
|
.fillMaxHeight(0.84f)
|
||||||
|
.padding(horizontal = padding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = {
|
onClick = { onImportTle(); onDismiss() },
|
||||||
onImportTle()
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
text = "TLE/3LE (.txt)\nOMM (.csv)",
|
text = "TLE/3LE (.txt)\nOMM (.csv)",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
CardButton(
|
CardButton(
|
||||||
onClick = {
|
onClick = { onImportTransceivers(); onDismiss() },
|
||||||
onImportTransceivers()
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
text = "Transceivers\nSatNOGS (.json)",
|
text = "Transceivers\nSatNOGS (.json)",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
}
|
||||||
Row(
|
sourceSection(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
sectionKey = "sat",
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
label = "Satellites data",
|
||||||
modifier = Modifier.fillMaxWidth()
|
urls = satUrls,
|
||||||
|
onAdd = { satUrls.add(nextId.longValue++ to "") },
|
||||||
|
onMoveUp = { i -> if (i > 0) satUrls.add(i - 1, satUrls.removeAt(i)) },
|
||||||
|
onRemove = { i -> satUrls.removeAt(i) },
|
||||||
|
onUrlChange = { i, v -> satUrls[i] = satUrls[i].first to v }
|
||||||
|
)
|
||||||
|
sourceSection(
|
||||||
|
sectionKey = "tx",
|
||||||
|
label = "Transceivers data",
|
||||||
|
urls = txUrls,
|
||||||
|
onAdd = { txUrls.add(nextId.longValue++ to "") },
|
||||||
|
onMoveUp = { i -> if (i > 0) txUrls.add(i - 1, txUrls.removeAt(i)) },
|
||||||
|
onRemove = { i -> txUrls.removeAt(i) },
|
||||||
|
onUrlChange = { i, v -> txUrls[i] = txUrls[i].first to v }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.sourceSection(
|
||||||
|
sectionKey: String,
|
||||||
|
label: String,
|
||||||
|
urls: List<Pair<Long, String>>,
|
||||||
|
onAdd: () -> Unit,
|
||||||
|
onMoveUp: (Int) -> Unit,
|
||||||
|
onRemove: (Int) -> Unit,
|
||||||
|
onUrlChange: (Int, String) -> Unit
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.prefs_data_sources_tle_switch))
|
item {
|
||||||
Switch(
|
|
||||||
checked = isEnabledCustomTle.value,
|
|
||||||
onCheckedChange = { enabled ->
|
|
||||||
if (!enabled) {
|
|
||||||
isEnabledCustomTle.value = false
|
|
||||||
} else {
|
|
||||||
requestCustomSourcesPermission(
|
|
||||||
{ isEnabledCustomTle.value = true },
|
|
||||||
{ isEnabledCustomTle.value = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
value = urlTle.value,
|
|
||||||
onValueChange = { urlTle.value = it },
|
|
||||||
label = { Text(text = stringResource(id = R.string.prefs_data_sources_url_title)) },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
enabled = isEnabledCustomTle.value,
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Row(
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
text = label,
|
||||||
modifier = Modifier.fillMaxWidth()
|
fontWeight = FontWeight.Medium,
|
||||||
) {
|
color = MaterialTheme.colorScheme.primary,
|
||||||
Text(text = stringResource(id = R.string.prefs_data_sources_transceivers_switch))
|
modifier = Modifier.weight(1f)
|
||||||
Switch(
|
|
||||||
checked = isEnabledCustomTransceivers.value,
|
|
||||||
onCheckedChange = { enabled ->
|
|
||||||
if (!enabled) {
|
|
||||||
isEnabledCustomTransceivers.value = false
|
|
||||||
} else {
|
|
||||||
requestCustomSourcesPermission(
|
|
||||||
{ isEnabledCustomTransceivers.value = true },
|
|
||||||
{ isEnabledCustomTransceivers.value = false }
|
|
||||||
)
|
)
|
||||||
|
IconCard(action = onAdd, resId = R.drawable.ic_add, containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
itemsIndexed(urls, key = { _, entry -> "$sectionKey-${entry.first}" }) { index, (_, url) ->
|
||||||
}
|
val enabledTint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = urlTransceivers.value,
|
value = url,
|
||||||
onValueChange = { urlTransceivers.value = it },
|
onValueChange = { onUrlChange(index, it) },
|
||||||
label = { Text(text = stringResource(id = R.string.prefs_data_sources_url_title)) },
|
label = { Text("Source URL") },
|
||||||
singleLine = true,
|
leadingIcon = {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
IconButton(onClick = { onMoveUp(index) }) {
|
||||||
enabled = isEnabledCustomTransceivers.value,
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (index > 0) enabledTint else enabledTint.copy(alpha = 0.32f),
|
||||||
|
modifier = Modifier.rotate(270f)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { onRemove(index) }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_delete),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateItem(fadeInSpec = spring(), fadeOutSpec = spring())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-54
@@ -64,6 +64,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||||
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||||
@@ -93,22 +94,10 @@ fun SettingsDestination() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) {
|
private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> Unit) {
|
||||||
val dialogs = rememberDialogVisibility()
|
val dialogs = rememberDialogVisibility()
|
||||||
val pendingCustomSourcesGrant = remember { mutableStateOf<(() -> Unit)?>(null) }
|
|
||||||
val pendingCustomSourcesDeny = remember { mutableStateOf<(() -> Unit)?>(null) }
|
|
||||||
val permissions = rememberSettingsPermissions(
|
val permissions = rememberSettingsPermissions(
|
||||||
sendAction = onAction,
|
sendAction = onAction,
|
||||||
onBluetoothGranted = { dialogs.bluetooth = true },
|
onBluetoothGranted = { dialogs.bluetooth = true },
|
||||||
onNetworkGranted = { dialogs.network = true },
|
onNetworkGranted = { dialogs.network = true }
|
||||||
onCustomSourcesPermissionGranted = {
|
|
||||||
pendingCustomSourcesGrant.value?.invoke()
|
|
||||||
pendingCustomSourcesGrant.value = null
|
|
||||||
pendingCustomSourcesDeny.value = null
|
|
||||||
},
|
|
||||||
onCustomSourcesPermissionDenied = {
|
|
||||||
pendingCustomSourcesDeny.value?.invoke()
|
|
||||||
pendingCustomSourcesGrant.value = null
|
|
||||||
pendingCustomSourcesDeny.value = null
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
@@ -129,31 +118,16 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
|||||||
}
|
}
|
||||||
if (dialogs.dataSources) {
|
if (dialogs.dataSources) {
|
||||||
DataSourcesDialog(
|
DataSourcesDialog(
|
||||||
useCustomTle = uiState.dataSourcesSettings.useCustomTLE,
|
satelliteUrls = uiState.dataSourcesSettings.satelliteUrls,
|
||||||
useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers,
|
transceiversUrls = uiState.dataSourcesSettings.transceiversUrls,
|
||||||
tleUrl = uiState.dataSourcesSettings.tleUrl,
|
|
||||||
transceiversUrl = uiState.dataSourcesSettings.transceiversUrl,
|
|
||||||
requestCustomSourcesPermission = { onGranted, onDenied ->
|
|
||||||
pendingCustomSourcesGrant.value = onGranted
|
|
||||||
pendingCustomSourcesDeny.value = onDenied
|
|
||||||
permissions.launchCustomSourcesPermission()
|
|
||||||
},
|
|
||||||
onImportTle = { permissions.launchTleImport(); dialogs.dataSources = false },
|
onImportTle = { permissions.launchTleImport(); dialogs.dataSources = false },
|
||||||
onImportTransceivers = { permissions.launchTransceiverImport(); dialogs.dataSources = false },
|
onImportTransceivers = { permissions.launchTransceiverImport(); dialogs.dataSources = false },
|
||||||
onDismiss = { dialogs.dataSources = false },
|
onDismiss = { dialogs.dataSources = false },
|
||||||
onSave = { useCustomTle, useCustomTransceivers, tleUrl, transceiversUrl ->
|
onSave = { satUrls, txUrls ->
|
||||||
val current = uiState.dataSourcesSettings
|
val newSettings = DataSourcesSettings(satelliteUrls = satUrls, transceiversUrls = txUrls)
|
||||||
val newSettings = current.copy(
|
if (newSettings != uiState.dataSourcesSettings) onAction(SettingsAction.UpdateDataSources(newSettings))
|
||||||
useCustomTLE = if (!useCustomTle || tleUrl.isNotBlank()) useCustomTle else current.useCustomTLE,
|
|
||||||
tleUrl = if (!useCustomTle || tleUrl.isNotBlank()) tleUrl else current.tleUrl,
|
|
||||||
useCustomTransceivers = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) useCustomTransceivers else current.useCustomTransceivers,
|
|
||||||
transceiversUrl = if (!useCustomTransceivers || transceiversUrl.isNotBlank()) transceiversUrl else current.transceiversUrl
|
|
||||||
)
|
|
||||||
if (newSettings != current) onAction(SettingsAction.UpdateDataSources(newSettings))
|
|
||||||
if (newSettings.useCustomTLE || newSettings.useCustomTransceivers) {
|
|
||||||
onAction(SettingsAction.UpdateFromWeb)
|
onAction(SettingsAction.UpdateFromWeb)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (dialogs.network) {
|
if (dialogs.network) {
|
||||||
@@ -752,17 +726,14 @@ private class SettingsPermissions(
|
|||||||
val launchTleImport: () -> Unit,
|
val launchTleImport: () -> Unit,
|
||||||
val launchTransceiverImport: () -> Unit,
|
val launchTransceiverImport: () -> Unit,
|
||||||
val launchBluetooth: () -> Unit,
|
val launchBluetooth: () -> Unit,
|
||||||
val launchNetwork: () -> Unit,
|
val launchNetwork: () -> Unit
|
||||||
val launchCustomSourcesPermission: () -> Unit
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberSettingsPermissions(
|
private fun rememberSettingsPermissions(
|
||||||
sendAction: (SettingsAction) -> Unit,
|
sendAction: (SettingsAction) -> Unit,
|
||||||
onBluetoothGranted: () -> Unit,
|
onBluetoothGranted: () -> Unit,
|
||||||
onNetworkGranted: () -> Unit,
|
onNetworkGranted: () -> Unit
|
||||||
onCustomSourcesPermissionGranted: () -> Unit,
|
|
||||||
onCustomSourcesPermissionDenied: () -> Unit
|
|
||||||
): SettingsPermissions {
|
): SettingsPermissions {
|
||||||
val locationError = stringResource(R.string.prefs_loc_gps_error)
|
val locationError = stringResource(R.string.prefs_loc_gps_error)
|
||||||
val locationRequest = rememberLauncherForActivityResult(
|
val locationRequest = rememberLauncherForActivityResult(
|
||||||
@@ -799,15 +770,6 @@ private fun rememberSettingsPermissions(
|
|||||||
else sendAction(SettingsAction.ShowToast(networkError))
|
else sendAction(SettingsAction.ShowToast(networkError))
|
||||||
}
|
}
|
||||||
|
|
||||||
val customSourcesRequest = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
|
||||||
if (granted) {
|
|
||||||
onCustomSourcesPermissionGranted()
|
|
||||||
} else {
|
|
||||||
onCustomSourcesPermissionDenied()
|
|
||||||
sendAction(SettingsAction.ShowToast(networkError))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return remember {
|
return remember {
|
||||||
SettingsPermissions(
|
SettingsPermissions(
|
||||||
launchLocation = {
|
launchLocation = {
|
||||||
@@ -824,13 +786,6 @@ private fun rememberSettingsPermissions(
|
|||||||
} else {
|
} else {
|
||||||
onNetworkGranted()
|
onNetworkGranted()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
launchCustomSourcesPermission = {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
|
|
||||||
customSourcesRequest.launch(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
|
||||||
} else {
|
|
||||||
onCustomSourcesPermissionGranted()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
#Sat Jun 27 12:51:12 BST 2026
|
#Tue Aug 11 14:16:53 CEST 2026
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
Reference in new issue
Block a user