Skip to content

Commit 35a587c

Browse files
eriedclaude
andcommitted
tabbed settings with Display split, icon-on-top layout, gauge slider polish
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 19ecebb commit 35a587c

17 files changed

Lines changed: 673 additions & 96 deletions

File tree

app/src/main/java/com/eried/eucplanet/data/db/AppDatabase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import com.eried.eucplanet.data.model.TripRecord
88

99
@Database(
1010
entities = [AppSettings::class, TripRecord::class, AlarmRule::class],
11-
version = 18,
11+
version = 20,
1212
exportSchema = false
1313
)
1414
abstract class AppDatabase : RoomDatabase() {

app/src/main/java/com/eried/eucplanet/data/model/AppSettings.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ data class AppSettings(
112112
val themeMode: String = "black",
113113
// accentColor: key into the accent palette
114114
val accentColor: String = "default",
115+
// Colored danger-zone band behind the speed arc (yellow/orange/red thresholds).
116+
@ColumnInfo(defaultValue = "0")
117+
val showGaugeColorBand: Boolean = false,
118+
// Percentages of the full speed sweep where orange and red zones begin (yellow fills below orange).
119+
@ColumnInfo(defaultValue = "65")
120+
val gaugeOrangeThresholdPct: Int = 65,
121+
@ColumnInfo(defaultValue = "85")
122+
val gaugeRedThresholdPct: Int = 85,
123+
// Haptic feedback on dashboard action button taps.
124+
@ColumnInfo(defaultValue = "1")
125+
val hapticFeedback: Boolean = true,
126+
// "AMPS" or "WATTS" — long-press the amps card to switch.
127+
@ColumnInfo(defaultValue = "AMPS")
128+
val currentDisplayMode: String = "AMPS",
115129

116130
// Backup folder (SAF tree URI on local storage; companion sync app handles cloud upload)
117131
val syncFolderUri: String? = null,

app/src/main/java/com/eried/eucplanet/flic/FlicManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class FlicManager @Inject constructor(
4848
private val _scanning = MutableStateFlow(false)
4949
val scanning: StateFlow<Boolean> = _scanning.asStateFlow()
5050

51+
// Timestamp of last dispatched Flic action. UI uses this to flash a Flic indicator briefly.
52+
private val _lastActionAt = MutableStateFlow(0L)
53+
val lastActionAt: StateFlow<Long> = _lastActionAt.asStateFlow()
54+
5155
private val _scanStatus = MutableStateFlow("")
5256
val scanStatus: StateFlow<String> = _scanStatus.asStateFlow()
5357

@@ -205,6 +209,7 @@ class FlicManager @Inject constructor(
205209
}
206210

207211
private suspend fun executeAction(action: FlicAction, settings: AppSettings) {
212+
if (action != FlicAction.NONE) _lastActionAt.value = System.currentTimeMillis()
208213
when (action) {
209214
FlicAction.NONE -> {}
210215
FlicAction.HORN -> wheelRepository.sendHorn()

app/src/main/java/com/eried/eucplanet/ui/dashboard/DashboardScreen.kt

Lines changed: 331 additions & 49 deletions
Large diffs are not rendered by default.

app/src/main/java/com/eried/eucplanet/ui/dashboard/DashboardViewModel.kt

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@ import com.eried.eucplanet.data.repository.MetricSample
1010
import com.eried.eucplanet.data.repository.SettingsRepository
1111
import com.eried.eucplanet.data.repository.TripRepository
1212
import com.eried.eucplanet.data.repository.WheelRepository
13+
import com.eried.eucplanet.flic.FlicManager
1314
import com.eried.eucplanet.service.AutomationManager
1415
import com.eried.eucplanet.service.VoiceService
1516
import com.eried.eucplanet.service.WheelService
1617
import dagger.hilt.android.lifecycle.HiltViewModel
1718
import dagger.hilt.android.qualifiers.ApplicationContext
19+
import kotlinx.coroutines.Dispatchers
1820
import kotlinx.coroutines.flow.SharingStarted
1921
import kotlinx.coroutines.flow.StateFlow
2022
import kotlinx.coroutines.flow.map
2123
import kotlinx.coroutines.flow.stateIn
2224
import kotlinx.coroutines.launch
25+
import kotlinx.coroutines.runBlocking
2326
import javax.inject.Inject
2427

2528
data class MetricHistory(
@@ -38,13 +41,19 @@ class DashboardViewModel @Inject constructor(
3841
private val tripRepository: TripRepository,
3942
private val voiceService: VoiceService,
4043
private val automationManager: AutomationManager,
44+
private val flicManager: FlicManager,
4145
@ApplicationContext private val context: Context
4246
) : ViewModel() {
4347

4448
companion object {
4549
private const val SPARKLINE_SIZE = 300 // 5 minutes at 1 sample/sec
4650
}
4751

52+
// Synchronous initial settings read so StateFlows start with the user's persisted values
53+
// instead of hardcoded defaults (prevents a visible flash on app open).
54+
private val initialSettings: com.eried.eucplanet.data.model.AppSettings =
55+
runBlocking(Dispatchers.IO) { settingsRepository.get() }
56+
4857
val wheelData: StateFlow<com.eried.eucplanet.data.model.WheelData> = wheelRepository.wheelData
4958

5059
val connectionState: StateFlow<ConnectionState> = wheelRepository.connectionState
@@ -58,21 +67,67 @@ class DashboardViewModel @Inject constructor(
5867
val tripCount: StateFlow<Int> = tripRepository.tripCount
5968
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
6069

70+
// Newest trip id (first row of trips sorted by startTime DESC), or null if none.
71+
val latestTripId: StateFlow<Long?> = tripRepository.allTrips
72+
.map { it.firstOrNull()?.id }
73+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
74+
75+
val currentTripId: StateFlow<Long?> = tripRepository.currentTripId
76+
6177
val tiltbackSpeed: StateFlow<Float> = settingsRepository.settings
6278
.map { it.tiltbackSpeedKmh }
63-
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 50f)
79+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.tiltbackSpeedKmh)
6480

6581
val safetyTiltbackSpeed: StateFlow<Float> = settingsRepository.settings
6682
.map { it.safetyTiltbackKmh }
67-
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 25f)
83+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.safetyTiltbackKmh)
6884

6985
val imperialUnits: StateFlow<Boolean> = settingsRepository.settings
7086
.map { it.imperialUnits }
71-
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
87+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.imperialUnits)
7288

7389
val accentKey: StateFlow<String> = settingsRepository.settings
7490
.map { it.accentColor }
75-
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), com.eried.eucplanet.ui.theme.AccentKeyDefault)
91+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.accentColor)
92+
93+
val showGaugeColorBand: StateFlow<Boolean> = settingsRepository.settings
94+
.map { it.showGaugeColorBand }
95+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.showGaugeColorBand)
96+
97+
// Orange/red threshold percentages for the gauge color band (both 0-100).
98+
val gaugeOrangePct: StateFlow<Int> = settingsRepository.settings
99+
.map { it.gaugeOrangeThresholdPct }
100+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.gaugeOrangeThresholdPct)
101+
102+
val gaugeRedPct: StateFlow<Int> = settingsRepository.settings
103+
.map { it.gaugeRedThresholdPct }
104+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.gaugeRedThresholdPct)
105+
106+
val currentDisplayMode: StateFlow<String> = settingsRepository.settings
107+
.map { it.currentDisplayMode }
108+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.currentDisplayMode)
109+
110+
val hasFlicConfigured: StateFlow<Boolean> = settingsRepository.settings
111+
.map { it.flic1Address != null || it.flic2Address != null }
112+
.stateIn(viewModelScope, SharingStarted.Eagerly, initialSettings.flic1Address != null || initialSettings.flic2Address != null)
113+
114+
val flicFlashAt: StateFlow<Long> = flicManager.lastActionAt
115+
116+
fun toggleCurrentDisplayMode() {
117+
viewModelScope.launch {
118+
val current = settingsRepository.get()
119+
val next = if (current.currentDisplayMode == "WATTS") "AMPS" else "WATTS"
120+
settingsRepository.update(current.copy(currentDisplayMode = next))
121+
}
122+
}
123+
124+
fun startRecording() {
125+
viewModelScope.launch { tripRepository.startRecording() }
126+
}
127+
128+
fun stopRecording() {
129+
viewModelScope.launch { tripRepository.stopRecording() }
130+
}
76131

