Skip to content

Commit 961184a

Browse files
Implement settings persistence, improve pager navigation, and fix UI imports
- Persist visualizer, overlay, and idle settings via SharedPreferences in `MainViewModel`. - Overhaul `HorizontalPager` scrolling with spring animations and improved haptic feedback logic. - Initialize dynamic gain and strobe settings in `AudioCaptureService` from preferences. - Fix Bluetooth device detection logic for older Android versions. - Clean up UI component imports and references in `AudioSetupScreen` and `GlyphSettingsScreen`.
1 parent 9798986 commit 961184a

5 files changed

Lines changed: 165 additions & 41 deletions

File tree

app/src/main/java/com/better/nothing/music/vizualizer/service/AudioCaptureService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,13 @@ public void onCreate() {
424424
mOverlayHeight = appPrefs.getInt("overlay_height", 12);
425425
mOverlayYOffset = appPrefs.getInt("overlay_y_offset", 2);
426426

427+
boolean dynamicGainEnabled = appPrefs.getBoolean("dynamic_gain_enabled", true);
428+
if (mAudioProcessor != null) {
429+
mAudioProcessor.setAutoGainEnabled(dynamicGainEnabled);
430+
}
431+
427432
mGlyphRenderer = new GlyphRenderer(mGamma, mIdleBreathingEnabled, mNotificationFlashEnabled, mSelectedDevice);
433+
mGlyphRenderer.setStrobeEnabled(appPrefs.getBoolean("strobe_enabled", false));
428434
mGlyphRenderer.setMaxBrightness(mMaxBrightness);
429435
float spectrumGain = appPrefs.getFloat("spectrum_gain", 4.0f);
430436
mGlyphRenderer.setSpectrumGain(spectrumGain);

app/src/main/java/com/better/nothing/music/vizualizer/ui/MainActivity.kt

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import androidx.activity.compose.setContent
2828
import androidx.activity.enableEdgeToEdge
2929
import androidx.activity.result.contract.ActivityResultContracts
3030
import androidx.activity.viewModels
31+
import androidx.compose.animation.core.FastOutLinearInEasing
3132
import androidx.compose.animation.core.FastOutSlowInEasing
33+
import androidx.compose.animation.core.LinearEasing
34+
import androidx.compose.animation.core.LinearOutSlowInEasing
35+
import androidx.compose.animation.core.Spring
36+
import androidx.compose.animation.core.spring
3237
import androidx.compose.animation.core.tween
3338
import androidx.compose.foundation.layout.Box
3439
import androidx.compose.foundation.layout.fillMaxSize
@@ -41,6 +46,10 @@ import androidx.compose.material3.Scaffold
4146
import androidx.compose.runtime.Composable
4247
import androidx.compose.runtime.LaunchedEffect
4348
import androidx.compose.runtime.getValue
49+
import androidx.compose.runtime.mutableStateOf
50+
import androidx.compose.runtime.remember
51+
import androidx.compose.runtime.setValue
52+
import androidx.compose.runtime.snapshotFlow
4453
import androidx.compose.ui.Modifier
4554
import androidx.compose.ui.graphics.graphicsLayer
4655
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@@ -412,10 +421,10 @@ fun AudioDeviceInfo.isBluetoothOutput(): Boolean {
412421
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
413422
type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || type == AudioDeviceInfo.TYPE_BLE_HEADSET || type == AudioDeviceInfo.TYPE_BLE_SPEAKER || type == AudioDeviceInfo.TYPE_BLE_BROADCAST
414423
} else {
415-
TODO("VERSION.SDK_INT < TIRAMISU")
424+
type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
416425
}
417426
} else {
418-
TODO("VERSION.SDK_INT < S")
427+
type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
419428
}
420429
}
421430

