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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.weight
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -321,169 +320,167 @@ internal fun TimelineGraph(
|
||||
var curveRight = currentCurveRight
|
||||
val w = size.width.toFloat()
|
||||
val h = size.height.toFloat()
|
||||
val startX = viewport.timeToX(selection.startSec, w)
|
||||
val endX = viewport.timeToX(selection.endSec, w)
|
||||
val hitPx = 32f
|
||||
|
||||
var activePointerId = down.id
|
||||
var lastPos = down.position
|
||||
var isTransform = false
|
||||
var moved = false
|
||||
var activePointerId = down.id
|
||||
var lastPos = down.position
|
||||
var isTransform = false
|
||||
var moved = false
|
||||
|
||||
val hits = buildList {
|
||||
curveLeft.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
||||
val point = curveLeft.points[idx]
|
||||
val x = viewport.timeToX(point.tSec, w)
|
||||
val y = valueToY(point.value01, h)
|
||||
val dx = down.position.x - x
|
||||
val dy = down.position.y - y
|
||||
add(Triple(leftId, idx, dx * dx + dy * dy))
|
||||
val startX = viewport.timeToX(selection.startSec, w)
|
||||
val endX = viewport.timeToX(selection.endSec, w)
|
||||
val hits = buildList {
|
||||
curveLeft.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
||||
val point = curveLeft.points[idx]
|
||||
val x = viewport.timeToX(point.tSec, w)
|
||||
val y = valueToY(point.value01, h)
|
||||
val dx = down.position.x - x
|
||||
val dy = down.position.y - y
|
||||
add(Triple(leftId, idx, dx * dx + dy * dy))
|
||||
}
|
||||
curveRight.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
||||
val point = curveRight.points[idx]
|
||||
val x = viewport.timeToX(point.tSec, w)
|
||||
val y = valueToY(point.value01, h)
|
||||
val dx = down.position.x - x
|
||||
val dy = down.position.y - y
|
||||
add(Triple(rightId, idx, dx * dx + dy * dy))
|
||||
}
|
||||
}
|
||||
curveRight.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
||||
val point = curveRight.points[idx]
|
||||
val x = viewport.timeToX(point.tSec, w)
|
||||
val y = valueToY(point.value01, h)
|
||||
val dx = down.position.x - x
|
||||
val dy = down.position.y - y
|
||||
add(Triple(rightId, idx, dx * dx + dy * dy))
|
||||
}
|
||||
}
|
||||
|
||||
val nearestHit = hits.minByOrNull { it.third }
|
||||
if (nearestHit != null) {
|
||||
draggingCurveId = nearestHit.first
|
||||
draggingPointIndex = nearestHit.second
|
||||
dragMode = HandleDrag.NONE
|
||||
currentOnTapSideSelect(if (nearestHit.first == leftId) Side.LEFT else Side.RIGHT)
|
||||
} else {
|
||||
draggingPointIndex = null
|
||||
draggingCurveId = null
|
||||
dragMode = when {
|
||||
abs(down.position.x - startX) <= hitPx -> HandleDrag.START
|
||||
abs(down.position.x - endX) <= hitPx -> HandleDrag.END
|
||||
else -> HandleDrag.NONE
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
val pressed = event.changes.filter { it.pressed }
|
||||
if (pressed.isEmpty()) break
|
||||
|
||||
if (pressed.size >= 2) {
|
||||
isTransform = true
|
||||
val nearestHit = hits.minByOrNull { it.third }
|
||||
if (nearestHit != null) {
|
||||
draggingCurveId = nearestHit.first
|
||||
draggingPointIndex = nearestHit.second
|
||||
dragMode = HandleDrag.NONE
|
||||
currentOnTapSideSelect(if (nearestHit.first == leftId) Side.LEFT else Side.RIGHT)
|
||||
} else {
|
||||
draggingPointIndex = null
|
||||
draggingCurveId = null
|
||||
dragMode = HandleDrag.NONE
|
||||
} else if (pressed.size == 1 && isTransform) {
|
||||
// Single finger after multi-finger: treat as single-finger drag
|
||||
isTransform = false
|
||||
dragMode = when {
|
||||
abs(down.position.x - startX) <= hitPx -> HandleDrag.START
|
||||
abs(down.position.x - endX) <= hitPx -> HandleDrag.END
|
||||
else -> HandleDrag.NONE
|
||||
}
|
||||
}
|
||||
|
||||
if (isTransform) {
|
||||
val zoom = event.calculateZoom()
|
||||
val pan = event.calculatePan()
|
||||
val centroid = event.calculateCentroid(useCurrent = true)
|
||||
if (zoom != 1f || pan.x != 0f || pan.y != 0f) {
|
||||
moved = true
|
||||
val newViewport = viewport.applyZoomPan(
|
||||
zoomChange = zoom,
|
||||
panPx = pan.x,
|
||||
widthPx = size.width.toFloat(),
|
||||
centroidPx = centroid.x,
|
||||
)
|
||||
viewport = newViewport
|
||||
currentOnViewportChanged(newViewport)
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||
val pressed = event.changes.filter { it.pressed }
|
||||
if (pressed.isEmpty()) break
|
||||
|
||||
if (pressed.size >= 2) {
|
||||
isTransform = true
|
||||
draggingPointIndex = null
|
||||
draggingCurveId = null
|
||||
dragMode = HandleDrag.NONE
|
||||
} else if (pressed.size == 1 && isTransform) {
|
||||
isTransform = false
|
||||
lastPos = pressed.first().position
|
||||
}
|
||||
event.changes.forEach { it.consume() }
|
||||
continue
|
||||
}
|
||||
|
||||
val primary = pressed.firstOrNull { it.id == activePointerId } ?: pressed.first()
|
||||
activePointerId = primary.id
|
||||
val delta = primary.position - lastPos
|
||||
lastPos = primary.position
|
||||
|
||||
if (delta.getDistanceSquared() > 0.5f) moved = true
|
||||
|
||||
val pointIndex = draggingPointIndex
|
||||
val curveId = draggingCurveId
|
||||
if (pointIndex != null && curveId != null) {
|
||||
val targetCurve = if (curveId == leftId) curveLeft else curveRight
|
||||
val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h)
|
||||
if (curveId == leftId) curveLeft = updated else curveRight = updated
|
||||
currentOnUpdateCurve(curveId, updated)
|
||||
onCurveEditedHaptic()
|
||||
primary.consume()
|
||||
continue
|
||||
}
|
||||
|
||||
val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0)
|
||||
when (dragMode) {
|
||||
HandleDrag.START -> {
|
||||
val newSelection = TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)
|
||||
selection = newSelection
|
||||
currentOnSelectionChanged(newSelection)
|
||||
primary.consume()
|
||||
}
|
||||
HandleDrag.END -> {
|
||||
val newSelection = TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))
|
||||
selection = newSelection
|
||||
currentOnSelectionChanged(newSelection)
|
||||
primary.consume()
|
||||
}
|
||||
HandleDrag.NONE -> {
|
||||
if (delta.x != 0f) {
|
||||
if (isTransform) {
|
||||
val zoom = event.calculateZoom()
|
||||
val pan = event.calculatePan()
|
||||
val centroid = event.calculateCentroid(useCurrent = true)
|
||||
if (zoom != 1f || pan.x != 0f || pan.y != 0f) {
|
||||
moved = true
|
||||
val newViewport = viewport.applyZoomPan(
|
||||
zoomChange = 1f,
|
||||
panPx = delta.x,
|
||||
zoomChange = zoom,
|
||||
panPx = pan.x,
|
||||
widthPx = w,
|
||||
centroidPx = w / 2f,
|
||||
centroidPx = centroid.x,
|
||||
)
|
||||
viewport = newViewport
|
||||
currentOnViewportChanged(newViewport)
|
||||
}
|
||||
event.changes.forEach { it.consume() }
|
||||
continue
|
||||
}
|
||||
|
||||
val primary = pressed.firstOrNull { it.id == activePointerId } ?: pressed.first()
|
||||
activePointerId = primary.id
|
||||
val delta = primary.position - lastPos
|
||||
lastPos = primary.position
|
||||
|
||||
if (delta.getDistanceSquared() > 0.5f) moved = true
|
||||
|
||||
val pointIndex = draggingPointIndex
|
||||
val curveId = draggingCurveId
|
||||
if (pointIndex != null && curveId != null) {
|
||||
val targetCurve = if (curveId == leftId) curveLeft else curveRight
|
||||
val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h)
|
||||
if (curveId == leftId) curveLeft = updated else curveRight = updated
|
||||
currentOnUpdateCurve(curveId, updated)
|
||||
onCurveEditedHaptic()
|
||||
primary.consume()
|
||||
continue
|
||||
}
|
||||
|
||||
val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0)
|
||||
when (dragMode) {
|
||||
HandleDrag.START -> {
|
||||
val newSelection = TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)
|
||||
selection = newSelection
|
||||
currentOnSelectionChanged(newSelection)
|
||||
primary.consume()
|
||||
}
|
||||
HandleDrag.END -> {
|
||||
val newSelection = TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))
|
||||
selection = newSelection
|
||||
currentOnSelectionChanged(newSelection)
|
||||
primary.consume()
|
||||
}
|
||||
HandleDrag.NONE -> {
|
||||
if (delta.x != 0f) {
|
||||
val newViewport = viewport.applyZoomPan(
|
||||
zoomChange = 1f,
|
||||
panPx = delta.x,
|
||||
widthPx = w,
|
||||
centroidPx = w / 2f,
|
||||
)
|
||||
viewport = newViewport
|
||||
currentOnViewportChanged(newViewport)
|
||||
primary.consume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!moved && !isTransform) {
|
||||
val side = if (down.position.x < size.width / 2f) Side.LEFT else Side.RIGHT
|
||||
currentOnTapSideSelect(side)
|
||||
if (!moved && !isTransform) {
|
||||
val side = if (down.position.x < size.width / 2f) Side.LEFT else Side.RIGHT
|
||||
currentOnTapSideSelect(side)
|
||||
}
|
||||
draggingPointIndex = null
|
||||
draggingCurveId = null
|
||||
dragMode = HandleDrag.NONE
|
||||
}
|
||||
draggingPointIndex = null
|
||||
draggingCurveId = null
|
||||
dragMode = HandleDrag.NONE
|
||||
}
|
||||
}
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawGrid()
|
||||
|
||||
val startX = viewport.timeToX(selection.startSec, size.width)
|
||||
val endX = viewport.timeToX(selection.endSec, size.width)
|
||||
drawRect(
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
topLeft = Offset(min(startX, endX), 0f),
|
||||
size = androidx.compose.ui.geometry.Size(abs(endX - startX), size.height)
|
||||
)
|
||||
drawLine(Color.White.copy(alpha = 0.35f), Offset(startX, 0f), Offset(startX, size.height), strokeWidth = 3f)
|
||||
drawLine(Color.White.copy(alpha = 0.35f), Offset(endX, 0f), Offset(endX, size.height), strokeWidth = 3f)
|
||||
|
||||
val handleColor = Color.White.copy(alpha = 0.9f)
|
||||
drawCircle(handleColor, radius = 10f, center = Offset(startX, size.height - 14f))
|
||||
drawCircle(handleColor, radius = 10f, center = Offset(endX, size.height - 14f))
|
||||
|
||||
val playX = viewport.timeToX(playheadSec.toInt(), size.width)
|
||||
drawLine(Color(0xFFFFFFFF).copy(alpha = 0.6f), Offset(playX, 0f), Offset(playX, size.height), strokeWidth = 2f)
|
||||
|
||||
drawCurve(curveLeft, selected = selectedLeft, color = leftColor, viewport = viewport)
|
||||
drawCurve(curveRight, selected = selectedRight, color = rightColor, viewport = viewport)
|
||||
}
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
drawGrid()
|
||||
|
||||
val startX = viewport.timeToX(selection.startSec, size.width)
|
||||
val endX = viewport.timeToX(selection.endSec, size.width)
|
||||
drawRect(
|
||||
color = Color.White.copy(alpha = 0.06f),
|
||||
topLeft = Offset(min(startX, endX), 0f),
|
||||
size = androidx.compose.ui.geometry.Size(abs(endX - startX), size.height)
|
||||
)
|
||||
drawLine(Color.White.copy(alpha = 0.35f), Offset(startX, 0f), Offset(startX, size.height), strokeWidth = 3f)
|
||||
drawLine(Color.White.copy(alpha = 0.35f), Offset(endX, 0f), Offset(endX, size.height), strokeWidth = 3f)
|
||||
|
||||
val handleColor = Color.White.copy(alpha = 0.9f)
|
||||
drawCircle(handleColor, radius = 10f, center = Offset(startX, size.height - 14f))
|
||||
drawCircle(handleColor, radius = 10f, center = Offset(endX, size.height - 14f))
|
||||
|
||||
val playX = viewport.timeToX(playheadSec.toInt(), size.width)
|
||||
drawLine(Color(0xFFFFFFFF).copy(alpha = 0.6f), Offset(playX, 0f), Offset(playX, size.height), strokeWidth = 2f)
|
||||
|
||||
drawCurve(curveLeft, selected = selectedLeft, color = leftColor, viewport = viewport)
|
||||
drawCurve(curveRight, selected = selectedRight, color = rightColor, viewport = viewport)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mindmachine.mvp.session
|
||||
|
||||
import com.mindmachine.mvp.domain.SessionPreset
|
||||
import com.mindmachine.mvp.domain.VisualPattern
|
||||
|
||||
object TimelineProgramFactory {
|
||||
fun fromPreset(preset: SessionPreset): TimelineEditorState {
|
||||
@@ -36,4 +37,45 @@ object TimelineProgramFactory {
|
||||
playheadSec = 0f,
|
||||
)
|
||||
}
|
||||
|
||||
fun toPreset(
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
timeline: TimelineEditorState,
|
||||
sortOrder: Int,
|
||||
isUserProgram: Boolean,
|
||||
): SessionPreset {
|
||||
val t0 = 0f
|
||||
val flashMs = ParameterMapping.logMap01(
|
||||
timeline.visualLeft.valueAt(t0),
|
||||
ParameterRanges.FLASH_INTERVAL_MS_MIN,
|
||||
ParameterRanges.FLASH_INTERVAL_MS_MAX,
|
||||
).toInt().coerceIn(ParameterRanges.FLASH_INTERVAL_MS_MIN.toInt(), ParameterRanges.FLASH_INTERVAL_MS_MAX.toInt())
|
||||
val carrierHz = ParameterMapping.logMap01(
|
||||
timeline.audioLeft.valueAt(t0),
|
||||
ParameterRanges.CARRIER_HZ_MIN,
|
||||
ParameterRanges.CARRIER_HZ_MAX,
|
||||
)
|
||||
val binauralHz = ParameterMapping.logMap01(
|
||||
timeline.audioRight.valueAt(t0),
|
||||
ParameterRanges.BINAURAL_HZ_MIN,
|
||||
ParameterRanges.BINAURAL_HZ_MAX,
|
||||
)
|
||||
|
||||
return SessionPreset(
|
||||
id = id,
|
||||
name = name,
|
||||
description = description,
|
||||
defaultDurationSec = timeline.durationSec,
|
||||
visualPatternType = VisualPattern.FLASH,
|
||||
flashIntervalMs = flashMs,
|
||||
intensityPercent = 60,
|
||||
carrierFrequencyHz = carrierHz,
|
||||
binauralDifferenceHz = binauralHz,
|
||||
cautionNote = "Stop if discomfort occurs.",
|
||||
sortOrder = sortOrder,
|
||||
isUserProgram = isUserProgram,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user