77132
val modelName: StateFlow<String?> = wheelRepository.modelName
78133

app/src/main/java/com/eried/eucplanet/ui/navigation/NavGraph.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ import com.eried.eucplanet.util.MultipleEventsCutter
3030
sealed class Screen(val route: String) {
3131
data object Dashboard : Screen("dashboard")
3232
data object Scan : Screen("scan")
33-
data object Settings : Screen("settings")
33+
data object Settings : Screen("settings?tab={tab}") {
34+
fun createRoute(tab: Int?) = if (tab == null) "settings" else "settings?tab=$tab"
35+
}
3436
data object Recording : Screen("recording")
3537
data object Flic : Screen("flic")
3638
data object TripDetail : Screen("trip_detail/{tripId}") {
@@ -68,8 +70,14 @@ fun NavGraph(navController: NavHostController) {
6870
composable(Screen.Dashboard.route) {
6971
DashboardScreen(
7072
onNavigateToScan = { navController.navigateSingle(Screen.Scan.route) },
71-
onNavigateToSettings = { navController.navigateSingle(Screen.Settings.route) },
73+
onNavigateToSettings = { tab ->
74+
navController.navigateSingle(Screen.Settings.createRoute(tab))
75+
},
7276
onNavigateToRecording = { navController.navigateSingle(Screen.Recording.route) },
77+
onNavigateToFlic = { navController.navigateSingle(Screen.Settings.createRoute(7)) },
78+
onNavigateToTripDetail = { tripId ->
79+
navController.navigateSingle(Screen.TripDetail.createRoute(tripId))
80+
},
7381
onNavigateToMetric = { metric ->
7482
navController.navigateSingle(Screen.MetricDetail.createRoute(metric))
7583
}
@@ -81,10 +89,18 @@ fun NavGraph(navController: NavHostController) {
8189
onBack = { navController.popSingle() }
8290
)
8391
}
84-
composable(Screen.Settings.route) {
92+
composable(
93+
Screen.Settings.route,
94+
arguments = listOf(navArgument("tab") {
95+
type = NavType.IntType
96+
defaultValue = 0
97+
})
98+
) { backStackEntry ->
99+
val tab = backStackEntry.arguments?.getInt("tab") ?: 0
85100
SettingsScreen(
86101
onBack = { navController.popSingle() },
87-
onNavigateToFlic = { navController.navigateSingle(Screen.Flic.route) }
102+
onNavigateToFlic = { navController.navigateSingle(Screen.Flic.route) },
103+
initialTab = tab
88104
)
89105
}
90106
composable(Screen.Flic.route) {
@@ -106,9 +122,10 @@ fun NavGraph(navController: NavHostController) {
106122
) { backStackEntry ->
107123
val tripId = backStackEntry.arguments?.getLong("tripId")
108124
val recordingEntry = remember(backStackEntry) {
109-
navController.getBackStackEntry(Screen.Recording.route)
125+
runCatching { navController.getBackStackEntry(Screen.Recording.route) }.getOrNull()
110126
}
111-
val viewModel: RecordingViewModel = hiltViewModel(recordingEntry)
127+
val viewModel: RecordingViewModel =
128+
if (recordingEntry != null) hiltViewModel(recordingEntry) else hiltViewModel()
112129
val trips by viewModel.trips.collectAsState()
113130
val trip = tripId?.let { id -> trips.find { it.id == id } }
114131
var notFound by remember { mutableStateOf(false) }

0 commit comments

Comments
 (0)