fix(ui): resolve build errors in MainScreen
This commit is contained in:
parent
c22a20b2c8
commit
3bcbda4d27
app/src/main/java/com/voca/app
@ -381,45 +381,37 @@ fun MainScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// --- Conditionally Render Controls ---
|
// --- Use Unified Controls ---
|
||||||
|
UnifiedPlaybackControls(
|
||||||
|
ttsState = ttsState,
|
||||||
|
playbackState = playbackState,
|
||||||
|
isContentAvailable = isDocumentLoaded || hasUserInput,
|
||||||
|
isMp3Mode = isMp3FlowActive, // Determine if MP3 mode is active
|
||||||
|
onTogglePlayback = { viewModel.onAction(MainAction.TogglePlayback) }, // Pass the new action
|
||||||
|
onStopPlayback = { viewModel.onAction(MainAction.StopPlayback) } // Pass the new action
|
||||||
|
)
|
||||||
|
// --- End Unified Controls ---
|
||||||
|
|
||||||
|
// --- MP3 Generation/Save/Seek UI (Keep separate for now) ---
|
||||||
if (isMp3FlowActive) {
|
if (isMp3FlowActive) {
|
||||||
// MP3 Player Controls - Only show if MP3 flow is active
|
Mp3GenerationAndSeekControls(
|
||||||
Timber.d("Rendering MP3 Controls because isMp3FlowActive = true")
|
playbackState = playbackState,
|
||||||
Mp3PlayerControls(
|
mp3GenerationState = mp3GenerationState,
|
||||||
playbackState = playbackState,
|
mp3SaveState = mp3SaveState,
|
||||||
mp3GenerationState = mp3GenerationState,
|
currentMp3UriFromViewModel = currentMp3Uri,
|
||||||
mp3SaveState = mp3SaveState,
|
onCancel = { (viewModel::onAction)(MainAction.CancelMp3Generation) },
|
||||||
currentMp3UriFromViewModel = currentMp3Uri,
|
onSeek = { positionMs -> (viewModel::onAction)(MainAction.SeekMp3(positionMs)) },
|
||||||
// Enable generation button only if there's content and MP3 isn't already active/generating
|
onGenerateMp3 = { (viewModel::onAction)(MainAction.GenerateMp3) }
|
||||||
generationEnabled = (isDocumentLoaded || hasUserInput) && !(playbackState.currentUri != null || mp3GenerationState is Mp3GenerationProgress.InProgress),
|
)
|
||||||
onPlay = { (viewModel::onAction)(MainAction.PlayMp3) },
|
} else if (isDocumentLoaded || hasUserInput) { // Use the same condition used for `generationEnabled` previously
|
||||||
onPause = { (viewModel::onAction)(MainAction.PauseMp3) },
|
Button(
|
||||||
onResume = { (viewModel::onAction)(MainAction.ResumeMp3) },
|
onClick = { viewModel.onAction(MainAction.GenerateMp3) },
|
||||||
onStop = { (viewModel::onAction)(MainAction.StopMp3) },
|
modifier = Modifier.padding(top = 8.dp) // Add some padding
|
||||||
onCancel = { (viewModel::onAction)(MainAction.CancelMp3Generation) },
|
) {
|
||||||
onSeek = { positionMs -> (viewModel::onAction)(MainAction.SeekMp3(positionMs)) },
|
Text("Create MP3")
|
||||||
onGenerateMp3 = { (viewModel::onAction)(MainAction.GenerateMp3) }
|
}
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// TTS Playback Controls - Only show if MP3 flow is NOT active
|
|
||||||
Timber.d("Rendering TTS Controls because isMp3FlowActive = false")
|
|
||||||
TtsControls(
|
|
||||||
ttsState = ttsState,
|
|
||||||
// Enable TTS controls only if there is content available
|
|
||||||
enabled = isDocumentLoaded || hasUserInput,
|
|
||||||
onPlayPauseToggle = {
|
|
||||||
if (ttsState.isSpeaking && !ttsState.isPaused) {
|
|
||||||
(viewModel::onAction)(MainAction.PauseSpeech)
|
|
||||||
} else if (ttsState.isPaused) {
|
|
||||||
(viewModel::onAction)(MainAction.ResumeSpeech)
|
|
||||||
} else {
|
|
||||||
(viewModel::onAction)(MainAction.PlayText) // ViewModel will decide source
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onStop = { (viewModel::onAction)(MainAction.StopSpeaking) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// ---
|
// --- End MP3 Generation/Save/Seek UI ---
|
||||||
|
|
||||||
// Optional: Show subtle indicator if background audio is playing (Consider removing if controls are clear)
|
// Optional: Show subtle indicator if background audio is playing (Consider removing if controls are clear)
|
||||||
/*
|
/*
|
||||||
@ -438,38 +430,58 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple composable for TTS control buttons
|
// --- NEW Unified Playback Controls Composable ---
|
||||||
@Composable
|
@Composable
|
||||||
fun TtsControls(
|
fun UnifiedPlaybackControls(
|
||||||
ttsState: TTSState,
|
ttsState: TTSState,
|
||||||
enabled: Boolean,
|
playbackState: PlaybackState,
|
||||||
onPlayPauseToggle: () -> Unit,
|
isContentAvailable: Boolean,
|
||||||
onStop: () -> Unit
|
isMp3Mode: Boolean, // True if MP3 is loaded/active, false for TTS
|
||||||
|
onTogglePlayback: () -> Unit,
|
||||||
|
onStopPlayback: () -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
// Determine Play/Pause state based on mode
|
||||||
|
val isPlaying = if (isMp3Mode) playbackState.isPlaying else ttsState.isSpeaking && !ttsState.isPaused
|
||||||
|
val isPaused = if (isMp3Mode) playbackState.isPaused else ttsState.isPaused
|
||||||
|
val canPlayback = isContentAvailable || (isMp3Mode && playbackState.currentUri != null) // Enable if content or MP3 URI exists
|
||||||
|
|
||||||
// Play/Pause Button
|
// Play/Pause Button
|
||||||
IconButton(onClick = onPlayPauseToggle, enabled = enabled) {
|
IconButton(onClick = onTogglePlayback, enabled = canPlayback) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (ttsState.isSpeaking && !ttsState.isPaused) Icons.Filled.Pause else Icons.Filled.PlayArrow,
|
imageVector = if (isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
|
||||||
contentDescription = if (ttsState.isSpeaking && !ttsState.isPaused) "Pause Speech" else "Play Speech",
|
contentDescription = when {
|
||||||
|
isPlaying -> "Pause"
|
||||||
|
isPaused -> "Resume"
|
||||||
|
else -> "Play"
|
||||||
|
},
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(48.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop Button (Enabled only if speaking or paused)
|
// Stop Button (Enabled if either TTS or MP3 is playing or paused)
|
||||||
IconButton(onClick = onStop, enabled = ttsState.isSpeaking || ttsState.isPaused) {
|
val canStop = (isMp3Mode && (playbackState.isPlaying || playbackState.isPaused)) ||
|
||||||
|
(!isMp3Mode && (ttsState.isSpeaking || ttsState.isPaused))
|
||||||
|
IconButton(onClick = onStopPlayback, enabled = canStop) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Stop,
|
imageVector = Icons.Filled.Stop,
|
||||||
contentDescription = "Stop Speech",
|
contentDescription = "Stop",
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier.size(48.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END Unified Playback Controls Composable ---
|
||||||
|
|
||||||
|
// Simple composable for TTS control buttons - REMOVED
|
||||||
|
/*
|
||||||
|
@Composable
|
||||||
|
fun TtsControls(...) { ... }
|
||||||
|
*/
|
||||||
|
|
||||||
// --- Preview Data Structure ---
|
// --- Preview Data Structure ---
|
||||||
// Simple data class to hold state for preview purposes, avoiding complex ViewModel mocking
|
// Simple data class to hold state for preview purposes, avoiding complex ViewModel mocking
|
||||||
@ -546,17 +558,14 @@ fun MainScreenPreviewDark() {
|
|||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
TtsControls(
|
// Use UnifiedPlaybackControls in Preview
|
||||||
enabled = true,
|
UnifiedPlaybackControls(
|
||||||
ttsState = ttsState,
|
ttsState = ttsState, // Pass dummy state
|
||||||
onPlayPauseToggle = {
|
playbackState = PlaybackState(), // Pass dummy state
|
||||||
if (ttsState.isSpeaking && !ttsState.isPaused) {
|
isContentAvailable = text.isNotEmpty(),
|
||||||
state.onAction(MainAction.PauseSpeech)
|
isMp3Mode = false, // Assume TTS mode for preview
|
||||||
} else {
|
onTogglePlayback = { state.onAction(MainAction.TogglePlayback) }, // Use new action
|
||||||
state.onAction(MainAction.PlayText)
|
onStopPlayback = { state.onAction(MainAction.StopPlayback) } // Use new action
|
||||||
}
|
|
||||||
},
|
|
||||||
onStop = { state.onAction(MainAction.StopSpeaking) }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
when(genState) {
|
when(genState) {
|
||||||
@ -776,15 +785,6 @@ fun Mp3PlayerControls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats milliseconds duration into MM:SS format.
|
|
||||||
*/
|
|
||||||
private fun formatDuration(millis: Long): String {
|
|
||||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis)
|
|
||||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) % 60
|
|
||||||
return String.format("%02d:%02d", minutes, seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New Composable for displaying the full text with sentence highlighting.
|
* New Composable for displaying the full text with sentence highlighting.
|
||||||
*/
|
*/
|
||||||
@ -936,4 +936,142 @@ fun FullTextDisplayArea(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW Composable for MP3 Generation and Seek Controls ---
|
||||||
|
@Composable
|
||||||
|
fun Mp3GenerationAndSeekControls(
|
||||||
|
playbackState: PlaybackState,
|
||||||
|
mp3GenerationState: Mp3GenerationProgress?,
|
||||||
|
mp3SaveState: FileSaveProgress?,
|
||||||
|
currentMp3UriFromViewModel: Uri?, // Keep for context, might be redundant
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onSeek: (Long) -> Unit,
|
||||||
|
onGenerateMp3: () -> Unit // Can likely remove this if generate button is outside
|
||||||
|
) {
|
||||||
|
val isGenerating = mp3GenerationState is Mp3GenerationProgress.InProgress
|
||||||
|
val isSaving = mp3SaveState is FileSaveProgress.InProgress
|
||||||
|
val isProcessing = isGenerating || isSaving // Combine generation and saving flags
|
||||||
|
|
||||||
|
// Ready ONLY if NOT processing AND (saving succeeded OR playback service has a URI loaded)
|
||||||
|
val isMp3Ready = !isProcessing &&
|
||||||
|
(mp3SaveState is FileSaveProgress.Success || playbackState.currentUri != null)
|
||||||
|
|
||||||
|
val hasError = !isProcessing && ( // Only show errors *after* processing finishes
|
||||||
|
playbackState.error != null ||
|
||||||
|
mp3GenerationState is Mp3GenerationProgress.Error ||
|
||||||
|
mp3SaveState is FileSaveProgress.Error
|
||||||
|
)
|
||||||
|
val isLoading = !isProcessing && !isMp3Ready && !hasError && playbackState.isLoading
|
||||||
|
|
||||||
|
// Determine overall visibility: Show if processing, ready, loading, or has error
|
||||||
|
val shouldShowSection = isProcessing || isMp3Ready || isLoading || hasError
|
||||||
|
|
||||||
|
if (shouldShowSection) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// --- Show Progress Indicator Section (Highest Priority) ---
|
||||||
|
if (isProcessing) {
|
||||||
|
val progress: Float = when {
|
||||||
|
isGenerating -> (mp3GenerationState as Mp3GenerationProgress.InProgress).progress
|
||||||
|
isSaving -> 0.5f // Indicate saving is happening, maybe use indeterminate later
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
val progressText: String = when {
|
||||||
|
isGenerating -> "Creating MP3..."
|
||||||
|
isSaving -> "Saving MP3..."
|
||||||
|
else -> "Processing..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(progressText, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress }, // Use calculated progress
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
// Add an indeterminate bar if progress is low or saving
|
||||||
|
if (progress <= 0.01f || isSaving) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
// Show cancel button only during generation, not saving
|
||||||
|
if (isGenerating) {
|
||||||
|
Button(
|
||||||
|
onClick = onCancel,
|
||||||
|
enabled = mp3GenerationState is Mp3GenerationProgress.InProgress,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
|
||||||
|
) {
|
||||||
|
Text("X")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Show Seek Bar Section (Only if Ready and has duration) ---
|
||||||
|
} else if (isMp3Ready && playbackState.durationMs > 0) {
|
||||||
|
// Seek Bar and Time Display
|
||||||
|
Slider(
|
||||||
|
value = playbackState.currentPositionMs.toFloat(),
|
||||||
|
onValueChange = { onSeek(it.roundToLong()) },
|
||||||
|
valueRange = 0f..playbackState.durationMs.toFloat(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(formatDuration(playbackState.currentPositionMs), style = MaterialTheme.typography.labelSmall)
|
||||||
|
Text(formatDuration(playbackState.durationMs), style = MaterialTheme.typography.labelSmall)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Show Loading Indicator Section (Only if Loading) ---
|
||||||
|
} else if (isLoading) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text("Loading MP3...", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
// --- Show Error Section (Only if not processing, not ready, not loading, but has error) ---
|
||||||
|
} else if (hasError) {
|
||||||
|
val errorMsg = when {
|
||||||
|
mp3GenerationState is Mp3GenerationProgress.Error -> "Error generating MP3: ${mp3GenerationState.message}"
|
||||||
|
mp3SaveState is FileSaveProgress.Error -> "Error saving MP3: ${mp3SaveState.message}"
|
||||||
|
playbackState.error != null -> "Error loading MP3: ${playbackState.error}"
|
||||||
|
else -> "An unknown error occurred."
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
errorMsg,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END MP3 Generation and Seek Controls Composable ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats milliseconds duration into MM:SS format.
|
||||||
|
*/
|
||||||
|
private fun formatDuration(millis: Long): String {
|
||||||
|
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis)
|
||||||
|
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) % 60
|
||||||
|
return String.format("%02d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing code ...
|
@ -63,20 +63,31 @@ sealed interface MainAction {
|
|||||||
data class ProcessSelectedDocument(val uri: Uri) : MainAction
|
data class ProcessSelectedDocument(val uri: Uri) : MainAction
|
||||||
data class ProcessUri(val uri: Uri) : MainAction
|
data class ProcessUri(val uri: Uri) : MainAction
|
||||||
data class ProcessSharedText(val text: String) : MainAction
|
data class ProcessSharedText(val text: String) : MainAction
|
||||||
data object PlayText : MainAction // Plays current page via TTS
|
data class SetTextAndPlay(val text: String) : MainAction // Set text and immediately play TTS - Keep for specific use cases
|
||||||
data object PauseSpeech : MainAction
|
|
||||||
data object ResumeSpeech : MainAction
|
// --- NEW Unified Playback Actions ---
|
||||||
data object StopSpeaking : MainAction
|
data object TogglePlayback : MainAction // Unified Play/Pause/Resume
|
||||||
data class SetTextAndPlay(val text: String) : MainAction // Set text and immediately play TTS
|
data object StopPlayback : MainAction // Unified Stop
|
||||||
|
// --- END Unified Actions ---
|
||||||
|
|
||||||
|
// --- REMOVED Specific Playback Actions ---
|
||||||
|
// data object PlayText : MainAction
|
||||||
|
// data object PauseSpeech : MainAction
|
||||||
|
// data object ResumeSpeech : MainAction
|
||||||
|
// data object StopSpeaking : MainAction
|
||||||
|
// data object PlayMp3 : MainAction
|
||||||
|
// data object PauseMp3 : MainAction
|
||||||
|
// data object ResumeMp3 : MainAction
|
||||||
|
// data object StopMp3 : MainAction
|
||||||
|
// --- END REMOVED ---
|
||||||
|
|
||||||
data object GenerateMp3 : MainAction
|
data object GenerateMp3 : MainAction
|
||||||
data object CancelMp3Generation : MainAction // New action to cancel MP3 generation
|
data object CancelMp3Generation : MainAction // New action to cancel MP3 generation
|
||||||
data class SaveMp3(val desiredFileName: String? = null) : MainAction // Optional desired filename
|
data class SaveMp3(val desiredFileName: String? = null) : MainAction // Optional desired filename
|
||||||
data object PlayMp3 : MainAction // Plays the last generated/loaded MP3
|
|
||||||
data object PauseMp3 : MainAction
|
// Keep Seek action for MP3
|
||||||
data object ResumeMp3 : MainAction
|
|
||||||
data object StopMp3 : MainAction
|
|
||||||
// Change parameter type to Long
|
|
||||||
data class SeekMp3(val positionMs: Long) : MainAction
|
data class SeekMp3(val positionMs: Long) : MainAction
|
||||||
|
|
||||||
data object NextPage : MainAction
|
data object NextPage : MainAction
|
||||||
data object PreviousPage : MainAction
|
data object PreviousPage : MainAction
|
||||||
data object TriggerDocumentPicker : MainAction
|
data object TriggerDocumentPicker : MainAction
|
||||||
@ -332,18 +343,16 @@ class MainViewModel(
|
|||||||
is MainAction.ProcessUri -> handleLoadDocumentAndInitiateAudioProcessing(action.uri)
|
is MainAction.ProcessUri -> handleLoadDocumentAndInitiateAudioProcessing(action.uri)
|
||||||
is MainAction.ProcessSharedText -> handleUpdateText(action.text)
|
is MainAction.ProcessSharedText -> handleUpdateText(action.text)
|
||||||
is MainAction.UpdateText -> handleUpdateText(action.text)
|
is MainAction.UpdateText -> handleUpdateText(action.text)
|
||||||
is MainAction.SetTextAndPlay -> handleSetTextAndPlay(action.text) // Processes text then speaks
|
is MainAction.SetTextAndPlay -> handleSetTextAndPlay(action.text)
|
||||||
is MainAction.UpdateInputText -> {
|
is MainAction.UpdateInputText -> {
|
||||||
_userInputText.value = action.text
|
_userInputText.value = action.text
|
||||||
}
|
}
|
||||||
is MainAction.TriggerDocumentPicker -> handleTriggerDocumentPicker()
|
is MainAction.TriggerDocumentPicker -> handleTriggerDocumentPicker()
|
||||||
|
|
||||||
// TTS Actions
|
// --- UPDATED Playback Actions ---
|
||||||
MainAction.PlayText -> handlePlayText() // Plays current page
|
MainAction.TogglePlayback -> handleTogglePlayback()
|
||||||
MainAction.PauseSpeech -> handlePauseSpeech()
|
MainAction.StopPlayback -> handleStopPlayback()
|
||||||
MainAction.ResumeSpeech -> handleResumeSpeech()
|
// --- END UPDATED ---
|
||||||
MainAction.StopSpeaking -> handleStopSpeaking()
|
|
||||||
// MainAction.ToggleTTS -> handleToggleTTS() // Removed as separate Pause/Resume/Play are preferred
|
|
||||||
|
|
||||||
// MP3 Generation & Saving (Now combined or manual)
|
// MP3 Generation & Saving (Now combined or manual)
|
||||||
MainAction.GenerateMp3 -> handleGenerateAndSaveMp3() // Manual trigger for combined process
|
MainAction.GenerateMp3 -> handleGenerateAndSaveMp3() // Manual trigger for combined process
|
||||||
@ -351,10 +360,6 @@ class MainViewModel(
|
|||||||
is MainAction.SaveMp3 -> Unit // Explicitly ignore if saving is purely automatic now
|
is MainAction.SaveMp3 -> Unit // Explicitly ignore if saving is purely automatic now
|
||||||
|
|
||||||
// MP3 Playback
|
// MP3 Playback
|
||||||
MainAction.PlayMp3 -> handlePlayMp3()
|
|
||||||
MainAction.PauseMp3 -> handlePauseMp3()
|
|
||||||
MainAction.ResumeMp3 -> handleResumeMp3()
|
|
||||||
MainAction.StopMp3 -> handleStopMp3()
|
|
||||||
is MainAction.SeekMp3 -> handleSeekMp3(action.positionMs)
|
is MainAction.SeekMp3 -> handleSeekMp3(action.positionMs)
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
@ -519,41 +524,54 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePlayText() {
|
// --- NEW Handler for Unified Playback Toggle ---
|
||||||
Timber.d("handlePlayText called.")
|
private fun handleTogglePlayback() {
|
||||||
|
Timber.d("handleTogglePlayback called.")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// --- ADD CHECK: Do not start TTS if an MP3 is loaded ---
|
val mp3Uri = _currentMp3Uri.value
|
||||||
if (_currentMp3Uri.value != null) {
|
val currentPlaybackState = playbackState.value
|
||||||
Timber.w("handlePlayText: MP3 URI is loaded ($_currentMp3Uri.value). Skipping TTS playback.")
|
val currentTtsState = ttsStateFlow.value
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
// --- END CHECK ---
|
|
||||||
|
|
||||||
val textToSpeak = getCurrentDocumentStateUseCase.currentPageText.value
|
if (mp3Uri != null) {
|
||||||
if (!textToSpeak.isNullOrBlank()) {
|
// --- MP3 Mode ---
|
||||||
Timber.d("Speaking current page text: '${textToSpeak.take(50)}...'")
|
Timber.d("Toggle playback in MP3 mode. State: $currentPlaybackState")
|
||||||
speakTextUseCase(textToSpeak)
|
when {
|
||||||
|
currentPlaybackState.isPlaying -> pauseMp3UseCase()
|
||||||
|
currentPlaybackState.isPaused -> resumeMp3UseCase()
|
||||||
|
else -> playMp3UseCase(mp3Uri) // Start playback if idle
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("PlayText action called but no text available (document or input).")
|
// --- TTS Mode ---
|
||||||
|
Timber.d("Toggle playback in TTS mode. State: $currentTtsState")
|
||||||
|
val textToSpeak = getCurrentDocumentStateUseCase.currentPageText.value
|
||||||
|
if (!textToSpeak.isNullOrBlank()) {
|
||||||
|
when {
|
||||||
|
currentTtsState.isSpeaking && !currentTtsState.isPaused -> pauseTTSUseCase()
|
||||||
|
currentTtsState.isPaused -> resumeTTSUseCase()
|
||||||
|
else -> speakTextUseCase(textToSpeak) // Start TTS if idle
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.w("TogglePlayback (TTS): No text available.")
|
||||||
|
// Optionally emit an error or show a message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END Handler ---
|
||||||
|
|
||||||
private fun handlePauseSpeech() {
|
// --- NEW Handler for Unified Stop ---
|
||||||
Timber.d("handlePauseSpeech called.")
|
private fun handleStopPlayback() {
|
||||||
pauseTTSUseCase()
|
Timber.d("handleStopPlayback called.")
|
||||||
}
|
viewModelScope.launch {
|
||||||
|
// Stop both TTS and MP3 playback regardless of the current mode
|
||||||
private fun handleResumeSpeech() {
|
Timber.d("Stopping both TTS and MP3.")
|
||||||
Timber.d("handleResumeSpeech called.")
|
stopTTSUseCase()
|
||||||
resumeTTSUseCase()
|
stopMp3UseCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleStopSpeaking() {
|
|
||||||
Timber.d("handleStopSpeaking called.")
|
|
||||||
stopTTSUseCase()
|
|
||||||
}
|
}
|
||||||
|
// --- END Handler ---
|
||||||
|
|
||||||
|
// --- ADDED BACK: Handler for SetTextAndPlay ---
|
||||||
private fun handleSetTextAndPlay(text: String) {
|
private fun handleSetTextAndPlay(text: String) {
|
||||||
Timber.d("handleSetTextAndPlay called.")
|
Timber.d("handleSetTextAndPlay called.")
|
||||||
if (text.isBlank()) {
|
if (text.isBlank()) {
|
||||||
@ -566,17 +584,20 @@ class MainViewModel(
|
|||||||
|
|
||||||
resetStateForNewContent() // Reset state FIRST
|
resetStateForNewContent() // Reset state FIRST
|
||||||
delay(50) // Small delay to allow state reset propagation if needed
|
delay(50) // Small delay to allow state reset propagation if needed
|
||||||
_mp3SaveProgress.value = null // Reset save state
|
|
||||||
|
_isLoading.value = true
|
||||||
|
_processingStatus.value = PdfProcessingStatus.PROCESSING_TEXT
|
||||||
try {
|
try {
|
||||||
processDocumentUseCase.processText(text)
|
processDocumentUseCase.processText(text)
|
||||||
delay(50) // Keep delay for now, but ideally improve state propagation
|
delay(50) // Keep delay for now, but ideally improve state propagation
|
||||||
val updatedText = getCurrentDocumentStateUseCase.currentPageText.value
|
val updatedText = getCurrentDocumentStateUseCase.currentPageText.value
|
||||||
if (!updatedText.isNullOrBlank()) {
|
if (!updatedText.isNullOrBlank()) {
|
||||||
speakTextUseCase(updatedText)
|
// Initiate TTS playback after processing
|
||||||
_processingStatus.value = PdfProcessingStatus.COMPLETED
|
speakTextUseCase(updatedText)
|
||||||
|
_processingStatus.value = PdfProcessingStatus.COMPLETED // Mark as completed after starting TTS
|
||||||
} else {
|
} else {
|
||||||
Timber.w("handleSetTextAndPlay: Text set, but state not updated in time for TTS.")
|
Timber.w("handleSetTextAndPlay: Text set, but state not updated in time for TTS.")
|
||||||
_processingStatus.value = PdfProcessingStatus.COMPLETED
|
_processingStatus.value = PdfProcessingStatus.COMPLETED // Mark as completed even if TTS didn't start
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Error in setTextAndPlay")
|
Timber.e(e, "Error in setTextAndPlay")
|
||||||
@ -586,6 +607,7 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- END ADDED BACK ---
|
||||||
|
|
||||||
private fun handleGenerateAndSaveMp3() {
|
private fun handleGenerateAndSaveMp3() {
|
||||||
Timber.d("handleGenerateAndSaveMp3 called. Adding debug checkpoints.")
|
Timber.d("handleGenerateAndSaveMp3 called. Adding debug checkpoints.")
|
||||||
@ -818,60 +840,6 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePlayMp3(mp3Uri: Uri? = null) {
|
|
||||||
Timber.d("handlePlayMp3 called.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
// Use the _currentMp3Uri state if no explicit URI is passed
|
|
||||||
val uriToPlay = mp3Uri ?: _currentMp3Uri.value
|
|
||||||
|
|
||||||
if (uriToPlay != null) {
|
|
||||||
try {
|
|
||||||
Timber.d("Attempting to play MP3 from URI: $uriToPlay")
|
|
||||||
Timber.i("--> [VM] handlePlayMp3: Calling playMp3UseCase for URI: $uriToPlay")
|
|
||||||
// Call use case with the correct URI
|
|
||||||
playMp3UseCase(uriToPlay)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error starting MP3 playback for URI: $uriToPlay")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.w("Play MP3 requested, but no URI available.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePauseMp3() {
|
|
||||||
Timber.d("handlePauseMp3 called.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
pauseMp3UseCase()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error pausing MP3 playback")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleResumeMp3() {
|
|
||||||
Timber.d("handleResumeMp3 called.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
resumeMp3UseCase()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error resuming MP3 playback")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleStopMp3() {
|
|
||||||
Timber.d("handleStopMp3 called.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
stopMp3UseCase()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error stopping MP3 playback")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleSeekMp3(positionMs: Long) {
|
private fun handleSeekMp3(positionMs: Long) {
|
||||||
Timber.d("handleSeekMp3 called with position: $positionMs ms")
|
Timber.d("handleSeekMp3 called with position: $positionMs ms")
|
||||||
// No need for viewModelScope for simple seek
|
// No need for viewModelScope for simple seek
|
||||||
|
Loading…
x
Reference in New Issue
Block a user