diff --git a/app/build.gradle b/app/build.gradle index d769971a..cb124633 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,9 +6,8 @@ plugins { } android { - compileSdkVersion 33 - buildToolsVersion '33.0.0' namespace 'com.rtbishop.look4sat' + compileSdk 33 defaultConfig { applicationId "com.rtbishop.look4sat" @@ -21,7 +20,6 @@ android { vectorDrawables { useSupportLibrary true } - kapt { arguments { arg("room.schemaLocation", "$projectDir/schemas") @@ -31,6 +29,9 @@ android { correctErrorTypes true } } + buildFeatures { + compose true + } buildTypes { release { minifyEnabled true @@ -38,29 +39,25 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - buildFeatures { - compose true - viewBinding true - } - composeOptions { - kotlinCompilerExtensionVersion compose_compiler_version - } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } + composeOptions { + kotlinCompilerExtensionVersion compose_compiler_version + } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } - testOptions { - animationsDisabled true - unitTests.includeAndroidResources true - } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } + testOptions { + animationsDisabled true + unitTests.includeAndroidResources true + } } kotlin { @@ -69,43 +66,34 @@ kotlin { dependencies { implementation project(":base") - implementation "androidx.core:core-ktx:$core_ktx_version" - implementation "androidx.core:core-splashscreen:$core_splashscreen_version" - implementation "androidx.constraintlayout:constraintlayout:$constraint_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" - implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" - implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" + // Android + implementation "androidx.core:core-splashscreen:$splashscreen_version" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" - implementation "com.google.dagger:hilt-android:$hilt_version" - kapt "com.google.dagger:hilt-compiler:$hilt_version" - implementation "com.jakewharton.timber:timber:$timber_version" - implementation "org.osmdroid:osmdroid-android:$osmdroid_version" - + implementation "com.google.dagger:hilt-android:$hilt_android_version" + kapt "com.google.dagger:hilt-compiler:$hilt_android_version" + // Compose implementation "androidx.activity:activity-compose:$activity_compose_version" implementation "androidx.compose.animation:animation:$compose_version" implementation "androidx.compose.compiler:compiler:$compose_compiler_version" - implementation "androidx.compose.material:material:$material_version" implementation "androidx.compose.material3:material3:$material3_version" implementation "androidx.compose.runtime:runtime:$compose_version" - implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "androidx.compose.ui:ui:$compose_version" - implementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.hilt:hilt-navigation-compose:$hilt_compose_version" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" implementation "androidx.navigation:navigation-compose:$navigation_version" - + // Utility + implementation "com.jakewharton.timber:timber:$timber_version" + implementation "org.osmdroid:osmdroid-android:$osmdroid_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version" - + // Test testImplementation "junit:junit:$junit_version" testImplementation "org.mockito:mockito-core:$mockito_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version" androidTestImplementation "org.mockito:mockito-android:$mockito_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c95173dc..45364ace 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -12,13 +13,16 @@ + android:theme="@style/Theme.Look4Sat.Main" + tools:targetApi="31"> - \ No newline at end of file + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index b31c292c..00000000 Binary files a/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt index 6834655a..6020c3ec 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/MainActivity.kt @@ -22,8 +22,6 @@ import android.content.Context import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Bundle -import android.os.SystemClock -import android.view.View import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -47,15 +45,3 @@ class MainActivity : ComponentActivity() { super.attachBaseContext(newBase) } } - -fun View.clickWithDebounce(debounceTime: Long = 875L, action: () -> Unit) { - this.setOnClickListener(object : View.OnClickListener { - private var lastClickTime: Long = 0 - - override fun onClick(v: View) { - if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return - else action() - lastClickTime = SystemClock.elapsedRealtime() - } - }) -} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt index b51e6aa0..4c63e4d7 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -40,7 +40,7 @@ fun EntriesScreen( viewModel: EntriesViewModel = hiltViewModel(), passesViewModel: PassesViewModel = hiltViewModel() ) { - val state = viewModel.satData.observeAsState(DataState.Loading) + val state = viewModel.satData.collectAsState(initial = DataState.Loading) val showDialog = rememberSaveable { mutableStateOf(false) } val toggleDialog = { showDialog.value = showDialog.value.not() } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt index ab2538ea..92781e5f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesViewModel.kt @@ -17,50 +17,59 @@ */ package com.rtbishop.look4sat.presentation.entriesScreen -import androidx.lifecycle.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISettingsManager import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.SatItem import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import java.util.* import javax.inject.Inject +@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class EntriesViewModel @Inject constructor( - private val repository: IDataRepository, - private val settings: ISettingsManager + private val repository: IDataRepository, private val settings: ISettingsManager ) : ViewModel() { - private val satType = MutableLiveData("All") + private val satType = MutableStateFlow("All") // private val transModes = MutableLiveData(settings.loadModesSelection()) - private val currentQuery = MutableLiveData(String()) - private val itemsFromRepo = liveData { - delay(250) - emit(loadEntriesWithSelection()) - } as MutableLiveData - private val itemsWithType = satType.switchMap { type -> + private val currentQuery = MutableStateFlow(String()) + private val itemsFromRepo = MutableStateFlow>(emptyList()) + private val itemsWithType = satType.flatMapLatest { type -> itemsFromRepo.map { items -> filterByType(items, type) } } // private val itemsWithModes = transModes.switchMap { modes -> // itemsWithType.map { items -> filterByModes(items, modes) } // } - private val itemsWithQuery = currentQuery.switchMap { query -> + private val itemsWithQuery = currentQuery.flatMapLatest { query -> itemsWithType.map { items -> filterByQuery(items, query) } } val satData = itemsWithQuery.map { items -> DataState.Success(items) } val satTypes: List = settings.sourcesMap.keys.sorted() - fun getSatType() = satType.value ?: "All" + init { + viewModelScope.launch { + delay(250) + itemsFromRepo.value = loadEntriesWithSelection() + } + } + + fun getSatType() = satType.value fun setSatType(type: String) { satType.value = type } fun selectCurrentItems(selectAll: Boolean) { - itemsWithQuery.value?.let { itemsWithQuery -> - updateSelection(itemsWithQuery.map { item -> item.catnum }, selectAll) + viewModelScope.launch { + val lastValue = itemsWithQuery.last() + updateSelection(lastValue.map { item -> item.catnum }, selectAll) } } @@ -74,13 +83,13 @@ class EntriesViewModel @Inject constructor( } fun saveSelection(): List { - return itemsFromRepo.value?.let { itemsAll -> + return itemsFromRepo.value.let { itemsAll -> itemsAll.filter { item -> item.isSelected }.map { item -> item.catnum } - } ?: emptyList() + } } fun updateSelection(catNums: List, isSelected: Boolean) { - itemsFromRepo.value?.let { itemsAll -> + itemsFromRepo.value.let { itemsAll -> val copiedList = itemsAll.map { item -> item.copy() } catNums.forEach { catnum -> copiedList.find { item -> item.catnum == catnum }?.isSelected = isSelected diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapScreen.kt index 58d40993..ef8a5ff4 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapScreen.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ElevatedCard import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -63,10 +63,10 @@ fun MapScreen(viewModel: MapViewModel = hiltViewModel()) { val context = LocalContext.current Configuration.getInstance().load(context, viewModel.getPreferences()) viewModel.selectDefaultSatellite(-1) - val stationPos = viewModel.stationPos.observeAsState() - val positions = viewModel.positions.observeAsState() - val satTrack = viewModel.track.observeAsState() - val footprint = viewModel.footprint.observeAsState() + val stationPos = viewModel.stationPos.collectAsState(initial = null) + val positions = viewModel.positions.collectAsState(initial = null) + val satTrack = viewModel.track.collectAsState(initial = null) + val footprint = viewModel.footprint.collectAsState(initial = null) val positionClick = { satellite: Satellite -> viewModel.selectSatellite(satellite) } Timber.d("MapScreen recomposition") Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { @@ -195,8 +195,7 @@ private fun MapView(modifier: Modifier = Modifier, update: ((map: MapView) -> Un @Composable private fun rememberMapViewWithLifecycle(context: Context = LocalContext.current): MapView { - // add clipToOutline = true if needed - val mapView = remember { MapView(context).apply { id = R.id.osm_map_view } }.apply { + val mapView = remember { MapView(context) }.apply { setMultiTouchControls(true) setTileSource(TileSourceFactory.WIKIMEDIA) minZoomLevel = getMinZoom(resources.displayMetrics.heightPixels) + 0.25 diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapViewModel.kt index d549de04..7070d237 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/mapScreen/MapViewModel.kt @@ -18,7 +18,8 @@ package com.rtbishop.look4sat.presentation.mapScreen import android.content.SharedPreferences -import androidx.lifecycle.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.domain.ISettingsManager @@ -30,8 +31,12 @@ import com.rtbishop.look4sat.utility.toDegrees import com.rtbishop.look4sat.utility.toTimerString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.flow import java.util.* import javax.inject.Inject +import kotlin.collections.set import kotlin.math.max import kotlin.math.min @@ -50,23 +55,23 @@ class MapViewModel @Inject constructor( private var dataUpdateRate = 1000L private var selectedSatellite: Satellite? = null - val stationPos = liveData { + val stationPos = flow { val osmLat = clipLat(stationPosition.lat) val osmLon = clipLon(stationPosition.lon) emit(GeoPos(osmLat, osmLon)) } - private val _track = MutableLiveData>>() - val track: LiveData>> = _track + private val _track = MutableSharedFlow>>() + val track: SharedFlow>> = _track - private val _footprint = MutableLiveData() - val footprint: LiveData = _footprint + private val _footprint = MutableSharedFlow() + val footprint: SharedFlow = _footprint - private val _mapData = MutableLiveData() - val mapData: LiveData = this._mapData + private val _mapData = MutableSharedFlow() + val mapData: SharedFlow = _mapData - private val _positions = MutableLiveData>() - val positions: LiveData> = _positions + private val _positions = MutableSharedFlow>() + val positions: SharedFlow> = _positions fun getPreferences() = preferences @@ -144,7 +149,7 @@ class MapViewModel @Inject constructor( currentTrack.add(currentPosition) } satTracks.add(currentTrack) - _track.postValue(satTracks) + _track.emit(satTracks) } private suspend fun getPositions(satellites: List, pos: GeoPos, date: Date) { @@ -155,12 +160,12 @@ class MapViewModel @Inject constructor( val osmLon = clipLon(satPos.longitude.toDegrees()) positions[satellite] = GeoPos(osmLat, osmLon) } - _positions.postValue(positions) + _positions.emit(positions) } private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) { val satPos = satelliteManager.getPosition(satellite, pos, date.time) - _footprint.postValue(satPos) + _footprint.emit(satPos) } private suspend fun getSatData(sat: Satellite, pos: GeoPos, date: Date) { @@ -202,7 +207,7 @@ class MapViewModel @Inject constructor( phase, visibility ) - _mapData.postValue(satData) + _mapData.emit(satData) } private fun clipLat(latitude: Double): Double { diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt index b1a73c35..0bbcb6b2 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt @@ -1,5 +1,3 @@ -@file:OptIn(ExperimentalMaterialApi::class) - package com.rtbishop.look4sat.presentation.passesScreen import androidx.compose.foundation.ExperimentalFoundationApi @@ -8,14 +6,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.PullRefreshState -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -34,16 +27,19 @@ import com.rtbishop.look4sat.domain.predict.NearEarthSat import com.rtbishop.look4sat.domain.predict.OrbitalData import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.presentation.MainTheme +import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshIndicator +import com.rtbishop.look4sat.presentation.pullRefresh.PullRefreshState +import com.rtbishop.look4sat.presentation.pullRefresh.pullRefresh +import com.rtbishop.look4sat.presentation.pullRefresh.rememberPullRefreshState import java.text.SimpleDateFormat import java.util.* private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH) -@OptIn(ExperimentalMaterialApi::class) @Composable fun PassesScreen(navToRadar: () -> Unit, viewModel: PassesViewModel = hiltViewModel()) { - val state = viewModel.passes.observeAsState() - val timerText = viewModel.timerText.observeAsState() + val state = viewModel.passes.collectAsState(initial = null) + val timerText = viewModel.timerText.collectAsState(initial = null) val isRefreshing = state.value is DataState.Loading val refreshState = rememberPullRefreshState(refreshing = isRefreshing, onRefresh = { viewModel.calculatePasses() }) diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt index 21537f53..c18623e5 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt @@ -17,7 +17,8 @@ */ package com.rtbishop.look4sat.presentation.passesScreen -import androidx.lifecycle.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISatelliteManager import com.rtbishop.look4sat.domain.ISettingsManager @@ -26,6 +27,8 @@ import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.utility.toTimerString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @HiltViewModel @@ -35,13 +38,12 @@ class PassesViewModel @Inject constructor( private val settings: ISettingsManager ) : ViewModel() { - private val _passes = MutableLiveData>>() + private val _passes = MutableStateFlow>>(DataState.Loading) private val timerData = Triple("Next - Id:Null", "Name: Null", "00:00:00") - private val _timerText = MutableLiveData>() + private val _timerText = MutableStateFlow(timerData) private var passesProcessing: Job? = null - val entriesTotal: LiveData = repository.getEntriesTotal().asLiveData() - val passes: LiveData>> = _passes - val timerText: LiveData> = _timerText + val passes: StateFlow>> = _passes + val timerText: StateFlow?> = _timerText fun getHoursAhead() = settings.getHoursAhead() @@ -63,18 +65,18 @@ class PassesViewModel @Inject constructor( val name = nextPass.name val millisBeforeStart = nextPass.aosTime.minus(timeNow) val timerString = millisBeforeStart.toTimerString() - _timerText.postValue(Triple("Next - Id:$catNum", name, timerString)) + _timerText.emit(Triple("Next - Id:$catNum", name, timerString)) } catch (e: NoSuchElementException) { val lastPass = newPasses.last() val catNum = lastPass.catNum val name = lastPass.name val millisBeforeEnd = lastPass.losTime.minus(timeNow) val timerString = millisBeforeEnd.toTimerString() - _timerText.postValue(Triple("Next - Id:$catNum", name, timerString)) + _timerText.emit(Triple("Next - Id:$catNum", name, timerString)) } - } else _timerText.postValue(timerData) + } else _timerText.emit(timerData) - _passes.postValue(DataState.Success(newPasses)) + _passes.emit(DataState.Success(newPasses)) delay(1000) } } @@ -97,7 +99,7 @@ class PassesViewModel @Inject constructor( selection: List? = null ) { viewModelScope.launch { - _passes.postValue(DataState.Loading) + _passes.emit(DataState.Loading) // _timerText.postValue(timerDefaultText) passesProcessing?.cancelAndJoin() selection?.let { items -> settings.saveEntriesSelection(items) } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshIndicator.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshIndicator.kt new file mode 100644 index 00000000..398ca648 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshIndicator.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rtbishop.look4sat.presentation.pullRefresh + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +/** + * The default indicator for Compose pull-to-refresh, based on Android's SwipeRefreshLayout. + * @param refreshing A boolean representing whether a refresh is occurring. + * @param state The [PullRefreshState] which controls where and how the indicator will be drawn. + * @param modifier Modifiers for the indicator. + * @param backgroundColor The color of the indicator's background. + * @param contentColor The color of the indicator's arc and arrow. + * @param scale A boolean controlling whether the indicator's size scales with pull progress or not. + */ +@Composable +fun PullRefreshIndicator( + refreshing: Boolean, + state: PullRefreshState, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.surface, + contentColor: Color = contentColorFor(backgroundColor), + scale: Boolean = false +) { + val showElevation by remember(refreshing, state) { + derivedStateOf { refreshing || state.position > 0.5f } + } + + Surface( + modifier = modifier + .size(IndicatorSize) + .pullRefreshIndicatorTransform(state, scale), + shape = SpinnerShape, + color = backgroundColor, + shadowElevation = if (showElevation) Elevation else 0.dp, + ) { + Crossfade( + targetState = refreshing, animationSpec = tween(durationMillis = CrossfadeDurationMs) + ) { refreshing -> + Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + ) { + val spinnerSize = (ArcRadius + StrokeWidth).times(2) + + if (refreshing) { + CircularProgressIndicator( + color = contentColor, + strokeWidth = StrokeWidth, + modifier = Modifier.size(spinnerSize), + ) + } else { + CircularArrowIndicator(state, contentColor, Modifier.size(spinnerSize)) + } + } + } + } +} + +/** + * Modifier.size MUST be specified. + */ +@Composable +private fun CircularArrowIndicator( + state: PullRefreshState, + color: Color, + modifier: Modifier, +) { + val path = remember { Path().apply { fillType = PathFillType.EvenOdd } } + + Canvas(modifier.semantics { contentDescription = "Refreshing" }) { + val values = ArrowValues(state.progress) + + rotate(degrees = values.rotation) { + val arcRadius = ArcRadius.toPx() + StrokeWidth.toPx() / 2f + val arcBounds = Rect( + size.center.x - arcRadius, + size.center.y - arcRadius, + size.center.x + arcRadius, + size.center.y + arcRadius + ) + drawArc( + color = color, + alpha = values.alpha, + startAngle = values.startAngle, + sweepAngle = values.endAngle - values.startAngle, + useCenter = false, + topLeft = arcBounds.topLeft, + size = arcBounds.size, + style = Stroke( + width = StrokeWidth.toPx(), cap = StrokeCap.Square + ) + ) + drawArrow(path, arcBounds, color, values) + } + } +} + +@Immutable +private class ArrowValues( + val alpha: Float, + val rotation: Float, + val startAngle: Float, + val endAngle: Float, + val scale: Float +) + +private fun ArrowValues(progress: Float): ArrowValues { + // Discard first 40% of progress. Scale remaining progress to full range between 0 and 100%. + val adjustedPercent = max(min(1f, progress) - 0.4f, 0f) * 5 / 3 + // How far beyond the threshold pull has gone, as a percentage of the threshold. + val overshootPercent = abs(progress) - 1.0f + // Limit the overshoot to 200%. Linear between 0 and 200. + val linearTension = overshootPercent.coerceIn(0f, 2f) + // Non-linear tension. Increases with linearTension, but at a decreasing rate. + val tensionPercent = linearTension - linearTension.pow(2) / 4 + + // Calculations based on SwipeRefreshLayout specification. + val alpha = progress.coerceIn(0f, 1f) + val endTrim = adjustedPercent * MaxProgressArc + val rotation = (-0.25f + 0.4f * adjustedPercent + tensionPercent) * 0.5f + val startAngle = rotation * 360 + val endAngle = (rotation + endTrim) * 360 + val scale = min(1f, adjustedPercent) + + return ArrowValues(alpha, rotation, startAngle, endAngle, scale) +} + +private fun DrawScope.drawArrow(arrow: Path, bounds: Rect, color: Color, values: ArrowValues) { + arrow.reset() + arrow.moveTo(0f, 0f) // Move to left corner + arrow.lineTo(x = ArrowWidth.toPx() * values.scale, y = 0f) // Line to right corner + + // Line to tip of arrow + arrow.lineTo( + x = ArrowWidth.toPx() * values.scale / 2, y = ArrowHeight.toPx() * values.scale + ) + + val radius = min(bounds.width, bounds.height) / 2f + val inset = ArrowWidth.toPx() * values.scale / 2f + arrow.translate( + Offset( + x = radius + bounds.center.x - inset, y = bounds.center.y + StrokeWidth.toPx() / 2f + ) + ) + arrow.close() + rotate(degrees = values.endAngle) { + drawPath(path = arrow, color = color, alpha = values.alpha) + } +} + +private const val CrossfadeDurationMs = 100 +private const val MaxProgressArc = 0.8f + +private val IndicatorSize = 40.dp +private val SpinnerShape = CircleShape +private val ArcRadius = 7.5.dp +private val StrokeWidth = 2.5.dp +private val ArrowWidth = 10.dp +private val ArrowHeight = 5.dp +private val Elevation = 6.dp diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshModifier.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshModifier.kt new file mode 100644 index 00000000..353bf6b9 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshModifier.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rtbishop.look4sat.presentation.pullRefresh + +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.Drag +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.platform.inspectable +import androidx.compose.ui.unit.Velocity + +/** + * A modifier for translating the position and scaling the size of a pull-to-refresh indicator + * based on the given [PullRefreshState]. + * @param state The [PullRefreshState] which determines the position of the indicator. + * @param scale A boolean controlling whether the indicator's size scales with pull progress or not. + */ +fun Modifier.pullRefreshIndicatorTransform( + state: PullRefreshState, + scale: Boolean = false, +) = composed(inspectorInfo = debugInspectorInfo { + name = "pullRefreshIndicatorTransform" + properties["state"] = state + properties["scale"] = scale +}) { + var height by remember { mutableStateOf(0) } + + Modifier + .onSizeChanged { height = it.height } + .graphicsLayer { + translationY = state.position - height + + if (scale && !state.refreshing) { + val scaleFraction = LinearOutSlowInEasing + .transform(state.position / state.threshold) + .coerceIn(0f, 1f) + scaleX = scaleFraction + scaleY = scaleFraction + } + } +} + +/** + * PullRefresh modifier to be used in conjunction with [PullRefreshState]. Provides a connection + * to the nested scroll system. Based on Android's SwipeRefreshLayout. + * @param state The [PullRefreshState] associated with this pull-to-refresh component. + * The state will be updated by this modifier. + * @param enabled If not enabled, all scroll delta and fling velocity will be ignored. + */ +fun Modifier.pullRefresh( + state: PullRefreshState, enabled: Boolean = true +) = inspectable(inspectorInfo = debugInspectorInfo { + name = "pullRefresh" + properties["state"] = state + properties["enabled"] = enabled +}) { + Modifier.pullRefresh(state::onPull, { state.onRelease() }, enabled) +} + +/** + * A modifier for building pull-to-refresh components. Provides a connection to the nested scroll + * system. + * @param onPull Callback for dispatching vertical scroll delta, takes float pullDelta as argument. + * Positive delta (pulling down) is dispatched only if the child does not consume it (i.e. pulling + * down despite being at the top of a scrollable component), whereas negative delta (swiping up) is + * dispatched first (in case it is needed to push the indicator back up), and then whatever is not + * consumed is passed on to the child. + * @param onRelease Callback for when drag is released, takes float flingVelocity as argument. + * @param enabled If not enabled, all scroll delta and fling velocity will be ignored and neither + * [onPull] nor [onRelease] will be invoked. + */ +fun Modifier.pullRefresh( + onPull: (pullDelta: Float) -> Float, + onRelease: suspend (flingVelocity: Float) -> Unit, + enabled: Boolean = true +) = inspectable(inspectorInfo = debugInspectorInfo { + name = "pullRefresh" + properties["onPull"] = onPull + properties["onRelease"] = onRelease + properties["enabled"] = enabled +}) { + Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled)) +} + +private class PullRefreshNestedScrollConnection( + private val onPull: (pullDelta: Float) -> Float, + private val onRelease: suspend (flingVelocity: Float) -> Unit, + private val enabled: Boolean +) : NestedScrollConnection { + + override fun onPreScroll( + available: Offset, source: NestedScrollSource + ): Offset = when { + !enabled -> Offset.Zero + source == Drag && available.y < 0 -> Offset(0f, onPull(available.y)) // Swiping up + else -> Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, available: Offset, source: NestedScrollSource + ): Offset = when { + !enabled -> Offset.Zero + source == Drag && available.y > 0 -> Offset(0f, onPull(available.y)) // Pulling down + else -> Offset.Zero + } + + override suspend fun onPreFling(available: Velocity): Velocity { + onRelease(available.y) + return Velocity.Zero + } +} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshState.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshState.kt new file mode 100644 index 00000000..285d6a07 --- /dev/null +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshState.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rtbishop.look4sat.presentation.pullRefresh + +import androidx.compose.animation.core.animate +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.pow + +/** + * Creates a [PullRefreshState] that is remembered across compositions. + * Changes to [refreshing] will result in [PullRefreshState] being updated. + * @param refreshing A boolean representing whether a refresh is currently occurring. + * @param onRefresh The function to be called to trigger a refresh. + * @param refreshThreshold The threshold below which, if a release + * occurs, [onRefresh] will be called. + * @param refreshingOffset The offset at which the indicator will be drawn while refreshing. This + * offset corresponds to the position of the bottom of the indicator. + */ +@Composable +fun rememberPullRefreshState( + refreshing: Boolean, + onRefresh: () -> Unit, + refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold, + refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset, +): PullRefreshState { + require(refreshThreshold > 0.dp) { "The refresh trigger must be greater than zero!" } + + val scope = rememberCoroutineScope() + val onRefreshState = rememberUpdatedState(onRefresh) + val thresholdPx: Float + val refreshingOffsetPx: Float + + with(LocalDensity.current) { + thresholdPx = refreshThreshold.toPx() + refreshingOffsetPx = refreshingOffset.toPx() + } + + // refreshThreshold and refreshingOffset should not be changed after instantiation, so any + // changes to these values are ignored. + val state = remember(scope) { + PullRefreshState(scope, onRefreshState, refreshingOffsetPx, thresholdPx) + } + + SideEffect { + state.setRefreshing(refreshing) + } + + return state +} + +/** + * A state object that can be used in conjunction with [pullRefresh] to add pull-to-refresh + * behaviour to a scroll component. Based on Android's SwipeRefreshLayout. + * + * Provides [progress], a float representing how far the user has pulled as a percentage of the + * refreshThreshold. Values of one or less indicate that the user has not yet pulled past the + * threshold. Values greater than one indicate how far past the threshold the user has pulled. + * + * Can be used in conjunction with [pullRefreshIndicatorTransform] to implement Android-like + * pull-to-refresh behaviour with a custom indicator. + * + * Should be created using [rememberPullRefreshState]. + */ +class PullRefreshState internal constructor( + private val animationScope: CoroutineScope, + private val onRefreshState: State<() -> Unit>, + private val refreshingOffset: Float, + internal val threshold: Float +) { + /** + * A float representing how far the user has pulled as a percentage of the refreshThreshold. + * + * If the component has not been pulled at all, progress is zero. If the pull has reached + * halfway to the threshold, progress is 0.5f. A value greater than 1 indicates that pull has + * gone beyond the refreshThreshold - e.g. a value of 2f indicates that the user has pulled to + * two times the refreshThreshold. + */ + val progress get() = adjustedDistancePulled / threshold + + internal val refreshing get() = _refreshing + internal val position get() = _position + + private val adjustedDistancePulled by derivedStateOf { distancePulled * DragMultiplier } + + private var _refreshing by mutableStateOf(false) + private var _position by mutableStateOf(0f) + private var distancePulled by mutableStateOf(0f) + + internal fun onPull(pullDelta: Float): Float { + if (this._refreshing) return 0f // Already refreshing, do nothing. + + val newOffset = (distancePulled + pullDelta).coerceAtLeast(0f) + val dragConsumed = newOffset - distancePulled + distancePulled = newOffset + _position = calculateIndicatorPosition() + return dragConsumed + } + + internal fun onRelease() { + if (!this._refreshing) { + if (adjustedDistancePulled > threshold) { + onRefreshState.value() + } else { + animateIndicatorTo(0f) + } + } + distancePulled = 0f + } + + internal fun setRefreshing(refreshing: Boolean) { + if (this._refreshing != refreshing) { + this._refreshing = refreshing + this.distancePulled = 0f + animateIndicatorTo(if (refreshing) refreshingOffset else 0f) + } + } + + private fun animateIndicatorTo(offset: Float) = animationScope.launch { + animate(initialValue = _position, targetValue = offset) { value, _ -> + _position = value + } + } + + private fun calculateIndicatorPosition(): Float = when { + // If drag hasn't gone past the threshold, the position is the adjustedDistancePulled. + adjustedDistancePulled <= threshold -> adjustedDistancePulled + else -> { + // How far beyond the threshold pull has gone, as a percentage of the threshold. + val overshootPercent = abs(progress) - 1.0f + // Limit the overshoot to 200%. Linear between 0 and 200. + val linearTension = overshootPercent.coerceIn(0f, 2f) + // Non-linear tension. Increases with linearTension, but at a decreasing rate. + val tensionPercent = linearTension - linearTension.pow(2) / 4 + // The additional offset beyond the threshold. + val extraOffset = threshold * tensionPercent + threshold + extraOffset + } + } +} + +/** + * Default parameter values for [rememberPullRefreshState]. + */ +object PullRefreshDefaults { + /** + * If the indicator is below this threshold offset when it is released, a refresh + * will be triggered. + */ + val RefreshThreshold = 80.dp + + /** + * The offset at which the indicator should be rendered whilst a refresh is occurring. + */ + val RefreshingOffset = 56.dp +} + +/** + * The distance pulled is multiplied by this value to give us the adjusted distance pulled, which + * is used in calculating the indicator position (when the adjusted distance pulled is less than + * the refresh threshold, it is the indicator position, otherwise the indicator position is + * derived from the progress). + */ +private const val DragMultiplier = 0.5f diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarFragment.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarFragment.kt deleted file mode 100644 index 2c054691..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarFragment.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Look4Sat. Amateur radio satellite tracker and pass predictor. - * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.rtbishop.look4sat.presentation.radarScreen - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.SimpleItemAnimator -import com.rtbishop.look4sat.R -import com.rtbishop.look4sat.databinding.FragmentRadarBinding -import com.rtbishop.look4sat.domain.predict.SatPass -import com.rtbishop.look4sat.domain.predict.SatPos -import com.rtbishop.look4sat.utility.toDegrees -import com.rtbishop.look4sat.utility.toTimerString -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class RadarFragment : Fragment(R.layout.fragment_radar) { - - private val viewModel: RadarViewModel by viewModels() - private val radioAdapter = RadioAdapter() - private var binding: FragmentRadarBinding? = null - private var radarView: RadarView? = null - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentRadarBinding.bind(view).apply { - radarRecycler.apply { - setHasFixedSize(true) - this.adapter = radioAdapter - this.layoutManager = LinearLayoutManager(requireContext()) - (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - addItemDecoration(DividerItemDecoration(requireContext(), 1)) - } - setupObservers() - } - } - - override fun onResume() { - super.onResume() - viewModel.enableSensor() - } - - override fun onPause() { - super.onPause() - viewModel.disableSensor() - } - - override fun onDestroyView() { - binding?.radarRecycler?.adapter = null - radarView = null - binding = null - super.onDestroyView() - } - - private fun setupObservers() { - viewModel.getPass(45000, System.currentTimeMillis()).observe(viewLifecycleOwner) { pass -> - binding?.run { - radarView = RadarView(requireContext()).apply { - setShowAim(viewModel.getUseCompass()) - setScanning(viewModel.getShowSweep()) - } - radarCard.addView(radarView) - viewModel.radarData.observe(viewLifecycleOwner) { passData -> - radarView?.setPosition(passData.satPos) - radarView?.setPositions(passData.satTrack) - setPassText(pass, passData.satPos) - } - viewModel.transmitters.observe(viewLifecycleOwner) { list -> - if (list.isNotEmpty()) { - radioAdapter.submitList(list) - radarProgress.visibility = View.INVISIBLE - } else { - radarProgress.visibility = View.INVISIBLE - radarEmptyLayout.visibility = View.VISIBLE - } - radarView?.invalidate() - } - viewModel.orientation.observe(viewLifecycleOwner) { value -> - radarView?.setOrientation(value.first, value.second, value.third) - } -// radarBtnBack.clickWithDebounce { findNavController().navigateUp() } -// radarBtnMap.clickWithDebounce { -// val direction = RadarFragmentDirections.globalToMap(pass.catNum) -// findNavController().navigate(direction) -// } -// radarBtnNotify.isEnabled = false -// radarBtnSettings.clickWithDebounce { -// val direction = RadarFragmentDirections.globalToSettings() -// findNavController().navigate(direction) -// } - } - } - } - - private fun setPassText(satPass: SatPass, satPos: SatPos) { - binding?.run { - val timeNow = System.currentTimeMillis() - val radarAzim = getString(R.string.radar_az_value) - val radarElev = getString(R.string.radar_el_value) - val radarAlt = getString(R.string.radar_alt_value) - val radarDist = getString(R.string.radar_dist_value) - radarAzValue.text = String.format(radarAzim, satPos.azimuth.toDegrees()) - radarElValue.text = String.format(radarElev, satPos.elevation.toDegrees()) - radarAltValue.text = String.format(radarAlt, satPos.altitude) - radarDstValue.text = String.format(radarDist, satPos.distance) - if (satPos.eclipsed) { - radarVisibility.text = getText(R.string.radar_eclipsed) - } else { - radarVisibility.text = getText(R.string.radar_visible) - } - if (!satPass.isDeepSpace) { - if (timeNow < satPass.aosTime) { - val millisBeforeStart = satPass.aosTime.minus(timeNow) - radarTimer.text = millisBeforeStart.toTimerString() - } else { - val millisBeforeEnd = satPass.losTime.minus(timeNow) - radarTimer.text = millisBeforeEnd.toTimerString() - if (timeNow > satPass.losTime) { - radarTimer.text = 0L.toTimerString() -// findNavController().navigateUp() - } - } - } else radarTimer.text = 0L.toTimerString() - } - } -} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt index abbe50aa..07fb233b 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarScreen.kt @@ -55,3 +55,79 @@ fun RadarScreen(viewModel: RadarViewModel = hiltViewModel()) { } } } + +//private val divider = 1000000f +//radioDownlink.text = String.format(Locale.ENGLISH, link, downlink / divider) +//radioUplink.text = String.format(Locale.ENGLISH, link, uplink / divider) + +//private fun setupObservers() { +// viewModel.getPass(45000, System.currentTimeMillis()).observe(viewLifecycleOwner) { pass -> +// binding?.run { +// radarView = RadarView(requireContext()).apply { +// setShowAim(viewModel.getUseCompass()) +// setScanning(viewModel.getShowSweep()) +// } +// radarCard.addView(radarView) +// viewModel.radarData.observe(viewLifecycleOwner) { passData -> +// radarView?.setPosition(passData.satPos) +// radarView?.setPositions(passData.satTrack) +// setPassText(pass, passData.satPos) +// } +// viewModel.transmitters.observe(viewLifecycleOwner) { list -> +// if (list.isNotEmpty()) { +// radioAdapter.submitList(list) +// radarProgress.visibility = View.INVISIBLE +// } else { +// radarProgress.visibility = View.INVISIBLE +// radarEmptyLayout.visibility = View.VISIBLE +// } +// radarView?.invalidate() +// } +// viewModel.orientation.observe(viewLifecycleOwner) { value -> +// radarView?.setOrientation(value.first, value.second, value.third) +// } +// radarBtnBack.clickWithDebounce { findNavController().navigateUp() } +// radarBtnMap.clickWithDebounce { +// val direction = RadarFragmentDirections.globalToMap(pass.catNum) +// findNavController().navigate(direction) +// } +// radarBtnNotify.isEnabled = false +// radarBtnSettings.clickWithDebounce { +// val direction = RadarFragmentDirections.globalToSettings() +// findNavController().navigate(direction) +// } +// } +// } +//} +// +//private fun setPassText(satPass: SatPass, satPos: SatPos) { +// binding?.run { +// val timeNow = System.currentTimeMillis() +// val radarAzim = getString(R.string.radar_az_value) +// val radarElev = getString(R.string.radar_el_value) +// val radarAlt = getString(R.string.radar_alt_value) +// val radarDist = getString(R.string.radar_dist_value) +// radarAzValue.text = String.format(radarAzim, satPos.azimuth.toDegrees()) +// radarElValue.text = String.format(radarElev, satPos.elevation.toDegrees()) +// radarAltValue.text = String.format(radarAlt, satPos.altitude) +// radarDstValue.text = String.format(radarDist, satPos.distance) +// if (satPos.eclipsed) { +// radarVisibility.text = getText(R.string.radar_eclipsed) +// } else { +// radarVisibility.text = getText(R.string.radar_visible) +// } +// if (!satPass.isDeepSpace) { +// if (timeNow < satPass.aosTime) { +// val millisBeforeStart = satPass.aosTime.minus(timeNow) +// radarTimer.text = millisBeforeStart.toTimerString() +// } else { +// val millisBeforeEnd = satPass.losTime.minus(timeNow) +// radarTimer.text = millisBeforeEnd.toTimerString() +// if (timeNow > satPass.losTime) { +// radarTimer.text = 0L.toTimerString() +//// findNavController().navigateUp() +// } +// } +// } else radarTimer.text = 0L.toTimerString() +// } +//} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt index 158b5ff2..776815ae 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarViewModel.kt @@ -22,7 +22,6 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ISatelliteManager @@ -37,10 +36,11 @@ import com.rtbishop.look4sat.utility.DataReporter import com.rtbishop.look4sat.utility.round import com.rtbishop.look4sat.utility.toDegrees import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class RadarViewModel @Inject constructor( @@ -60,7 +60,7 @@ class RadarViewModel @Inject constructor( val transmitters: LiveData> = _transmitters val orientation: LiveData> = _orientation - fun getPass(catNum: Int, aosTime: Long) = liveData { + fun getPass(catNum: Int, aosTime: Long) = flow { satManager.calculatedPasses.collect { passes -> val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime } pass?.let { satPass -> @@ -111,8 +111,8 @@ class RadarViewModel @Inject constructor( if (settings.getRotatorEnabled()) { val server = settings.getRotatorServer() val port = settings.getRotatorPort().toInt() - val azimuth = satPos.azimuth.toDegrees().round(2) - val elevation = satPos.elevation.toDegrees().round(2) + val azimuth = satPos.azimuth.toDegrees().round(1) + val elevation = satPos.elevation.toDegrees().round(1) reporter.reportRotation(server, port, azimuth, elevation) } _passData.postValue(RadarData(satPos, track)) diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadioAdapter.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadioAdapter.kt deleted file mode 100644 index 12fd0d70..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadioAdapter.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Look4Sat. Amateur radio satellite tracker and pass predictor. - * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.rtbishop.look4sat.presentation.radarScreen - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.rtbishop.look4sat.R -import com.rtbishop.look4sat.databinding.ItemRadioBinding -import com.rtbishop.look4sat.domain.model.SatRadio -import java.util.* - -class RadioAdapter : RecyclerView.Adapter() { - - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean { - return oldItem.uuid == newItem.uuid - } - - override fun areContentsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean { - return oldItem.downlink == newItem.downlink - } - } - private val differ = AsyncListDiffer(this, diffCallback) - - fun submitList(items: List) = differ.submitList(items) - - override fun getItemCount() = differ.currentList.size - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransHolder { - return TransHolder.from(parent) - } - - override fun onBindViewHolder(holder: TransHolder, position: Int) { - holder.bind(differ.currentList[position]) - } - - class TransHolder private constructor(private val binding: ItemRadioBinding) : - RecyclerView.ViewHolder(binding.root) { - - private val divider = 1000000f - private val stringYes = itemView.context.getString(R.string.radio_string_yes) - private val stringNo = itemView.context.getString(R.string.radio_string_no) - private val link = itemView.context.getString(R.string.radio_link_low) - private val linkNull = itemView.context.getString(R.string.radio_no_link) - private val mode = itemView.context.getString(R.string.radio_mode) - private val inverted = itemView.context.getString(R.string.radio_inverted) - - fun bind(radio: SatRadio) { - binding.run { - radioInfo.text = radio.info - radio.downlink.let { downlink -> - if (downlink != null) { - radioDownlink.text = String.format(Locale.ENGLISH, link, downlink / divider) - } else { - radioDownlink.text = linkNull - } - } - radio.uplink.let { uplink -> - if (uplink != null) { - radioUplink.text = String.format(Locale.ENGLISH, link, uplink / divider) - } else { - radioUplink.text = linkNull - } - } - if (radio.mode != null) { - radioMode.text = String.format(mode, radio.mode) - } else { - radioMode.text = String.format(mode, stringNo) - } - if (radio.isInverted) { - radioInverted.text = String.format(inverted, stringYes) - } else { - radioInverted.text = String.format(inverted, stringNo) - } - } - } - - companion object { - fun from(parent: ViewGroup): TransHolder { - val inflater = LayoutInflater.from(parent.context) - return TransHolder(ItemRadioBinding.inflate(inflater, parent, false)) - } - } - } -} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt deleted file mode 100644 index 9fba5b1e..00000000 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Look4Sat. Amateur radio satellite tracker and pass predictor. - * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.rtbishop.look4sat.presentation.settingsScreen - -import android.Manifest -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.widget.NestedScrollView -import androidx.core.widget.doOnTextChanged -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.asLiveData -import com.rtbishop.look4sat.R -import com.rtbishop.look4sat.databinding.FragmentSettingsBinding -import com.rtbishop.look4sat.domain.model.DataState -import com.rtbishop.look4sat.domain.predict.GeoPos -import com.rtbishop.look4sat.presentation.clickWithDebounce -import com.rtbishop.look4sat.utility.isValidIPv4 -import com.rtbishop.look4sat.utility.isValidPort -import dagger.hilt.android.AndroidEntryPoint -import java.text.SimpleDateFormat -import java.util.* - -@AndroidEntryPoint -class SettingsFragment : Fragment(R.layout.fragment_settings) { - - private val viewModel: SettingsViewModel by viewModels() - private val bluetooth = when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH - else -> Manifest.permission.BLUETOOTH_CONNECT - } - private val bluetoothContract = ActivityResultContracts.RequestPermission() - private val bluetoothRequest = registerForActivityResult(bluetoothContract) { isGranted -> - if (!isGranted) { - showToast(getString(R.string.BTremote_perm_error)) - toggleBTstate(isGranted) - } - } - private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION - private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION - private val locationContract = ActivityResultContracts.RequestMultiplePermissions() - private val locationRequest = registerForActivityResult(locationContract) { permissions -> - when { - permissions[locationFine] == true -> viewModel.setPositionFromGps() - permissions[locationCoarse] == true -> viewModel.setPositionFromNet() - else -> showToast(getString(R.string.location_gps_error)) - } - } - private val contentContract = ActivityResultContracts.GetContent() - private val contentRequest = registerForActivityResult(contentContract) { uri -> - uri?.let { viewModel.updateFromFile(uri.toString()) } - } - private lateinit var binding: FragmentSettingsBinding - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding = FragmentSettingsBinding.bind(view).apply { -// settingsBtnBack.clickWithDebounce { findNavController().navigateUp() } - settingsScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, y, _, newY -> - if (y > newY) settingsFab.hide() else settingsFab.show() - }) - settingsBtnGithub.clickWithDebounce { - gotoUrl("https://github.com/rt-bishop/Look4Sat/") - } - settingsFab.clickWithDebounce { - gotoUrl("https://ko-fi.com/rt_bishop") - } - settingsBtnFdroid.clickWithDebounce { - gotoUrl("https://f-droid.org/en/packages/com.rtbishop.look4sat/") - } - } - setupLocationCard() - setupDataCard() - setupRemoteCard() - setupBTCard() - setupOtherCard() - viewModel.stationPosition.asLiveData().observe(viewLifecycleOwner) { stationPos -> - stationPos?.let { handleStationPosition(stationPos) } - } - viewModel.getUpdateState().asLiveData().observe(viewLifecycleOwner) { updateState -> - updateState?.let { handleSatState(updateState) } - } - } - - private fun setupLocationCard() { - binding.run { - setPositionText(viewModel.getStationPosition()) - settingsLocation.locationBtnGps.clickWithDebounce { - locationRequest.launch(arrayOf(locationFine, locationCoarse)) - } -// settingsLocation.locationBtnManual.clickWithDebounce { -// val action = SettingsFragmentDirections.globalToPosition() -// findNavController().navigate(action) -// } -// settingsLocation.locationBtnQth.clickWithDebounce { -// val action = SettingsFragmentDirections.globalToLocator() -// findNavController().navigate(action) -// } -// getNavResult>(R.id.nav_settings, "position") { position -> -// viewModel.setStationPosition(position.first, position.second) -// } -// getNavResult(R.id.nav_settings, "locator") { locator -> -// viewModel.setPositionFromQth(locator) -// } - } - } - - private fun setupDataCard() { - binding.run { - setUpdateTime(viewModel.getLastUpdateTime()) - settingsData.dataBtnWeb.clickWithDebounce { viewModel.updateFromWeb() } - settingsData.dataBtnFile.clickWithDebounce { contentRequest.launch("*/*") } - settingsData.dataBtnClear.clickWithDebounce { viewModel.clearAllData() } - viewModel.entriesTotal.observe(viewLifecycleOwner) { number -> - val entriesFormat = getString(R.string.data_entries) - settingsData.dataEntries.text = String.format(entriesFormat, number) - } - viewModel.radiosTotal.observe(viewLifecycleOwner) { number -> - val radiosFormat = getString(R.string.data_radios) - settingsData.dataRadios.text = String.format(radiosFormat, number) - } -// getNavResult>(R.id.nav_settings, "sources") { -// viewModel.updateFromWeb() -// } - } - } - - private fun setupRemoteCard() { - binding.run { - settingsRemote.remoteSwitch.apply { - isChecked = viewModel.getRotatorEnabled() - settingsRemote.remoteIp.isEnabled = isChecked - settingsRemote.remoteIpEdit.setText(viewModel.getRotatorServer()) - settingsRemote.remotePort.isEnabled = isChecked - settingsRemote.remotePortEdit.setText(viewModel.getRotatorPort()) - setOnCheckedChangeListener { _, isChecked -> - viewModel.setRotatorEnabled(isChecked) - settingsRemote.remoteIp.isEnabled = isChecked - settingsRemote.remotePort.isEnabled = isChecked - } - } - settingsRemote.remoteIpEdit.doOnTextChanged { text, _, _, _ -> - if (text.toString().isValidIPv4()) viewModel.setRotatorServer(text.toString()) - } - settingsRemote.remotePortEdit.doOnTextChanged { text, _, _, _ -> - if (text.toString().isValidPort()) viewModel.setRotatorPort(text.toString()) - } - } - } - - private fun setupBTCard() { - binding.run { - settingsBtremote.BTremoteSwitch.apply { - isChecked = viewModel.getBTEnabled() - settingsBtremote.BTremoteAddress.isEnabled = isChecked - settingsBtremote.BTAddressEdit.setText(viewModel.getBTDeviceAddr()) - settingsBtremote.BTremoteFormat.isEnabled = isChecked - settingsBtremote.BTFormatEdit.setText(viewModel.getBTFormat()) - setOnCheckedChangeListener { _, isChecked -> - toggleBTstate(isChecked) - bluetoothRequest.launch(bluetooth) - } - } - settingsBtremote.BTAddressEdit.doOnTextChanged { text, _, _, _ -> - viewModel.setBTDeviceAddr(text.toString()) - } - settingsBtremote.BTFormatEdit.doOnTextChanged { text, _, _, _ -> - viewModel.setBTFormat(text.toString()) - } - } - } - - private fun toggleBTstate(value: Boolean) { - binding.run { - viewModel.setBTEnabled(value) - settingsBtremote.BTremoteSwitch.isChecked = value - settingsBtremote.BTremoteAddress.isEnabled = value - settingsBtremote.BTremoteFormat.isEnabled = value - } - } - - private fun setupOtherCard() { - binding.run { - settingsOther.otherSwitchUtc.apply { - isChecked = viewModel.getUseUTC() - setOnCheckedChangeListener { _, isChecked -> viewModel.setUseUTC(isChecked) } - } - settingsOther.otherSwitchUpdate.apply { - isChecked = viewModel.getAutoUpdateEnabled() - setOnCheckedChangeListener { _, isChecked -> viewModel.setAutoUpdateEnabled(isChecked) } - } - settingsOther.otherSwitchSweep.apply { - isChecked = viewModel.getShowSweep() - setOnCheckedChangeListener { _, isChecked -> viewModel.setShowSweep(isChecked) } - } - settingsOther.otherSwitchSensors.apply { - isChecked = viewModel.getUseCompass() - setOnCheckedChangeListener { _, isChecked -> viewModel.setUseCompass(isChecked) } - } - } - } - - private fun handleStationPosition(pos: DataState) { - when (pos) { - is DataState.Success -> { - setPositionText(pos.data) - binding.settingsLocation.locationProgress.isIndeterminate = false - viewModel.setPositionHandled() - showToast(getString(R.string.location_success)) - } - is DataState.Error -> { - binding.settingsLocation.locationProgress.isIndeterminate = false - viewModel.setPositionHandled() - showToast(pos.message.toString()) - } - DataState.Loading -> { - binding.settingsLocation.locationProgress.isIndeterminate = true - } - DataState.Handled -> {} - } - } - - private fun setPositionText(geoPos: GeoPos) { - binding.run { - val latFormat = getString(R.string.location_lat) - val lonFormat = getString(R.string.location_lon) - settingsLocation.locationLat.text = String.format(latFormat, geoPos.lat) - settingsLocation.locationLon.text = String.format(lonFormat, geoPos.lon) - } - } - - private fun handleSatState(state: DataState) { - when (state) { - is DataState.Success -> { - binding.settingsData.dataProgress.isIndeterminate = false - setUpdateTime(state.data) - viewModel.setUpdateHandled() - if (state.data == 0L) { - showToast(getString(R.string.data_clear_success)) - } else { - showToast(getString(R.string.data_success)) - } - } - is DataState.Error -> { - binding.settingsData.dataProgress.isIndeterminate = false - viewModel.setUpdateHandled() - showToast(getString(R.string.data_error)) - } - is DataState.Loading -> { - binding.settingsData.dataProgress.isIndeterminate = true - } - is DataState.Handled -> {} - } - } - - private fun setUpdateTime(updateTime: Long) { - val updatePattern = getString(R.string.data_update) - val updateDate = if (updateTime == 0L) { - getString(R.string.pass_placeholder) - } else { - SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()).format(Date(updateTime)) - } - binding.settingsData.dataUpdate.text = String.format(updatePattern, updateDate) - } - - private fun showToast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() - } - - private fun gotoUrl(url: String) { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) - } -} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsScreen.kt index 0e7af701..c4437e14 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsScreen.kt @@ -132,3 +132,250 @@ private fun CardCredits(modifier: Modifier = Modifier) { } } } + +//private val viewModel: SettingsViewModel by viewModels() +//private val bluetooth = when { +// Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH +// else -> Manifest.permission.BLUETOOTH_CONNECT +//} +//private val bluetoothContract = ActivityResultContracts.RequestPermission() +//private val bluetoothRequest = registerForActivityResult(bluetoothContract) { isGranted -> +// if (!isGranted) { +// showToast(getString(R.string.BTremote_perm_error)) +// toggleBTstate(isGranted) +// } +//} +//private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION +//private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION +//private val locationContract = ActivityResultContracts.RequestMultiplePermissions() +//private val locationRequest = registerForActivityResult(locationContract) { permissions -> +// when { +// permissions[locationFine] == true -> viewModel.setPositionFromGps() +// permissions[locationCoarse] == true -> viewModel.setPositionFromNet() +// else -> showToast(getString(R.string.location_gps_error)) +// } +//} +//private val contentContract = ActivityResultContracts.GetContent() +//private val contentRequest = registerForActivityResult(contentContract) { uri -> +// uri?.let { viewModel.updateFromFile(uri.toString()) } +//} +//private lateinit var binding: FragmentSettingsBinding +// +//override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// super.onViewCreated(view, savedInstanceState) +// binding = FragmentSettingsBinding.bind(view).apply { +// settingsBtnBack.clickWithDebounce { findNavController().navigateUp() } +// settingsScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, y, _, newY -> +// if (y > newY) settingsFab.hide() else settingsFab.show() +// }) +// settingsBtnGithub.clickWithDebounce { +// gotoUrl("https://github.com/rt-bishop/Look4Sat/") +// } +// settingsFab.clickWithDebounce { +// gotoUrl("https://ko-fi.com/rt_bishop") +// } +// settingsBtnFdroid.clickWithDebounce { +// gotoUrl("https://f-droid.org/en/packages/com.rtbishop.look4sat/") +// } +// } +// setupLocationCard() +// setupDataCard() +// setupRemoteCard() +// setupBTCard() +// setupOtherCard() +// viewModel.stationPosition.asLiveData().observe(viewLifecycleOwner) { stationPos -> +// stationPos?.let { handleStationPosition(stationPos) } +// } +// viewModel.getUpdateState().asLiveData().observe(viewLifecycleOwner) { updateState -> +// updateState?.let { handleSatState(updateState) } +// } +//} +// +//private fun setupLocationCard() { +// binding.run { +// setPositionText(viewModel.getStationPosition()) +// settingsLocation.locationBtnGps.clickWithDebounce { +// locationRequest.launch(arrayOf(locationFine, locationCoarse)) +// } +// settingsLocation.locationBtnManual.clickWithDebounce { +// val action = SettingsFragmentDirections.globalToPosition() +// findNavController().navigate(action) +// } +// settingsLocation.locationBtnQth.clickWithDebounce { +// val action = SettingsFragmentDirections.globalToLocator() +// findNavController().navigate(action) +// } +// getNavResult>(R.id.nav_settings, "position") { position -> +// viewModel.setStationPosition(position.first, position.second) +// } +// getNavResult(R.id.nav_settings, "locator") { locator -> +// viewModel.setPositionFromQth(locator) +// } +// } +//} +// +//private fun setupDataCard() { +// binding.run { +// setUpdateTime(viewModel.getLastUpdateTime()) +// settingsData.dataBtnWeb.clickWithDebounce { viewModel.updateFromWeb() } +// settingsData.dataBtnFile.clickWithDebounce { contentRequest.launch("*/*") } +// settingsData.dataBtnClear.clickWithDebounce { viewModel.clearAllData() } +// viewModel.entriesTotal.observe(viewLifecycleOwner) { number -> +// val entriesFormat = getString(R.string.data_entries) +// settingsData.dataEntries.text = String.format(entriesFormat, number) +// } +// viewModel.radiosTotal.observe(viewLifecycleOwner) { number -> +// val radiosFormat = getString(R.string.data_radios) +// settingsData.dataRadios.text = String.format(radiosFormat, number) +// } +// getNavResult>(R.id.nav_settings, "sources") { +// viewModel.updateFromWeb() +// } +// } +//} +// +//private fun setupRemoteCard() { +// binding.run { +// settingsRemote.remoteSwitch.apply { +// isChecked = viewModel.getRotatorEnabled() +// settingsRemote.remoteIp.isEnabled = isChecked +// settingsRemote.remoteIpEdit.setText(viewModel.getRotatorServer()) +// settingsRemote.remotePort.isEnabled = isChecked +// settingsRemote.remotePortEdit.setText(viewModel.getRotatorPort()) +// setOnCheckedChangeListener { _, isChecked -> +// viewModel.setRotatorEnabled(isChecked) +// settingsRemote.remoteIp.isEnabled = isChecked +// settingsRemote.remotePort.isEnabled = isChecked +// } +// } +// settingsRemote.remoteIpEdit.doOnTextChanged { text, _, _, _ -> +// if (text.toString().isValidIPv4()) viewModel.setRotatorServer(text.toString()) +// } +// settingsRemote.remotePortEdit.doOnTextChanged { text, _, _, _ -> +// if (text.toString().isValidPort()) viewModel.setRotatorPort(text.toString()) +// } +// } +//} +// +//private fun setupBTCard() { +// binding.run { +// settingsBtremote.BTremoteSwitch.apply { +// isChecked = viewModel.getBTEnabled() +// settingsBtremote.BTremoteAddress.isEnabled = isChecked +// settingsBtremote.BTAddressEdit.setText(viewModel.getBTDeviceAddr()) +// settingsBtremote.BTremoteFormat.isEnabled = isChecked +// settingsBtremote.BTFormatEdit.setText(viewModel.getBTFormat()) +// setOnCheckedChangeListener { _, isChecked -> +// toggleBTstate(isChecked) +// bluetoothRequest.launch(bluetooth) +// } +// } +// settingsBtremote.BTAddressEdit.doOnTextChanged { text, _, _, _ -> +// viewModel.setBTDeviceAddr(text.toString()) +// } +// settingsBtremote.BTFormatEdit.doOnTextChanged { text, _, _, _ -> +// viewModel.setBTFormat(text.toString()) +// } +// } +//} +// +//private fun toggleBTstate(value: Boolean) { +// binding.run { +// viewModel.setBTEnabled(value) +// settingsBtremote.BTremoteSwitch.isChecked = value +// settingsBtremote.BTremoteAddress.isEnabled = value +// settingsBtremote.BTremoteFormat.isEnabled = value +// } +//} +// +//private fun setupOtherCard() { +// binding.run { +// settingsOther.otherSwitchUtc.apply { +// isChecked = viewModel.getUseUTC() +// setOnCheckedChangeListener { _, isChecked -> viewModel.setUseUTC(isChecked) } +// } +// settingsOther.otherSwitchUpdate.apply { +// isChecked = viewModel.getAutoUpdateEnabled() +// setOnCheckedChangeListener { _, isChecked -> viewModel.setAutoUpdateEnabled(isChecked) } +// } +// settingsOther.otherSwitchSweep.apply { +// isChecked = viewModel.getShowSweep() +// setOnCheckedChangeListener { _, isChecked -> viewModel.setShowSweep(isChecked) } +// } +// settingsOther.otherSwitchSensors.apply { +// isChecked = viewModel.getUseCompass() +// setOnCheckedChangeListener { _, isChecked -> viewModel.setUseCompass(isChecked) } +// } +// } +//} +// +//private fun handleStationPosition(pos: DataState) { +// when (pos) { +// is DataState.Success -> { +// setPositionText(pos.data) +// binding.settingsLocation.locationProgress.isIndeterminate = false +// viewModel.setPositionHandled() +// showToast(getString(R.string.location_success)) +// } +// is DataState.Error -> { +// binding.settingsLocation.locationProgress.isIndeterminate = false +// viewModel.setPositionHandled() +// showToast(pos.message.toString()) +// } +// DataState.Loading -> { +// binding.settingsLocation.locationProgress.isIndeterminate = true +// } +// DataState.Handled -> {} +// } +//} +// +//private fun setPositionText(geoPos: GeoPos) { +// binding.run { +// val latFormat = getString(R.string.location_lat) +// val lonFormat = getString(R.string.location_lon) +// settingsLocation.locationLat.text = String.format(latFormat, geoPos.lat) +// settingsLocation.locationLon.text = String.format(lonFormat, geoPos.lon) +// } +//} +// +//private fun handleSatState(state: DataState) { +// when (state) { +// is DataState.Success -> { +// binding.settingsData.dataProgress.isIndeterminate = false +// setUpdateTime(state.data) +// viewModel.setUpdateHandled() +// if (state.data == 0L) { +// showToast(getString(R.string.data_clear_success)) +// } else { +// showToast(getString(R.string.data_success)) +// } +// } +// is DataState.Error -> { +// binding.settingsData.dataProgress.isIndeterminate = false +// viewModel.setUpdateHandled() +// showToast(getString(R.string.data_error)) +// } +// is DataState.Loading -> { +// binding.settingsData.dataProgress.isIndeterminate = true +// } +// is DataState.Handled -> {} +// } +//} +// +//private fun setUpdateTime(updateTime: Long) { +// val updatePattern = getString(R.string.data_update) +// val updateDate = if (updateTime == 0L) { +// getString(R.string.pass_placeholder) +// } else { +// SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()).format(Date(updateTime)) +// } +// binding.settingsData.dataUpdate.text = String.format(updatePattern, updateDate) +//} +// +//private fun showToast(message: String) { +// Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() +//} +// +//private fun gotoUrl(url: String) { +// startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) +//} diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt index fb117d3b..7f2a58d1 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsViewModel.kt @@ -18,7 +18,6 @@ package com.rtbishop.look4sat.presentation.settingsScreen import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.ILocationManager import com.rtbishop.look4sat.domain.ISettingsManager @@ -35,8 +34,8 @@ class SettingsViewModel @Inject constructor( private val settings: ISettingsManager ) : ViewModel() { - val entriesTotal = repository.getEntriesTotal().asLiveData() - val radiosTotal = repository.getRadiosTotal().asLiveData() + val entriesTotal = repository.getEntriesTotal() + val radiosTotal = repository.getRadiosTotal() fun updateFromFile(uri: String) = repository.updateFromFile(uri) diff --git a/app/src/main/res/drawable/ic_checkbox_off.xml b/app/src/main/res/drawable/ic_checkbox_off.xml index 323c8421..c1326b0b 100644 --- a/app/src/main/res/drawable/ic_checkbox_off.xml +++ b/app/src/main/res/drawable/ic_checkbox_off.xml @@ -2,7 +2,6 @@ android:width="24dp" android:height="24dp" android:autoMirrored="true" - android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/card_data.xml b/app/src/main/res/layout/card_data.xml deleted file mode 100644 index 028cd056..00000000 --- a/app/src/main/res/layout/card_data.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - -