fix: MP3 text highlighting now updates during playback
This commit is contained in:
parent
f092e3937a
commit
22f38366d9
app/src/main/java/com/voca/app
data/repository
service
ui
viewmodel
@ -124,7 +124,7 @@ class Mp3PlayerRepositoryImpl(
|
|||||||
service.playbackState
|
service.playbackState
|
||||||
.catch { e -> Timber.e(e, "Error collecting service state flow") }
|
.catch { e -> Timber.e(e, "Error collecting service state flow") }
|
||||||
.collectLatest { serviceState ->
|
.collectLatest { serviceState ->
|
||||||
Timber.v("Received service state update: $serviceState")
|
Timber.d("--> [Repo Observer] Received state from service: $serviceState")
|
||||||
_playbackState.value = serviceState
|
_playbackState.value = serviceState
|
||||||
}
|
}
|
||||||
Timber.d("Service state flow collection ended.")
|
Timber.d("Service state flow collection ended.")
|
||||||
|
@ -46,6 +46,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.Job // Import Job
|
import kotlinx.coroutines.Job // Import Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
||||||
|
|
||||||
@ -293,7 +294,6 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
|||||||
|
|
||||||
startForegroundServiceIfNeeded() // Ensure service is foreground again
|
startForegroundServiceIfNeeded() // Ensure service is foreground again
|
||||||
updateNotification() // Update notification to playing state
|
updateNotification() // Update notification to playing state
|
||||||
startPositionUpdates()
|
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Not playing, pause command ignored.")
|
Timber.d("Not playing, pause command ignored.")
|
||||||
}
|
}
|
||||||
@ -468,13 +468,22 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
|||||||
currentState.currentPositionMs // Fallback to last known state position (already Long)
|
currentState.currentPositionMs // Fallback to last known state position (already Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate actual duration - CRITICAL FIX!
|
||||||
|
val actualDuration = durationMs ?: try {
|
||||||
|
// Get Int from MediaPlayer, convert to Long
|
||||||
|
mediaPlayer?.duration?.toLong() ?: currentState.durationMs
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
Timber.w("Error getting duration: ${e.message}")
|
||||||
|
currentState.durationMs // Fallback to last known state duration (already Long)
|
||||||
|
}
|
||||||
|
|
||||||
val newState = currentState.copy(
|
val newState = currentState.copy(
|
||||||
isPlaying = isPlaying ?: currentState.isPlaying,
|
isPlaying = isPlaying ?: currentState.isPlaying,
|
||||||
isPaused = isPaused ?: currentState.isPaused,
|
isPaused = isPaused ?: currentState.isPaused,
|
||||||
isLoading = isLoading ?: currentState.isLoading,
|
isLoading = isLoading ?: currentState.isLoading,
|
||||||
error = if (error != null) error else if (currentState.error != null && error == null) null else currentState.error,
|
error = if (error != null) error else if (currentState.error != null && error == null) null else currentState.error,
|
||||||
currentPositionMs = actualCurrentPosition,
|
currentPositionMs = actualCurrentPosition,
|
||||||
durationMs = durationMs ?: currentState.durationMs,
|
durationMs = actualDuration, // Use the calculated duration
|
||||||
currentUri = currentUri ?: currentState.currentUri
|
currentUri = currentUri ?: currentState.currentUri
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -574,9 +583,6 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
|||||||
mediaSession?.release() // Release media session
|
mediaSession?.release() // Release media session
|
||||||
mediaSession = null
|
mediaSession = null
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE) // Ensure foreground status is cleared
|
stopForeground(STOP_FOREGROUND_REMOVE) // Ensure foreground status is cleared
|
||||||
cancelPositionUpdates() // Cancel updater job
|
|
||||||
serviceScope.cancel() // Cancel the entire scope
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
@ -823,7 +829,7 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
|||||||
stopForeground(STOP_FOREGROUND_REMOVE) // Ensure foreground status is cleared
|
stopForeground(STOP_FOREGROUND_REMOVE) // Ensure foreground status is cleared
|
||||||
// Cancel any ongoing coroutines if using viewModelScope or similar
|
// Cancel any ongoing coroutines if using viewModelScope or similar
|
||||||
cancelPositionUpdates() // Cancel updater job
|
cancelPositionUpdates() // Cancel updater job
|
||||||
serviceScope.cancel() // Cancel the entire scope
|
serviceScope.cancel() // Cancel the entire scope ONLY in onDestroy
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -914,33 +920,134 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
|||||||
|
|
||||||
// --- Position Update Logic ---
|
// --- Position Update Logic ---
|
||||||
private fun startPositionUpdates() {
|
private fun startPositionUpdates() {
|
||||||
cancelPositionUpdates() // Ensure only one updater runs
|
// First, cancel any existing job
|
||||||
if (mediaPlayer?.isPlaying != true) {
|
cancelPositionUpdates()
|
||||||
Timber.d("--> [Service] Not starting position updates: player not playing.")
|
|
||||||
return // Don't start if not playing
|
// CRITICAL FIX: Re-initialize the scope if it was cancelled
|
||||||
|
if (serviceScope.isActive.not()) {
|
||||||
|
Timber.w("☢️ [POSITION UPDATES] ServiceScope was cancelled! Creating a new one.")
|
||||||
|
// Create a new scope for future coroutines
|
||||||
|
// This line fixes the issue where the scope was cancelled prematurely
|
||||||
|
// FIXME: Ideally, we would not be cancelling the scope in the first place
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("--> [Service] Starting position update coroutine.")
|
// Mandatory checks BEFORE starting
|
||||||
|
if (mediaPlayer == null) {
|
||||||
|
Timber.e("☢️ [POSITION UPDATES] Cannot start - MediaPlayer is null!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMediaPlayerPrepared) {
|
||||||
|
Timber.e("☢️ [POSITION UPDATES] Cannot start - MediaPlayer not prepared!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log detailed media player state before starting
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] Starting updates - isPlaying: ${mediaPlayer?.isPlaying}, currentPos: ${mediaPlayer?.safeGetCurrentPosition()}, duration: ${mediaPlayer?.duration}")
|
||||||
|
|
||||||
|
// Don't even try if player is not playing
|
||||||
|
if (mediaPlayer?.isPlaying != true) {
|
||||||
|
Timber.w("☢️ [POSITION UPDATES] Skipping updates - player not playing")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the update job
|
||||||
positionUpdateJob = serviceScope.launch {
|
positionUpdateJob = serviceScope.launch {
|
||||||
while (isActive && mediaPlayer?.isPlaying == true) { // Check isActive and isPlaying
|
try {
|
||||||
val currentPosition = mediaPlayer.safeGetCurrentPosition()
|
Timber.i("☢️ [POSITION UPDATES] Position updater coroutine STARTED in ${Thread.currentThread().name}")
|
||||||
// Update state flow only if the position actually changed
|
delay(100) // Short delay before first check
|
||||||
if (_playbackState.value.currentPositionMs != currentPosition) {
|
|
||||||
updatePlaybackState(currentPositionMs = currentPosition)
|
// Log initial state
|
||||||
|
val initialPosition = mediaPlayer?.safeGetCurrentPosition() ?: 0
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] Initial position: $initialPosition ms")
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
var lastReportedPosition = -1L
|
||||||
|
var consecutiveSamePositions = 0
|
||||||
|
|
||||||
|
// Use a while loop with multiple conditions to ensure we keep checking
|
||||||
|
while (isActive) {
|
||||||
|
count++
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if MediaPlayer is null or not playing INSIDE the loop
|
||||||
|
val player = mediaPlayer
|
||||||
|
if (player == null) {
|
||||||
|
Timber.w("☢️ [POSITION UPDATES] Loop iteration $count - MediaPlayer is NULL, stopping updates")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.isPlaying) {
|
||||||
|
Timber.w("☢️ [POSITION UPDATES] Loop iteration $count - MediaPlayer not playing, stopping updates")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current position - CRITICAL: Use direct call to media player here
|
||||||
|
val position = try {
|
||||||
|
player.currentPosition.toLong()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "☢️ [POSITION UPDATES] Error getting position from MediaPlayer")
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
|
val previous = _playbackState.value.currentPositionMs
|
||||||
|
|
||||||
|
// Safety check - detect if position is stuck
|
||||||
|
if (position == lastReportedPosition) {
|
||||||
|
consecutiveSamePositions++
|
||||||
|
if (consecutiveSamePositions >= 15) { // ~3 seconds stuck
|
||||||
|
Timber.w("☢️ [POSITION UPDATES] Position appears stuck at $position ms for ${consecutiveSamePositions} iterations!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consecutiveSamePositions = 0
|
||||||
|
lastReportedPosition = position
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force log frequent updates at first, then less frequently
|
||||||
|
if (count <= 10 || count % 5 == 0 || consecutiveSamePositions >= 10) {
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] Loop #$count - position: $position ms, previous: $previous ms, isPlaying: ${player.isPlaying}, stuck: $consecutiveSamePositions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Always update position to ensure UI gets current position values!
|
||||||
|
Timber.d("☢️ [POSITION UPDATES] Updating position: $position ms (was $previous ms)")
|
||||||
|
|
||||||
|
// Update playback state with new position - EVERY TIME
|
||||||
|
updatePlaybackState(
|
||||||
|
currentPositionMs = position,
|
||||||
|
durationMs = player.duration.toLong() // Include duration
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debug where UI might be getting wrong values from
|
||||||
|
if (count % 10 == 0) {
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] After update: state=${_playbackState.value.currentPositionMs}ms, player=${player.currentPosition}ms")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "☢️ [POSITION UPDATES] Exception in loop #$count")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before checking again - shorter interval for more responsive updates
|
||||||
|
delay(200)
|
||||||
}
|
}
|
||||||
delay(300) // Update interval (e.g., every 300ms)
|
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] Loop exited after $count iterations")
|
||||||
|
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] Coroutine cancelled normally")
|
||||||
|
throw e // Rethrow cancellation
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "☢️ [POSITION UPDATES] Uncaught exception in update coroutine")
|
||||||
|
} finally {
|
||||||
|
Timber.i("☢️ [POSITION UPDATES] Coroutine FINISHED")
|
||||||
}
|
}
|
||||||
// Log why the loop exited
|
|
||||||
Timber.d("--> [Service] Position update coroutine finished (isActive=$isActive, isPlaying=${mediaPlayer?.isPlaying})")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelPositionUpdates() {
|
private fun cancelPositionUpdates() {
|
||||||
if (positionUpdateJob?.isActive == true) {
|
positionUpdateJob?.let { job ->
|
||||||
Timber.d("--> [Service] Cancelling active position update coroutine.")
|
if (job.isActive) {
|
||||||
positionUpdateJob?.cancel()
|
Timber.i("☢️ [POSITION UPDATES] Cancelling active update job")
|
||||||
} else {
|
job.cancel()
|
||||||
// Timber.d("--> [Service] No active position update coroutine to cancel.") // Optional: less verbose log
|
}
|
||||||
}
|
}
|
||||||
positionUpdateJob = null
|
positionUpdateJob = null
|
||||||
}
|
}
|
||||||
|
@ -802,13 +802,13 @@ fun FullTextDisplayArea(
|
|||||||
// Get the final index to highlight, preferring MP3 ID if available
|
// Get the final index to highlight, preferring MP3 ID if available
|
||||||
val highlightIndex by remember(mp3HighlightedSentenceId, ttsSentenceIndex) {
|
val highlightIndex by remember(mp3HighlightedSentenceId, ttsSentenceIndex) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
// Disabled MP3 highlighting for now as word tracking is not ready
|
// Disabled MP3 highlighting for now as word tracking is not ready - RE-ENABLING
|
||||||
// mp3HighlightedSentenceId?.toIntOrNull() // Convert MP3 String ID to Int
|
mp3HighlightedSentenceId?.toIntOrNull() // Convert MP3 String ID to Int
|
||||||
// ?: ttsSentenceIndex.takeIf { it != -1 } // Fallback to TTS index
|
?: ttsSentenceIndex.takeIf { it != -1 } // Fallback to TTS index if MP3 ID is null
|
||||||
// ?: -1 // No highlight
|
?: -1 // No highlight if both are invalid/unavailable
|
||||||
|
|
||||||
// Only use TTS index for highlighting
|
// Only use TTS index for highlighting - REMOVING
|
||||||
ttsSentenceIndex.takeIf { it != -1 } ?: -1
|
// ttsSentenceIndex.takeIf { it != -1 } ?: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,6 +255,9 @@ class MainViewModel(
|
|||||||
// --- End Error Check ---
|
// --- End Error Check ---
|
||||||
|
|
||||||
if (state.isPlaying) { // Check the boolean flag
|
if (state.isPlaying) { // Check the boolean flag
|
||||||
|
// <<< ADDED LOGGING >>>
|
||||||
|
Timber.d(">>> Playback state isPlaying = true. Checking timings before starting tracker: ${_sentenceTimings.value.size} timings available. First few: ${_sentenceTimings.value.take(3)}")
|
||||||
|
Timber.d(">>> Calling startPlaybackTracking().")
|
||||||
startPlaybackTracking()
|
startPlaybackTracking()
|
||||||
} else {
|
} else {
|
||||||
stopPlaybackTracking()
|
stopPlaybackTracking()
|
||||||
@ -481,6 +484,8 @@ class MainViewModel(
|
|||||||
_fullDocumentText.value = loadedContent.fullText
|
_fullDocumentText.value = loadedContent.fullText
|
||||||
_processingStatus.value = PdfProcessingStatus.READY_TO_PLAY // Ready state
|
_processingStatus.value = PdfProcessingStatus.READY_TO_PLAY // Ready state
|
||||||
Timber.i("Successfully loaded existing content for $fileName. Ready to play.")
|
Timber.i("Successfully loaded existing content for $fileName. Ready to play.")
|
||||||
|
// <<< ADDED LOGGING >>>
|
||||||
|
Timber.i(">>> Timings loaded from existing content: ${_sentenceTimings.value.size} timings. First few: ${_sentenceTimings.value.take(3)}")
|
||||||
// Optionally auto-play?
|
// Optionally auto-play?
|
||||||
// handlePlayMp3()
|
// handlePlayMp3()
|
||||||
// *** IMPORTANT: Do nothing further, content is loaded ***
|
// *** IMPORTANT: Do nothing further, content is loaded ***
|
||||||
@ -698,6 +703,8 @@ class MainViewModel(
|
|||||||
_sentenceTimings.value = loadedTimings
|
_sentenceTimings.value = loadedTimings
|
||||||
_fullDocumentText.value = reconstructedText // Update full text based on timings
|
_fullDocumentText.value = reconstructedText // Update full text based on timings
|
||||||
_processingStatus.value = PdfProcessingStatus.READY_TO_PLAY // Use correct Enum value
|
_processingStatus.value = PdfProcessingStatus.READY_TO_PLAY // Use correct Enum value
|
||||||
|
// <<< ADDED LOGGING >>>
|
||||||
|
Timber.i(">>> Timings loaded after generation: ${_sentenceTimings.value.size} timings. First few: ${_sentenceTimings.value.take(3)}")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Error loading timings after generation from $jsonUri")
|
Timber.e(e, "Error loading timings after generation from $jsonUri")
|
||||||
// Update state (thread-safe)
|
// Update state (thread-safe)
|
||||||
@ -924,6 +931,7 @@ class MainViewModel(
|
|||||||
private fun startPlaybackTracking() {
|
private fun startPlaybackTracking() {
|
||||||
stopPlaybackTracking() // Ensure only one tracking job runs
|
stopPlaybackTracking() // Ensure only one tracking job runs
|
||||||
val currentTimings = _sentenceTimings.value
|
val currentTimings = _sentenceTimings.value
|
||||||
|
Timber.d(">>> startPlaybackTracking called. Timings empty? ${currentTimings.isEmpty()}")
|
||||||
if (currentTimings.isEmpty()) {
|
if (currentTimings.isEmpty()) {
|
||||||
Timber.w("Cannot start playback tracking: Sentence timings are empty.")
|
Timber.w("Cannot start playback tracking: Sentence timings are empty.")
|
||||||
return
|
return
|
||||||
@ -931,11 +939,61 @@ class MainViewModel(
|
|||||||
|
|
||||||
Timber.d("Starting playback tracking job.")
|
Timber.d("Starting playback tracking job.")
|
||||||
playbackTrackingJob = viewModelScope.launch {
|
playbackTrackingJob = viewModelScope.launch {
|
||||||
|
Timber.d(">>> Playback tracking Job STARTED.")
|
||||||
while (isActive) { // Loop while the job is active (and playback is PLAYING)
|
while (isActive) { // Loop while the job is active (and playback is PLAYING)
|
||||||
// Get position directly inside the loop to ensure it's fresh
|
// Get position directly inside the loop to ensure it's fresh
|
||||||
updateHighlightForCurrentPosition(playbackPositionMs.value, currentTimings)
|
val currentPosition = playbackPositionMs.value
|
||||||
delay(TRACKING_INTERVAL_MS)
|
Timber.d(">>> Tracking Loop: currentPosition = $currentPosition ms")
|
||||||
|
updateHighlightForCurrentPosition(currentPosition, currentTimings)
|
||||||
|
|
||||||
|
// --- Calculate adaptive delay ---
|
||||||
|
val highlightedId = _highlightedSentenceId.value
|
||||||
|
val highlightedIndex = highlightedId?.toIntOrNull()
|
||||||
|
|
||||||
|
val delayMs = if (highlightedIndex != null && highlightedIndex >= 0 && highlightedIndex < currentTimings.size) {
|
||||||
|
val currentSentence = currentTimings[highlightedIndex]
|
||||||
|
val sentenceEndTime = currentSentence.startMs + currentSentence.durationMs
|
||||||
|
|
||||||
|
// Calculate time remaining in the current sentence
|
||||||
|
val timeRemainingInSentence = sentenceEndTime - currentPosition
|
||||||
|
|
||||||
|
// Look ahead to the next sentence's start time if not the last sentence
|
||||||
|
val nextSentenceStartTime = if (highlightedIndex + 1 < currentTimings.size) {
|
||||||
|
currentTimings[highlightedIndex + 1].startMs
|
||||||
|
} else {
|
||||||
|
null // No next sentence
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time until the next sentence actually starts
|
||||||
|
val timeUntilNextSentenceStarts = nextSentenceStartTime?.let { it - currentPosition }
|
||||||
|
|
||||||
|
// Choose the smaller positive delay: either remaining time in current sentence
|
||||||
|
// or time until next sentence starts (if available).
|
||||||
|
// This aims to re-check *just before* or *just as* the next sentence begins.
|
||||||
|
val calculatedDelay = when {
|
||||||
|
timeUntilNextSentenceStarts != null && timeUntilNextSentenceStarts > 0 ->
|
||||||
|
minOf(timeRemainingInSentence, timeUntilNextSentenceStarts)
|
||||||
|
timeRemainingInSentence > 0 ->
|
||||||
|
timeRemainingInSentence
|
||||||
|
else ->
|
||||||
|
TRACKING_INTERVAL_MS // Fallback if remaining time is non-positive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure delay is positive and clamp to max interval
|
||||||
|
maxOf(1L, minOf(calculatedDelay, TRACKING_INTERVAL_MS))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Fallback if no highlight or index is invalid
|
||||||
|
TRACKING_INTERVAL_MS
|
||||||
|
}
|
||||||
|
// --- End adaptive delay calculation ---
|
||||||
|
|
||||||
|
// <-- ADDED LOG
|
||||||
|
Timber.d(">>> Tracking Loop: calculated delay = $delayMs ms")
|
||||||
|
Timber.v("Playback tracking delay: $delayMs ms") // Verbose logging for delay
|
||||||
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
|
Timber.d(">>> Playback tracking Job loop EXITED.")
|
||||||
}
|
}
|
||||||
// Log when the job completes or is cancelled
|
// Log when the job completes or is cancelled
|
||||||
playbackTrackingJob?.invokeOnCompletion { cause ->
|
playbackTrackingJob?.invokeOnCompletion { cause ->
|
||||||
@ -958,6 +1016,8 @@ class MainViewModel(
|
|||||||
// Helper function to find and update the highlighted sentence
|
// Helper function to find and update the highlighted sentence
|
||||||
// Can be called during tracking or explicitly on pause/seek
|
// Can be called during tracking or explicitly on pause/seek
|
||||||
private fun updateHighlightForCurrentPosition(positionMs: Long, timings: List<SentenceTiming>? = null) {
|
private fun updateHighlightForCurrentPosition(positionMs: Long, timings: List<SentenceTiming>? = null) {
|
||||||
|
// <-- ADDED LOG
|
||||||
|
Timber.d(">>> updateHighlightForCurrentPosition called with positionMs = $positionMs")
|
||||||
val currentTimings = timings ?: _sentenceTimings.value // Use provided timings or current state
|
val currentTimings = timings ?: _sentenceTimings.value // Use provided timings or current state
|
||||||
if (currentTimings.isEmpty()) {
|
if (currentTimings.isEmpty()) {
|
||||||
// Timber.v("Cannot update highlight: No timings available.") // Reduce log spam
|
// Timber.v("Cannot update highlight: No timings available.") // Reduce log spam
|
||||||
@ -976,9 +1036,13 @@ class MainViewModel(
|
|||||||
|
|
||||||
// Use index as the unique identifier (convert to String)
|
// Use index as the unique identifier (convert to String)
|
||||||
val newSentenceId = currentSentence?.index?.toString()
|
val newSentenceId = currentSentence?.index?.toString()
|
||||||
|
// <-- ADDED LOG
|
||||||
|
Timber.d(">>> updateHighlightForCurrentPosition found newSentenceId = $newSentenceId")
|
||||||
|
|
||||||
// Update the state flow only if the sentence ID has changed
|
// Update the state flow only if the sentence ID has changed
|
||||||
if (_highlightedSentenceId.value != newSentenceId) {
|
if (_highlightedSentenceId.value != newSentenceId) {
|
||||||
|
// <-- ADDED LOG
|
||||||
|
Timber.d(">>> !!! Updating _highlightedSentenceId to: $newSentenceId")
|
||||||
Timber.d("Updating highlighted sentence: ID=${newSentenceId} at position ${positionMs}ms")
|
Timber.d("Updating highlighted sentence: ID=${newSentenceId} at position ${positionMs}ms")
|
||||||
_highlightedSentenceId.value = newSentenceId
|
_highlightedSentenceId.value = newSentenceId
|
||||||
} else {
|
} else {
|
||||||
@ -1042,6 +1106,8 @@ class MainViewModel(
|
|||||||
Timber.w("Timings loaded, but reconstructed text is empty. Check SentenceTiming model or JSON content.")
|
Timber.w("Timings loaded, but reconstructed text is empty. Check SentenceTiming model or JSON content.")
|
||||||
// Proceed? Or error out? For now, proceed but log.
|
// Proceed? Or error out? For now, proceed but log.
|
||||||
}
|
}
|
||||||
|
// <<< ADDED LOGGING >>>
|
||||||
|
Timber.i(">>> Timings loaded from JSON pairing: ${_sentenceTimings.value.size} timings. First few: ${_sentenceTimings.value.take(3)}")
|
||||||
|
|
||||||
// Clear any previous TTS state
|
// Clear any previous TTS state
|
||||||
stopTTSUseCase()
|
stopTTSUseCase()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user