fix:audio generation off main thread
This commit is contained in:
parent
003fac544c
commit
2008ef64fc
app/src/main/java/com/voca/app
data/repository
domain/usecase
service
ui
@ -192,23 +192,21 @@ class Mp3PlayerRepositoryImpl(
|
||||
|
||||
override fun resume() {
|
||||
Timber.d("Calling RESUME on service via Intent")
|
||||
val serviceIntent = Intent(applicationContext, Mp3PlaybackService::class.java).apply {
|
||||
action = Mp3PlaybackService.ACTION_PLAY // Resume is often handled by PLAY action if paused
|
||||
// If service needs the current URI for resume, ensure it's passed if necessary.
|
||||
// Assuming service retains current URI, ACTION_PLAY is sufficient.
|
||||
// Alternatively, create a dedicated ACTION_RESUME if needed.
|
||||
_playbackState.value.currentUri?.let {
|
||||
putExtra(Mp3PlaybackService.EXTRA_MEDIA_URI, it.toString())
|
||||
}
|
||||
}
|
||||
try {
|
||||
ContextCompat.startForegroundService(applicationContext, serviceIntent)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error sending RESUME/PLAY intent to service")
|
||||
_playbackState.value = _playbackState.value.copy(error = "Error resuming: ${e.message}")
|
||||
}
|
||||
// Also call via binder if bound
|
||||
serviceBinder?.getService()?.resume()
|
||||
val serviceIntent = Intent(applicationContext, Mp3PlaybackService::class.java).apply {
|
||||
action = Mp3PlaybackService.ACTION_RESUME // Use explicit resume action
|
||||
}
|
||||
try {
|
||||
// Start service (needed if service stopped/killed while paused)
|
||||
ContextCompat.startForegroundService(applicationContext, serviceIntent)
|
||||
Timber.i("--> [Repo] resume: startForegroundService call SUCCEEDED")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "--> [Repo] resume: startForegroundService call FAILED")
|
||||
Timber.e(e, "Error ensuring service is running for resume")
|
||||
_playbackState.value = _playbackState.value.copy(error = "Error resuming playback service: ${e.message}")
|
||||
// Optionally return or just let the service handle potential errors if already running?
|
||||
}
|
||||
// Rely on the intent to be processed by the service's onStartCommand
|
||||
Timber.d("Service notified with ACTION_RESUME. State will update via observer.")
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) {
|
||||
|
@ -35,7 +35,8 @@ import com.voca.app.BuildConfig
|
||||
import com.voca.app.data.repository.FileSaverRepository.Companion.VOCA_SUBFOLDER
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
/**
|
||||
* Use case responsible for generating an MP3 file from the currently loaded document content.
|
||||
@ -97,7 +98,11 @@ class GenerateMp3UseCase(
|
||||
logWithTimestamp("Step 2: Starting Text-to-WAV conversion. Text length: ${fullText.length} chars")
|
||||
val ttsStartTime = System.currentTimeMillis()
|
||||
try {
|
||||
// Add extra logging inside collection
|
||||
logWithTimestamp("DEBUG: About to collect textToWavConverter flow.")
|
||||
textToWavConverter.convertTextToWav(context, fullText, fileNameBase).collectLatest { progress ->
|
||||
// Log received progress immediately
|
||||
logWithTimestamp("DEBUG: Received TtsWavProgress: ${progress::class.simpleName}")
|
||||
when (progress) {
|
||||
is TtsWavProgress.InProgress -> {
|
||||
val scaledProgress = progress.progress * 0.5f
|
||||
@ -115,17 +120,20 @@ class GenerateMp3UseCase(
|
||||
}
|
||||
is TtsWavProgress.Error -> {
|
||||
Timber.e(progress.cause, "Text-to-WAV conversion FAILED after ${(System.currentTimeMillis() - ttsStartTime)/1000}s: ${progress.message}")
|
||||
// Throw exception to be caught by the outer try-catch
|
||||
throw IOException("Text-to-WAV failed: ${progress.message}", progress.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
logWithTimestamp("DEBUG: Finished collecting textToWavConverter flow normally.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "GENERATE_MP3_USE_CASE_ERROR: Error collecting from TextToWavConverter flow after ${(System.currentTimeMillis() - ttsStartTime)/1000}s")
|
||||
// Catch exceptions from the collection itself or rethrown from TtsWavProgress.Error
|
||||
Timber.e(e, "GENERATE_MP3_USE_CASE_ERROR: Error during TextToWavConverter flow collection after ${(System.currentTimeMillis() - ttsStartTime)/1000}s")
|
||||
send(Mp3GenerationProgress.Error("Text-to-WAV failed: ${e.message}", e))
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
logWithTimestamp("DEBUG: Finished collecting from textToWavConverter flow.")
|
||||
logWithTimestamp("DEBUG: Passed TextToWavConverter collection block.")
|
||||
|
||||
// Ensure WAV file and timing metadata were created
|
||||
val currentWavFile = wavFile ?: run {
|
||||
@ -134,8 +142,8 @@ class GenerateMp3UseCase(
|
||||
return@channelFlow
|
||||
}
|
||||
val currentTimingMetadata = timingMetadata ?: run {
|
||||
Timber.w("TimingMetadata was null after successful TTS conversion. Proceeding without timings.")
|
||||
TimingMetadata(sentences = emptyList())
|
||||
Timber.w("TimingMetadata was null after successful TTS conversion. Proceeding without timings.")
|
||||
TimingMetadata(sentences = emptyList())
|
||||
}
|
||||
|
||||
if (currentWavFile.length() == 0L) {
|
||||
@ -148,102 +156,104 @@ class GenerateMp3UseCase(
|
||||
logWithTimestamp("Step 3: Starting WAV-to-MP3 encoding with FFmpeg. WAV file size: ${currentWavFile.length()} bytes")
|
||||
val encodingStartTime = System.currentTimeMillis()
|
||||
try {
|
||||
logWithTimestamp("DEBUG: About to call lameEncoder.encodeWavToMp3")
|
||||
logWithTimestamp("DEBUG: About to collect lameEncoder.encodeWavToMp3 flow.")
|
||||
lameEncoder.encodeWavToMp3(currentWavFile, deleteWavOnSuccess = true).collectLatest { progress ->
|
||||
logWithTimestamp("DEBUG: Received EncodingProgress: ${progress::class.simpleName}")
|
||||
when (progress) {
|
||||
is EncodingProgress.InProgress -> {
|
||||
val scaledProgress = 0.5f + (progress.progress * 0.5f)
|
||||
val elapsed = System.currentTimeMillis() - encodingStartTime
|
||||
logWithTimestamp("Encoding progress: ${(progress.progress * 100).toInt()}% (${elapsed/1000}s) - ${progress.stage}")
|
||||
send(Mp3GenerationProgress.InProgress(scaledProgress.coerceIn(0.5f, 1.0f), "MP3: ${progress.stage}"))
|
||||
logWithTimestamp("MP3 encoding progress: ${(progress.progress * 100).toInt()}% (${elapsed/1000}s) - ${progress.stage}")
|
||||
send(Mp3GenerationProgress.InProgress(scaledProgress.coerceIn(0f, 1.0f), "Encoding: ${progress.stage}"))
|
||||
}
|
||||
is EncodingProgress.Success -> {
|
||||
mp3File = progress.mp3File
|
||||
val encodingTime = System.currentTimeMillis() - encodingStartTime
|
||||
val safeFile = mp3File ?: throw IOException("MP3 encoding generated a null file")
|
||||
if (safeFile.length() == 0L) { throw IOException("MP3 encoding generated an empty file") }
|
||||
|
||||
logWithTimestamp("WAV-to-MP3 encoding SUCCESSFUL after ${encodingTime/1000}s: ${safeFile.absolutePath}, size: ${safeFile.length()} bytes")
|
||||
|
||||
// 4. Save Timing Metadata as JSON
|
||||
var jsonFileUri: Uri? = null
|
||||
logWithTimestamp("Step 4: Saving timing metadata as JSON companion file.")
|
||||
try {
|
||||
val jsonFile = File(safeFile.parent, "${safeFile.nameWithoutExtension}.json")
|
||||
val jsonString = json.encodeToString(currentTimingMetadata)
|
||||
withContext(Dispatchers.IO) {
|
||||
jsonFile.writeText(jsonString)
|
||||
}
|
||||
logWithTimestamp("Saved timing metadata to ${jsonFile.absolutePath}, size: ${jsonFile.length()} bytes")
|
||||
// Create Content URI for the JSON file
|
||||
jsonFileUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
jsonFile
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to save timing metadata JSON file. Proceeding without it.")
|
||||
}
|
||||
|
||||
// 5. Create Content URI using FileProvider
|
||||
val mp3FileUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
safeFile
|
||||
)
|
||||
Timber.i("Generated MP3 Content URI: $mp3FileUri")
|
||||
Timber.i("Generated JSON Content URI: $jsonFileUri")
|
||||
|
||||
// 6. Update the repository with the temporary MP3 path (optional, maybe remove if URI is enough)
|
||||
// documentRepository.updateLastGeneratedMp3Path(safeFile.absolutePath)
|
||||
|
||||
// 7. Emit Success with the Content URI
|
||||
send(Mp3GenerationProgress.Success(mp3FileUri, jsonFileUri))
|
||||
logWithTimestamp("MP3 GENERATION COMPLETE.")
|
||||
logWithTimestamp("WAV-to-MP3 encoding SUCCESSFUL after ${encodingTime/1000}s: ${mp3File?.absolutePath}, size: ${mp3File?.length() ?: 0} bytes")
|
||||
send(Mp3GenerationProgress.InProgress(1.0f, "MP3 encoding complete."))
|
||||
}
|
||||
is EncodingProgress.Error -> {
|
||||
val encodingTime = System.currentTimeMillis() - encodingStartTime
|
||||
Timber.e(progress.cause, "WAV-to-MP3 encoding FAILED after ${encodingTime/1000}s: ${progress.message}")
|
||||
throw IOException("WAV-to-MP3 failed: ${progress.message}", progress.cause)
|
||||
Timber.e(progress.cause, "WAV-to-MP3 encoding FAILED after ${(System.currentTimeMillis() - encodingStartTime)/1000}s: ${progress.message}")
|
||||
throw IOException("MP3 encoding failed: ${progress.message}", progress.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
logWithTimestamp("DEBUG: Finished collecting lameEncoder.encodeWavToMp3 flow normally.")
|
||||
} catch (e: Exception) {
|
||||
val encodingTime = System.currentTimeMillis() - encodingStartTime
|
||||
Timber.e(e, "Error in FFmpeg encoder flow after ${encodingTime/1000}s: ${e.message}")
|
||||
send(Mp3GenerationProgress.Error("WAV-to-MP3 or metadata saving failed: ${e.message}", e))
|
||||
|
||||
// Safely clean up WAV file
|
||||
currentWavFile.takeIf { it.exists() }?.let { safeWav ->
|
||||
logWithTimestamp("Cleaning up WAV file after encoding error: ${safeWav.absolutePath}")
|
||||
safeWav.delete()
|
||||
}
|
||||
|
||||
// Safely clean up potentially created MP3 file
|
||||
mp3File?.takeIf { it.exists() }?.let { safeMp3 ->
|
||||
logWithTimestamp("Cleaning up MP3 file after encoding error: ${safeMp3.absolutePath}")
|
||||
safeMp3.delete()
|
||||
}
|
||||
Timber.e(e, "GENERATE_MP3_USE_CASE_ERROR: Error during lameEncoder flow collection after ${(System.currentTimeMillis() - encodingStartTime)/1000}s")
|
||||
send(Mp3GenerationProgress.Error("MP3 encoding failed: ${e.message}", e))
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
logWithTimestamp("DEBUG: Passed lameEncoder collection block.")
|
||||
|
||||
// Ensure MP3 file was created
|
||||
val finalMp3File = mp3File ?: run {
|
||||
Timber.e("MP3 file was null after successful encoding.")
|
||||
send(Mp3GenerationProgress.Error("Internal error: MP3 file not generated."))
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
if (finalMp3File.length() == 0L) {
|
||||
Timber.e("Generated MP3 file is empty (0 bytes): ${finalMp3File.absolutePath}")
|
||||
send(Mp3GenerationProgress.Error("FFmpeg generated an empty MP3 file."))
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
// 4. (Optional) Save timing metadata alongside MP3 if needed
|
||||
// Example: Save to a .json file with the same base name
|
||||
val timingsFile = File(finalMp3File.parent, "${finalMp3File.nameWithoutExtension}.json")
|
||||
try {
|
||||
logWithTimestamp("Step 4: Saving sentence timings to ${timingsFile.absolutePath}")
|
||||
val jsonString = json.encodeToString(currentTimingMetadata)
|
||||
timingsFile.writeText(jsonString)
|
||||
logWithTimestamp("Sentence timings saved successfully.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to save sentence timings JSON file.")
|
||||
// Decide if this is a fatal error or just a warning
|
||||
// send(Mp3GenerationProgress.Error("Failed to save timings: ${e.message}", e))
|
||||
// return@channelFlow // Changed back
|
||||
}
|
||||
|
||||
val totalTime = System.currentTimeMillis() - startTime
|
||||
logWithTimestamp("Step 5: MP3 Generation SUCCESSFUL! Total time: ${totalTime / 1000}s. Output: ${finalMp3File.absolutePath}")
|
||||
|
||||
// Re-introduce FileProvider to get URIs
|
||||
val mp3FileUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
finalMp3File
|
||||
)
|
||||
var timingsFileUri: Uri? = null
|
||||
try {
|
||||
timingsFileUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
timingsFile
|
||||
)
|
||||
logWithTimestamp("Got timings file URI: $timingsFileUri")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get FileProvider URI for timings file, sending null.")
|
||||
}
|
||||
|
||||
send(Mp3GenerationProgress.Success(mp3FileUri, timingsFileUri)) // Send URIs
|
||||
|
||||
} catch (e: CancellationException) {
|
||||
Timber.w(e, "MP3 Generation Job Cancelled after ${(System.currentTimeMillis() - startTime) / 1000}s.")
|
||||
send(Mp3GenerationProgress.Cancelled)
|
||||
// Clean up intermediate files if cancelled?
|
||||
wavFile?.delete()
|
||||
mp3File?.delete()
|
||||
throw e // Rethrow cancellation
|
||||
} catch (e: Exception) {
|
||||
val totalTime = System.currentTimeMillis() - startTime
|
||||
Timber.e(e, "FATAL ERROR in GenerateMp3UseCase after ${totalTime/1000}s: ${e.message}")
|
||||
send(Mp3GenerationProgress.Error("MP3 Generation failed: ${e.message}", e))
|
||||
|
||||
// Safely clean up potentially created WAV file
|
||||
wavFile?.takeIf { it.exists() }?.let { safeWav ->
|
||||
logWithTimestamp("Cleaning up WAV file after fatal error: ${safeWav.absolutePath}")
|
||||
safeWav.delete()
|
||||
}
|
||||
// Also attempt cleanup for mp3File in the fatal error case
|
||||
mp3File?.takeIf { it.exists() }?.let { safeMp3 ->
|
||||
logWithTimestamp("Cleaning up MP3 file after fatal error: ${safeMp3.absolutePath}")
|
||||
safeMp3.delete()
|
||||
}
|
||||
Timber.e(e, "MP3 Generation FAILED after ${totalTime / 1000}s.")
|
||||
send(Mp3GenerationProgress.Error("MP3 generation failed: ${e.message}", e))
|
||||
// Clean up intermediate files on error
|
||||
wavFile?.delete()
|
||||
mp3File?.delete()
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO) // Apply flowOn operator HERE
|
||||
}
|
||||
// Helper to get document file name (implementation detail, might move to repo impl)
|
||||
private fun IDocumentRepository.getCurrentDocumentFileName(): String? {
|
||||
|
@ -61,6 +61,7 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
||||
const val ACTION_PLAY = "com.voca.app.service.ACTION_PLAY"
|
||||
const val ACTION_PAUSE = "com.voca.app.service.ACTION_PAUSE"
|
||||
const val ACTION_STOP = "com.voca.app.service.ACTION_STOP"
|
||||
const val ACTION_RESUME = "com.voca.app.service.ACTION_RESUME"
|
||||
const val ACTION_SEEK_TO = "com.voca.app.service.ACTION_SEEK_TO"
|
||||
const val EXTRA_SEEK_POSITION = "com.voca.app.service.EXTRA_SEEK_POSITION"
|
||||
const val EXTRA_MEDIA_URI = "com.voca.app.service.EXTRA_MEDIA_URI"
|
||||
@ -120,19 +121,31 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
||||
|
||||
if (uri != null) {
|
||||
Timber.d("onStartCommand: Handling PLAY action for $uri")
|
||||
Timber.i("--> [Service] onStartCommand: Handling ACTION_PLAY.")
|
||||
// Check if already playing this URI or if paused
|
||||
if (uri != currentUri || mediaPlayer == null || !isMediaPlayerPrepared) {
|
||||
Timber.i("--> [Service] onStartCommand: Calling startPlayback for new/different URI.")
|
||||
startPlayback(uri)
|
||||
} else if (mediaPlayer?.isPlaying == false && isMediaPlayerPrepared) {
|
||||
Timber.i("--> [Service] onStartCommand: Calling resumePlayback for existing URI.")
|
||||
resumePlayback() // If same URI but paused, resume
|
||||
|
||||
// --- Revised Logic ---
|
||||
if (uri == currentUri && mediaPlayer != null && isMediaPlayerPrepared) {
|
||||
// URI matches, player exists and is prepared
|
||||
if (_playbackState.value.isPaused) { // Check our state flow
|
||||
Timber.i("--> [Service] onStartCommand(PLAY): Resuming playback for existing URI via resumePlayback().")
|
||||
resumePlayback()
|
||||
} else if (_playbackState.value.isPlaying) {
|
||||
Timber.i("--> [Service] onStartCommand(PLAY): Already playing URI $uri. Ignoring.")
|
||||
// Ensure foreground and notification are correct
|
||||
startForegroundServiceIfNeeded()
|
||||
updateNotification()
|
||||
} else {
|
||||
// Player exists and prepared, but not playing/paused (e.g., error state? completed?)
|
||||
// Restart playback in this edge case.
|
||||
Timber.w("--> [Service] onStartCommand(PLAY): Received PLAY for existing prepared URI $uri, but not paused/playing. State: ${_playbackState.value}. Restarting.")
|
||||
startPlayback(uri) // Restart
|
||||
}
|
||||
} else {
|
||||
Timber.d("onStartCommand: PLAY action for URI $uri, but already playing or preparing.")
|
||||
// Optional: Update notification if needed, ensure foreground
|
||||
startForegroundServiceIfNeeded()
|
||||
// URI is different, or player needs initialization
|
||||
Timber.i("--> [Service] onStartCommand(PLAY): Starting playback for new/unprepared URI: $uri")
|
||||
startPlayback(uri)
|
||||
}
|
||||
// --- End Revised Logic ---
|
||||
|
||||
} else {
|
||||
Timber.w("onStartCommand: PLAY action received without a valid URI.")
|
||||
// Stop if started invalidly without a URI to play
|
||||
@ -144,6 +157,11 @@ class Mp3PlaybackService : Service(), AudioManager.OnAudioFocusChangeListener {
|
||||
Timber.i("--> [Service] onStartCommand: Handling ACTION_PAUSE.")
|
||||
pausePlayback()
|
||||
}
|
||||
ACTION_RESUME -> {
|
||||
Timber.d("onStartCommand: Handling RESUME action")
|
||||
Timber.i("--> [Service] onStartCommand: Handling ACTION_RESUME.")
|
||||
resumePlayback()
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
Timber.d("onStartCommand: Handling STOP action")
|
||||
Timber.i("--> [Service] onStartCommand: Handling ACTION_STOP.")
|
||||
|
@ -354,10 +354,8 @@ fun MainScreen(
|
||||
mp3HighlightedSentenceId = displayMp3HighlightedId,
|
||||
ttsSentenceIndex = if (!isMp3FlowActive) ttsState.currentSentenceIndex else -1, // Only use TTS index if MP3 not active
|
||||
ttsSentences = if (!isMp3FlowActive) ttsState.sentences else emptyList(), // Only use TTS sentences if MP3 not active
|
||||
modifier = Modifier
|
||||
.fillMaxSize() // Fill the parent Box
|
||||
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f))
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp) // Add some internal padding if needed
|
||||
modifier = Modifier.fillMaxSize() // Let FullTextDisplayArea handle internal styling
|
||||
// Removed border and padding from here
|
||||
)
|
||||
} else {
|
||||
OutlinedTextField(
|
||||
@ -365,9 +363,10 @@ fun MainScreen(
|
||||
onValueChange = { viewModel.onAction(MainAction.UpdateInputText(it)) },
|
||||
modifier = Modifier
|
||||
.fillMaxSize(), // Fill the entire parent Box
|
||||
// Remove explicit border and padding, use inherent outline
|
||||
label = { Text("Enter text to read or convert") },
|
||||
readOnly = false
|
||||
readOnly = false,
|
||||
// Apply a larger text style for consistency
|
||||
textStyle = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -759,8 +758,8 @@ fun Mp3PlayerControls(
|
||||
)
|
||||
}
|
||||
|
||||
// Show generate button (or similar action) only if not processing AND generation is enabled
|
||||
if (!isProcessing && generationEnabled) {
|
||||
// Show generate button ONLY if NOT processing, NOT ready, NOT loading, NO error AND generation IS enabled
|
||||
if (!isProcessing && !isMp3Ready && !isLoading && !hasError && generationEnabled) {
|
||||
Button(onClick = onGenerateMp3) {
|
||||
Text("Create MP3")
|
||||
}
|
||||
@ -795,9 +794,13 @@ fun FullTextDisplayArea(
|
||||
// Get the final index to highlight, preferring MP3 ID if available
|
||||
val highlightIndex by remember(mp3HighlightedSentenceId, ttsSentenceIndex) {
|
||||
derivedStateOf {
|
||||
mp3HighlightedSentenceId?.toIntOrNull() // Convert MP3 String ID to Int
|
||||
?: ttsSentenceIndex.takeIf { it != -1 } // Fallback to TTS index
|
||||
?: -1 // No highlight
|
||||
// Disabled MP3 highlighting for now as word tracking is not ready
|
||||
// mp3HighlightedSentenceId?.toIntOrNull() // Convert MP3 String ID to Int
|
||||
// ?: ttsSentenceIndex.takeIf { it != -1 } // Fallback to TTS index
|
||||
// ?: -1 // No highlight
|
||||
|
||||
// Only use TTS index for highlighting
|
||||
ttsSentenceIndex.takeIf { it != -1 } ?: -1
|
||||
}
|
||||
}
|
||||
|
||||
@ -916,10 +919,11 @@ fun FullTextDisplayArea(
|
||||
// Using BasicText with annotatedString for highlighting
|
||||
BasicText(
|
||||
text = annotatedString,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
// Use a larger, theme-based style for consistency
|
||||
style = MaterialTheme.typography.headlineSmall.copy(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
fontSize = 18.sp, // Example size, consider making configurable
|
||||
lineHeight = 24.sp // Add some line spacing for readability
|
||||
// Optional: Adjust line height if needed for the larger font
|
||||
lineHeight = MaterialTheme.typography.headlineSmall.lineHeight * 1.2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user