From bf51fddc4d65da4d7f517adf36d0a06ae237079a Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Fri, 3 Mar 2023 11:21:02 +0000 Subject: [PATCH] Further classes and dependencies cleanup --- app/build.gradle | 54 ++-- app/src/main/AndroidManifest.xml | 12 +- app/src/main/ic_launcher-playstore.png | Bin 25281 -> 0 bytes .../look4sat/presentation/MainActivity.kt | 14 - .../entriesScreen/EntriesScreen.kt | 4 +- .../entriesScreen/EntriesViewModel.kt | 43 ++- .../presentation/mapScreen/MapScreen.kt | 13 +- .../presentation/mapScreen/MapViewModel.kt | 33 +- .../presentation/passesScreen/PassesScreen.kt | 18 +- .../passesScreen/PassesViewModel.kt | 24 +- .../pullRefresh/PullRefreshIndicator.kt | 204 ++++++++++++ .../pullRefresh/PullRefreshModifier.kt | 136 ++++++++ .../pullRefresh/PullRefreshState.kt | 183 +++++++++++ .../presentation/radarScreen/RadarFragment.kt | 145 --------- .../presentation/radarScreen/RadarScreen.kt | 76 +++++ .../radarScreen/RadarViewModel.kt | 10 +- .../presentation/radarScreen/RadioAdapter.kt | 103 ------ .../settingsScreen/SettingsFragment.kt | 293 ------------------ .../settingsScreen/SettingsScreen.kt | 247 +++++++++++++++ .../settingsScreen/SettingsViewModel.kt | 5 +- app/src/main/res/drawable/ic_checkbox_off.xml | 1 - app/src/main/res/drawable/ic_checkbox_on.xml | 1 - app/src/main/res/drawable/ic_checkmark.xml | 1 - app/src/main/res/drawable/ic_elevation.xml | 1 - app/src/main/res/drawable/ic_filter.xml | 1 - app/src/main/res/drawable/ic_list.xml | 1 - app/src/main/res/drawable/ic_next.xml | 1 - app/src/main/res/drawable/ic_prev.xml | 1 - app/src/main/res/drawable/ic_radar.xml | 1 - app/src/main/res/drawable/ic_satellite.xml | 1 - app/src/main/res/drawable/ic_settings.xml | 1 - app/src/main/res/drawable/ic_world.xml | 1 - app/src/main/res/layout/card_btremote.xml | 74 ----- app/src/main/res/layout/card_data.xml | 107 ------- app/src/main/res/layout/card_location.xml | 99 ------ app/src/main/res/layout/card_other.xml | 81 ----- app/src/main/res/layout/card_remote.xml | 79 ----- app/src/main/res/layout/fragment_radar.xml | 235 -------------- app/src/main/res/layout/fragment_settings.xml | 131 -------- app/src/main/res/layout/item_radio.xml | 103 ------ app/src/main/res/layout/layout_map.xml | 5 - app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2270 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3923 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1485 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2460 -> 0 bytes .../main/res/mipmap-nodpi/ic_launcher.webp | Bin 0 -> 2560 bytes .../res/mipmap-nodpi/ic_launcher_round.webp | Bin 0 -> 5314 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3225 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 5580 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 4868 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 8746 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 6712 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 12435 -> 0 bytes app/src/main/res/values/styles.xml | 175 ----------- app/src/main/res/values/themes.xml | 18 ++ app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/network_security_config.xml | 2 +- build.gradle | 28 +- 59 files changed, 1019 insertions(+), 1779 deletions(-) delete mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshIndicator.kt create mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshModifier.kt create mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/pullRefresh/PullRefreshState.kt delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadarFragment.kt delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/radarScreen/RadioAdapter.kt delete mode 100644 app/src/main/java/com/rtbishop/look4sat/presentation/settingsScreen/SettingsFragment.kt delete mode 100644 app/src/main/res/layout/card_btremote.xml delete mode 100644 app/src/main/res/layout/card_data.xml delete mode 100644 app/src/main/res/layout/card_location.xml delete mode 100644 app/src/main/res/layout/card_other.xml delete mode 100644 app/src/main/res/layout/card_remote.xml delete mode 100644 app/src/main/res/layout/fragment_radar.xml delete mode 100644 app/src/main/res/layout/fragment_settings.xml delete mode 100644 app/src/main/res/layout/item_radio.xml delete mode 100644 app/src/main/res/layout/layout_map.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-nodpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-nodpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml 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 b31c292cce9e74d754c3664c7bc6b05a41378607..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25281 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 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 @@ - - - - - - - - - - - - - - - -