/
vguzev
/
speed-reader
Обзор
Документация
Войти
/
vguzev
/
speed-reader
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
app/src/main/java/com/smartapps/speedreader/ui/reader/ReaderViewModel.kt
301 строка
11 KB
vguzev
First commit
05 июл 2026, 13:50
05 июл 2026, 13:50
4a0bffe
Код
Авторство
О чём код?
package com.smartapps.speedreader.ui.reader import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.smartapps.speedreader.data.model.TextItem import com.smartapps.speedreader.ui.settings.SpeedSettings import com.smartapps.speedreader.ui.settings.SpeedStep import kotlinx.coroutines.Job 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.launch data class ReaderState( val currentWordIndex: Int = 0, val isPlaying: Boolean = true, val isFinished: Boolean = false ) class ReaderViewModel : ViewModel() { private val _textItem = MutableStateFlow<TextItem?>(null) val textItem: StateFlow<TextItem?> = _textItem.asStateFlow() private val _readerState = MutableStateFlow(ReaderState()) val readerState: StateFlow<ReaderState> = _readerState.asStateFlow() private val _currentWord = MutableStateFlow("") val currentWord: StateFlow<String> = _currentWord.asStateFlow() // Speed settings - will be set from outside via updateSpeedSettings() // Load initial settings from SharedPreferences private val _speedSettings = MutableStateFlow<SpeedSettings>( SpeedSettings( baseSpeed = 60, steps = listOf( SpeedStep(5, 120), SpeedStep(10, 300), SpeedStep(20, 500) ) ) ) val speedSettings: StateFlow<SpeedSettings> = _speedSettings.asStateFlow() // Current speed state flow private val _currentSpeed = MutableStateFlow(0) val currentSpeed: StateFlow<Int> = _currentSpeed.asStateFlow() private var readingJob: Job? = null private var speedUpdateJob: Job? = null private var startTime: Long = 0 // State for preservation across configuration changes private var savedCurrentWordIndex: Int = 0 private var savedIsPlaying: Boolean = true private var savedStartTime: Long = 0 private var savedTextItem: TextItem? = null fun setText(textItem: TextItem) { println("[ReaderViewModel] setText CALLED - textId=${textItem.id}, current speed settings: baseSpeed=${_speedSettings.value.baseSpeed}") _textItem.value = textItem savedTextItem = textItem _currentWord.value = textItem.content.split(" ").firstOrNull() ?: "" _readerState.value = ReaderState(currentWordIndex = 0, isPlaying = true) startTime = System.currentTimeMillis() println("[ReaderViewModel] setText - About to call startReading()") // Start reading immediately startReading() // Reset saved state when new text is set savedCurrentWordIndex = 0 savedIsPlaying = true savedStartTime = 0 } fun togglePlayPause() { val newState = _readerState.value.copy(isPlaying = !_readerState.value.isPlaying) _readerState.value = newState if (newState.isPlaying) { startReading() } else { stopReading() } } fun configureSpeed(baseSpeed: Int, steps: List<SpeedStep>) { val newSteps = steps.map { step -> SpeedStep(timeInSeconds = step.timeInSeconds, speed = step.speed) } _speedSettings.value = SpeedSettings(baseSpeed = baseSpeed, steps = newSteps) } fun updateSpeedSettings(settings: SpeedSettings) { println("[ReaderViewModel] updateSpeedSettings CALLED: baseSpeed=${settings.baseSpeed}, steps=${settings.steps.size}, isPlaying=${_readerState.value.isPlaying}, startTime=$startTime") println("[ReaderViewModel] BEFORE: _speedSettings.baseSpeed=${_speedSettings.value.baseSpeed}") _speedSettings.value = settings println("[ReaderViewModel] AFTER: _speedSettings.baseSpeed=${_speedSettings.value.baseSpeed}") // Recalculate current speed immediately with new settings // Only do this if startTime is already set (i.e., reading has started) if (_readerState.value.isPlaying && startTime > 0) { val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000.0 val speed = getSpeedForTime(elapsedSeconds, settings) _currentSpeed.value = speed println("[ReaderViewModel] UPDATED current speed to $speed (elapsed: ${elapsedSeconds.toLong()}s)") // Restart speed update job to continue monitoring updateCurrentSpeed() } else { println("[ReaderViewModel] Not updating speed - reading not started or not playing (startTime=$startTime, isPlaying=${_readerState.value.isPlaying})") } } fun reset() { stopReading() val words = textItem.value?.content?.split(" ") ?: emptyList() _currentWord.value = words.firstOrNull() ?: "" _readerState.value = ReaderState(currentWordIndex = 0) startTime = System.currentTimeMillis() // Reset saved state on reset savedCurrentWordIndex = 0 savedIsPlaying = true savedStartTime = 0 } fun restoreState(currentWordIndex: Int, isPlaying: Boolean, startTime: Long) { this.startTime = startTime savedCurrentWordIndex = currentWordIndex savedIsPlaying = isPlaying savedStartTime = startTime // Restore textItem if available _textItem.value = savedTextItem _readerState.value = ReaderState( currentWordIndex = currentWordIndex, isPlaying = isPlaying, isFinished = false ) // Restore current word val words = savedTextItem?.content?.split(" ") ?: emptyList() if (currentWordIndex in words.indices) { _currentWord.value = words[currentWordIndex] } else if (words.isNotEmpty()) { _currentWord.value = words.first() } if (isPlaying) { startReading() } else { stopReading() } } init { println("[ReaderViewModel] INIT - Created with default settings: baseSpeed=${_speedSettings.value.baseSpeed}") } private fun startReading() { println("[ReaderViewModel] startReading - Current speed settings: baseSpeed=${_speedSettings.value.baseSpeed}") readingJob?.cancel() // Set start time BEFORE starting the reading job startTime = System.currentTimeMillis() println("[ReaderViewModel] startReading - startTime set to $startTime") readingJob = viewModelScope.launch { val words = textItem.value?.content?.split(" ") ?: emptyList() var currentIndex = _readerState.value.currentWordIndex println("[ReaderViewModel] startReading - Starting with ${words.size} words, currentIndex=$currentIndex") while (currentIndex < words.size && _readerState.value.isPlaying) { _currentWord.value = words[currentIndex] _readerState.value = _readerState.value.copy( currentWordIndex = currentIndex, isPlaying = true ) val delayMs = getDelayForWord(currentIndex) println("[ReaderViewModel] startReading - Word $currentIndex delay=${delayMs}ms") delay(delayMs) currentIndex++ } if (currentIndex >= words.size) { _readerState.value = _readerState.value.copy( isPlaying = false, isFinished = true ) } } // Update current speed based on elapsed time updateCurrentSpeed() } private fun stopReading() { readingJob?.cancel() speedUpdateJob?.cancel() _currentSpeed.value = 0 } private fun updateCurrentSpeed() { println("[ReaderViewModel] updateCurrentSpeed CALLED") // Cancel previous job if exists speedUpdateJob?.cancel() speedUpdateJob = viewModelScope.launch { println("[ReaderViewModel] updateCurrentSpeed - Subscribing to combine with speedSettings: baseSpeed=${_speedSettings.value.baseSpeed}") combine(_readerState, speedSettings) { readerState, settings -> if (!readerState.isPlaying) { 0 } else { val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000.0 getSpeedForTime(elapsedSeconds, settings) } }.collect { speed -> println("[ReaderViewModel] updateCurrentSpeed - Speed updated to $speed (elapsed: ${(System.currentTimeMillis() - startTime) / 1000.0}s)") _currentSpeed.value = speed } } } private fun getDelayForWord(wordIndex: Int): Long { val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000.0 val speed = getSpeedForTime(elapsedSeconds, _speedSettings.value) println("[ReaderViewModel] getDelayForWord - wordIndex=$wordIndex, elapsed=${elapsedSeconds}s, speed=$speed") // Convert words per minute to milliseconds per word return (60000 / speed).toLong() } fun getSpeedForTime(elapsedSeconds: Double, settings: SpeedSettings): Int { println("[ReaderViewModel] getSpeedForTime - elapsed=${elapsedSeconds}s, baseSpeed=${settings.baseSpeed}, steps=${settings.steps.size}") // Find the speed based on elapsed time and steps val sortedSteps = settings.steps.sortedBy { it.timeInSeconds } // If no steps, return baseSpeed if (sortedSteps.isEmpty()) { println("[ReaderViewModel] getSpeedForTime - no steps, returning baseSpeed=${settings.baseSpeed}") return settings.baseSpeed } // Find the first step where elapsedSeconds >= timeInSeconds var resultSpeed = settings.baseSpeed // Use baseSpeed as default for (step in sortedSteps) { if (elapsedSeconds >= step.timeInSeconds) { resultSpeed = step.speed println("[ReaderViewModel] getSpeedForTime - matched step: time=${step.timeInSeconds}s, speed=${step.speed}") } else { break } } println("[ReaderViewModel] getSpeedForTime - returning speed=$resultSpeed") return resultSpeed } fun updateWordIndex(index: Int) { val words = textItem.value?.content?.split(" ") ?: emptyList() if (index in words.indices) { _currentWord.value = words[index] _readerState.value = _readerState.value.copy(currentWordIndex = index) } } fun getCurrentState(): Triple<Int, Boolean, Long> { return Triple(_readerState.value.currentWordIndex, _readerState.value.isPlaying, startTime) } fun getCurrentSpeed(): Int { if (!_readerState.value.isPlaying) { return 0 } val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000.0 return getSpeedForTime(elapsedSeconds, _speedSettings.value) } fun isStateRestored(): Boolean = false override fun onCleared() { super.onCleared() readingJob?.cancel() } } data class SpeedConfig( val startTime: Int, val startSpeed: Float, val steps: List<SpeedStep> ) data class SpeedStep( val timeInSeconds: Int, val speed: Int )