@@ -446,28 +455,62 @@ internal fun BetterVizApp(
446455

447456
val context = LocalContext.current
448457
val pagerState = rememberPagerState(initialPage = selectedTab.ordinal) { Tab.entries.size }
458+
var isProgrammaticScroll by remember { mutableStateOf(false) }
449459

450460
LaunchedEffect(selectedTab) {
451461
val target = selectedTab.ordinal
452-
if (pagerState.targetPage != target) {
453-
val distance = (pagerState.currentPage - target).absoluteValue
454-
val duration = (250 + distance * 80).toInt().coerceIn(250, 600)
455-
456-
pagerState.animateScrollToPage(
457-
page = target,
458-
animationSpec = tween(
459-
durationMillis = duration,
460-
easing = FastOutSlowInEasing
462+
val current = pagerState.currentPage
463+
if (current != target) {
464+
val direction = if (target > current) 1 else -1
465+
val steps = (target - current).absoluteValue
466+
467+
if (steps > 2) {
468+
isProgrammaticScroll = true
469+
try {
470+
for (i in 1 until steps) {
471+
val isFirstStep = i == 1
472+
val duration = if (isFirstStep) 200 else 100
473+
val easing = if (isFirstStep) FastOutLinearInEasing else LinearEasing
474+
475+
pagerState.animateScrollToPage(
476+
page = current + (i * direction),
477+
animationSpec = tween(durationMillis = duration, easing = easing)
478+
)
479+
}
480+
pagerState.animateScrollToPage(
481+
page = target,
482+
animationSpec = spring(
483+
dampingRatio = Spring.DampingRatioLowBouncy,
484+
stiffness = Spring.StiffnessMediumLow
485+
)
486+
)
487+
} finally {
488+
isProgrammaticScroll = false
489+
}
490+
} else {
491+
pagerState.animateScrollToPage(
492+
page = target,
493+
animationSpec = spring(
494+
dampingRatio = Spring.DampingRatioLowBouncy,
495+
stiffness = Spring.StiffnessMediumLow
496+
)
461497
)
462-
)
498+
}
463499
}
464500
}
465501

466502
val haptics = LocalHapticFeedback.current
467-
LaunchedEffect(pagerState.targetPage) {
468-
if (pagerState.targetPage != selectedTab.ordinal) {
503+
LaunchedEffect(pagerState) {
504+
snapshotFlow { pagerState.currentPage }.collect {
469505
haptics.performHapticFeedback(HapticFeedbackType.SegmentTick)
470-
viewModel.selectTab(Tab.entries[pagerState.targetPage])
506+
}
507+
}
508+
509+
LaunchedEffect(pagerState) {
510+
snapshotFlow { pagerState.settledPage }.collect { page ->
511+
if (!isProgrammaticScroll && viewModel.selectedTab.value.ordinal != page) {
512+
viewModel.selectTab(Tab.entries[page])
513+
}
471514
}
472515
}
473516

@@ -497,11 +540,11 @@ internal fun BetterVizApp(
497540
userScrollEnabled = true
498541
) { page ->
499542
val tab = Tab.entries[page]
500-
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
501543
Box(
502544
modifier = Modifier
503545
.fillMaxSize()
504546
.graphicsLayer {
547+
val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction
505548
val absOffset = pageOffset.coerceIn(-1f, 1f).let { kotlin.math.abs(it) }
506549
val fraction = 1f - absOffset
507550

@@ -513,7 +556,6 @@ internal fun BetterVizApp(
513556
val maxRotation = 8f
514557
val rotationAmount = maxRotation * (1f - fraction)
515558

516-
// 4. Use the original pageOffset sign to choose +20 or -20
517559
rotationZ = if (pageOffset > 0) -rotationAmount else rotationAmount
518560
}
519561
) {

app/src/main/java/com/better/nothing/music/vizualizer/ui/MainViewModel.kt

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,36 +122,64 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
122122
fun setDynamicGainEnabled(enabled: Boolean) {
123123
_dynamicGainEnabled.value = enabled
124124
MainActivity.serviceStatic?.setDynamicGainEnabled(enabled)
125+
viewModelScope.launch(Dispatchers.IO) {
126+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
127+
.edit { putBoolean("dynamic_gain_enabled", enabled) }
128+
}
125129
}
126130

127131
private val _overlayWidth = MutableStateFlow(120)
128132
val overlayWidth = _overlayWidth.asStateFlow()
129133
fun setOverlayWidth(width: Int) {
130134
_overlayWidth.value = width
131135
MainActivity.serviceStatic?.setOverlayWidth(width)
136+
viewModelScope.launch(Dispatchers.IO) {
137+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
138+
.edit { putInt("overlay_width", width) }
139+
}
132140
}
133141

134142
private val _overlayHeight = MutableStateFlow(12)
135143
val overlayHeight = _overlayHeight.asStateFlow()
136144
fun setOverlayHeight(height: Int) {
137145
_overlayHeight.value = height
138146
MainActivity.serviceStatic?.setOverlayHeight(height)
147+
viewModelScope.launch(Dispatchers.IO) {
148+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
149+
.edit { putInt("overlay_height", height) }
150+
}
139151
}
140152

141153
private val _overlayYOffset = MutableStateFlow(2)
142154
val overlayYOffset = _overlayYOffset.asStateFlow()
143155
fun setOverlayYOffset(offset: Int) {
144156
_overlayYOffset.value = offset
145157
MainActivity.serviceStatic?.setOverlayYOffset(offset)
158+
viewModelScope.launch(Dispatchers.IO) {
159+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
160+
.edit { putInt("overlay_y_offset", offset) }
161+
}
146162
}
147163

148164
private val _selectedTheme = MutableStateFlow("Default")
149165
val selectedTheme = _selectedTheme.asStateFlow()
150-
fun setSelectedTheme(theme: String) { _selectedTheme.value = theme }
166+
fun setSelectedTheme(theme: String) {
167+
_selectedTheme.value = theme
168+
viewModelScope.launch(Dispatchers.IO) {
169+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
170+
.edit { putString("selected_theme", theme) }
171+
}
172+
}
151173

152174
private val _selectedFont = MutableStateFlow("Default")
153175
val selectedFont = _selectedFont.asStateFlow()
154-
fun setSelectedFont(font: String) { _selectedFont.value = font }
176+
fun setSelectedFont(font: String) {
177+
_selectedFont.value = font
178+
viewModelScope.launch(Dispatchers.IO) {
179+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
180+
.edit { putString("selected_font", font) }
181+
}
182+
}
155183

156184
fun checkAppUpdate() {
157185
_appUpdateStatus.value = AppUpdateStatus.UpToDate
@@ -463,6 +491,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
463491
fun setOverlayEnabled(enabled: Boolean) {
464492
_overlayEnabled.value = enabled
465493
MainActivity.serviceStatic?.setOverlayEnabled(enabled)
494+
viewModelScope.launch(Dispatchers.IO) {
495+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
496+
.edit { putBoolean("overlay_enabled", enabled) }
497+
}
466498
}
467499

468500
val _idleBreathingEnabled = MutableStateFlow(false)
@@ -471,6 +503,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
471503
fun setIdleBreathingEnabled(enabled: Boolean) {
472504
_idleBreathingEnabled.value = enabled
473505
MainActivity.serviceStatic?.setIdleBreathingEnabled(enabled)
506+
viewModelScope.launch(Dispatchers.IO) {
507+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
508+
.edit { putBoolean("idle_breathing_enabled", enabled) }
509+
}
474510
}
475511

476512
val _idlePattern = MutableStateFlow("pulse")
@@ -479,6 +515,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
479515
fun setIdlePattern(pattern: String) {
480516
_idlePattern.value = pattern
481517
MainActivity.serviceStatic?.setIdlePattern(pattern)
518+
viewModelScope.launch(Dispatchers.IO) {
519+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
520+
.edit { putString("idle_pattern", pattern) }
521+
}
482522
}
483523

484524
val _notificationFlashEnabled = MutableStateFlow(false)
@@ -487,6 +527,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
487527
fun setNotificationFlashEnabled(enabled: Boolean) {
488528
_notificationFlashEnabled.value = enabled
489529
MainActivity.serviceStatic?.setNotificationFlashEnabled(enabled)
530+
viewModelScope.launch(Dispatchers.IO) {
531+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
532+
.edit { putBoolean("notification_flash_enabled", enabled) }
533+
}
490534
}
491535

492536
val _strobeEnabled = MutableStateFlow(false)
@@ -495,6 +539,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
495539
fun setStrobeEnabled(enabled: Boolean) {
496540
_strobeEnabled.value = enabled
497541
MainActivity.serviceStatic?.setStrobeEnabled(enabled)
542+
viewModelScope.launch(Dispatchers.IO) {
543+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
544+
.edit { putBoolean("strobe_enabled", enabled) }
545+
}
498546
}
499547

500548
val _disableGlyphsWhenSilent = MutableStateFlow(false)
@@ -503,6 +551,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
503551
fun setDisableGlyphsWhenSilent(enabled: Boolean) {
504552
_disableGlyphsWhenSilent.value = enabled
505553
MainActivity.serviceStatic?.setDisableGlyphsWhenSilent(enabled)
554+
viewModelScope.launch(Dispatchers.IO) {
555+
ctx.getSharedPreferences("viz_prefs", Context.MODE_PRIVATE)
556+
.edit { putBoolean("disable_glyphs_when_silent", enabled) }
557+
}
506558
}
507559

508560
val _musicThemeColor = MutableStateFlow(Color.White)
@@ -1345,6 +1397,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
13451397
_spectrumGain.value = prefs.getFloat("spectrum_gain", 1.0f)
13461398
_maxBrightness.value = prefs.getInt("max_brightness", 4095)
13471399
_selectedPreset.value = prefs.getString("selected_preset", "Default") ?: "Default"
1400+
_selectedTheme.value = prefs.getString("selected_theme", "Default") ?: "Default"
1401+
_selectedFont.value = prefs.getString("selected_font", "Default") ?: "Default"
1402+
_dynamicGainEnabled.value = prefs.getBoolean("dynamic_gain_enabled", true)
13481403

13491404
_hapticMotorEnabled.value = prefs.getBoolean("haptic_motor_enabled", false)
13501405
_hapticMode.value = HapticMode.valueOf(prefs.getString("haptic_mode", HapticMode.BASS_TO_AMPLITUDE.name)!!)
@@ -1362,6 +1417,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
13621417
_flashlightFreqMax.value = prefs.getInt("flashlight_freq_max", 250).toFloat()
13631418
_flashlightThreshold.value = prefs.getFloat("flashlight_threshold", 0.15f)
13641419
_flashlightBeatSensitivity.value = prefs.getFloat("flashlight_beat_sensitivity", 1.5f)
1420+
1421+
_idleBreathingEnabled.value = prefs.getBoolean("idle_breathing_enabled", false)
1422+
_idlePattern.value = prefs.getString("idle_pattern", "pulse") ?: "pulse"
1423+
_notificationFlashEnabled.value = prefs.getBoolean("notification_flash_enabled", false)
1424+
_strobeEnabled.value = prefs.getBoolean("strobe_enabled", false)
1425+
_disableGlyphsWhenSilent.value = prefs.getBoolean("disable_glyphs_when_silent", false)
1426+
_overlayEnabled.value = prefs.getBoolean("overlay_enabled", false)
1427+
_overlayWidth.value = prefs.getInt("overlay_width", 120)
1428+
_overlayHeight.value = prefs.getInt("overlay_height", 12)
1429+
_overlayYOffset.value = prefs.getInt("overlay_y_offset", 2)
1430+
13651431
reloadFlashlightSpeedForLevels()
13661432

13671433
updateSelectedDevice()

0 commit comments

Comments
 (0)