Auto commit Sat Mar 21 10:01:01 PM CDT 2026
This commit is contained in:
@@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.mindmachine.mvp.audio.BinauralAudioEngine
|
||||
import com.mindmachine.mvp.audio.HeadsetMonitor
|
||||
import com.mindmachine.mvp.data.SettingsRepository
|
||||
import com.mindmachine.mvp.data.UserProgramEntity
|
||||
import com.mindmachine.mvp.data.UserProgramRepository
|
||||
import com.mindmachine.mvp.domain.AppSettings
|
||||
import com.mindmachine.mvp.domain.CountdownPreference
|
||||
import com.mindmachine.mvp.domain.Presets
|
||||
@@ -19,8 +21,10 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
const val SAFETY_VERSION = 1
|
||||
|
||||
@@ -46,6 +50,7 @@ data class UiState(
|
||||
|
||||
class MainViewModel(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val userProgramRepository: UserProgramRepository,
|
||||
private val headsetMonitor: HeadsetMonitor,
|
||||
private val audioEngine: BinauralAudioEngine,
|
||||
) : ViewModel() {
|
||||
@@ -54,34 +59,136 @@ class MainViewModel(
|
||||
|
||||
private var runJob: Job? = null
|
||||
private var elapsedBeforePauseSec: Float = 0f
|
||||
private var cachedUserPrograms: List<UserProgramEntity> = emptyList()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.settings.collect { s ->
|
||||
combine(settingsRepository.settings, userProgramRepository.userPrograms) { settings, userPrograms ->
|
||||
settings to userPrograms
|
||||
}.collect { (settings, userPrograms) ->
|
||||
cachedUserPrograms = userPrograms
|
||||
val userPresets = userPrograms.map {
|
||||
TimelineProgramFactory.toPreset(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
timeline = it.timeline,
|
||||
sortOrder = it.sortOrder,
|
||||
isUserProgram = true,
|
||||
)
|
||||
}
|
||||
val allPresets = (Presets.builtIn + userPresets).sortedBy { it.sortOrder }
|
||||
|
||||
_ui.update { current ->
|
||||
val preset = current.presets.find { it.id == (s.lastPresetId ?: current.selectedPreset.id) } ?: current.selectedPreset
|
||||
current.copy(settings = s, selectedPreset = preset, config = current.config.copy(mode = s.defaultModePreference))
|
||||
val selectedId = settings.lastPresetId ?: current.selectedPreset.id
|
||||
val selectedPreset = allPresets.find { it.id == selectedId } ?: allPresets.first()
|
||||
val sameSelection = selectedPreset.id == current.selectedPreset.id
|
||||
current.copy(
|
||||
settings = settings,
|
||||
presets = allPresets,
|
||||
selectedPreset = selectedPreset,
|
||||
config = current.config.copy(mode = settings.defaultModePreference),
|
||||
timeline = if (sameSelection) current.timeline else timelineForPreset(selectedPreset, userPrograms),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) }
|
||||
|
||||
fun choosePreset(id: String) = viewModelScope.launch {
|
||||
val preset = _ui.value.presets.first { it.id == id }
|
||||
val state = _ui.value
|
||||
val preset = state.presets.first { it.id == id }
|
||||
settingsRepository.updateLastPreset(id)
|
||||
_ui.update {
|
||||
val cfg = preset.toConfig(it.settings.defaultModePreference)
|
||||
it.copy(
|
||||
selectedPreset = preset,
|
||||
config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)),
|
||||
timeline = TimelineProgramFactory.fromPreset(preset),
|
||||
timeline = timelineForPreset(preset, cachedUserPrograms),
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
elapsedBeforePauseSec = 0f
|
||||
}
|
||||
|
||||
fun createNewProgramDraft() {
|
||||
val draftId = "user-${UUID.randomUUID()}"
|
||||
val displayName = nextUntitledName()
|
||||
val timeline = TimelineEditorState.default(20 * 60)
|
||||
val preset = TimelineProgramFactory.toPreset(
|
||||
id = draftId,
|
||||
name = displayName,
|
||||
description = "Custom program",
|
||||
timeline = timeline,
|
||||
sortOrder = nextSortOrder(),
|
||||
isUserProgram = true,
|
||||
)
|
||||
_ui.update {
|
||||
it.copy(
|
||||
selectedPreset = preset,
|
||||
config = preset.toConfig(it.settings.defaultModePreference),
|
||||
timeline = timeline,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCurrentProgram(name: String) = viewModelScope.launch {
|
||||
val trimmed = name.trim()
|
||||
if (trimmed.isBlank()) {
|
||||
_ui.update { it.copy(error = "Enter a name before saving.") }
|
||||
return@launch
|
||||
}
|
||||
|
||||
val state = _ui.value
|
||||
val id = if (state.selectedPreset.isUserProgram) state.selectedPreset.id else "user-${UUID.randomUUID()}"
|
||||
val sortOrder = cachedUserPrograms.find { it.id == id }?.sortOrder ?: nextSortOrder()
|
||||
|
||||
userProgramRepository.upsert(
|
||||
UserProgramEntity(
|
||||
id = id,
|
||||
name = trimmed,
|
||||
description = "Custom program",
|
||||
timeline = state.timeline,
|
||||
sortOrder = sortOrder,
|
||||
)
|
||||
)
|
||||
settingsRepository.updateLastPreset(id)
|
||||
|
||||
_ui.update {
|
||||
val savedPreset = TimelineProgramFactory.toPreset(
|
||||
id = id,
|
||||
name = trimmed,
|
||||
description = "Custom program",
|
||||
timeline = it.timeline,
|
||||
sortOrder = sortOrder,
|
||||
isUserProgram = true,
|
||||
)
|
||||
it.copy(
|
||||
selectedPreset = savedPreset,
|
||||
error = "Saved \"$trimmed\"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteProgram(id: String) = viewModelScope.launch {
|
||||
userProgramRepository.delete(id)
|
||||
if (_ui.value.selectedPreset.id == id) {
|
||||
val fallback = Presets.builtIn.first()
|
||||
settingsRepository.updateLastPreset(fallback.id)
|
||||
_ui.update {
|
||||
it.copy(
|
||||
selectedPreset = fallback,
|
||||
config = fallback.toConfig(it.settings.defaultModePreference),
|
||||
timeline = TimelineProgramFactory.fromPreset(fallback),
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
||||
|
||||
fun setDurationSec(seconds: Int) = _ui.update {
|
||||
@@ -101,7 +208,6 @@ class MainViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
// Legacy fixed-parameter setters kept for now but no longer used by Setup.
|
||||
fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) }
|
||||
fun setCarrier(value: Float) = _ui.update { it.copy(config = it.config.copy(carrierFrequencyHz = value.coerceIn(80f, 400f)), error = null) }
|
||||
fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) }
|
||||
@@ -252,11 +358,29 @@ class MainViewModel(
|
||||
return (total - state.remainingSec).coerceIn(0f, total)
|
||||
}
|
||||
|
||||
private fun timelineForPreset(preset: SessionPreset, userPrograms: List<UserProgramEntity>): TimelineEditorState {
|
||||
return userPrograms.firstOrNull { it.id == preset.id }?.timeline ?: TimelineProgramFactory.fromPreset(preset)
|
||||
}
|
||||
|
||||
private fun nextSortOrder(): Int = (cachedUserPrograms.maxOfOrNull { it.sortOrder } ?: 999) + 1
|
||||
|
||||
private fun nextUntitledName(): String {
|
||||
val names = cachedUserPrograms.map { it.name.lowercase() }.toSet()
|
||||
var i = 1
|
||||
while (true) {
|
||||
val candidate = "Custom $i"
|
||||
if (candidate.lowercase() !in names) return candidate
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val userProgramRepository: UserProgramRepository,
|
||||
private val headsetMonitor: HeadsetMonitor,
|
||||
private val audioEngine: BinauralAudioEngine,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = MainViewModel(settingsRepository, headsetMonitor, audioEngine) as T
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
MainViewModel(settingsRepository, userProgramRepository, headsetMonitor, audioEngine) as T
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user