Repository navigation
Keep recording controls stable during transcription setup #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.WindowInsets | |
| import androidx.compose.foundation.layout.consumeWindowInsets | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.heightIn | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.layout.safeDrawing | ||
| import androidx.compose.foundation.layout.windowInsetsPadding | ||
|
|
@@ -181,15 +181,9 @@ private fun AudioRecordingDialog(recorder: com.logseq.logseq.AndroidAudioRecorde | |
| title = { Text(stringResource(R.string.audio_recording)) }, | ||
| text = { | ||
| Column { | ||
| if (state.preparing || state.preparingTranscription) { | ||
| LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) | ||
| Text(stringResource(R.string.audio_preparing), modifier = Modifier.padding(top = 8.dp)) | ||
| } | ||
| if (state.hasRecording) { | ||
| Row(verticalAlignment = Alignment.CenterVertically) { | ||
| LinearProgressIndicator(progress = { state.level }, modifier = Modifier.weight(1f)) | ||
| Text("%d:%02d".format(state.seconds / 60, state.seconds % 60), modifier = Modifier.padding(start = 12.dp)) | ||
| } | ||
| Row(verticalAlignment = Alignment.CenterVertically) { | ||
| LinearProgressIndicator(progress = { state.level }, modifier = Modifier.weight(1f)) | ||
| Text("%d:%02d".format(state.seconds / 60, state.seconds % 60), modifier = Modifier.padding(start = 12.dp)) | ||
| } | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| Row(verticalAlignment = Alignment.CenterVertically) { | ||
|
|
@@ -223,11 +217,18 @@ private fun AudioRecordingDialog(recorder: com.logseq.logseq.AndroidAudioRecorde | |
| } | ||
| } | ||
| } | ||
| if (state.hasRecording && request.transcriptionEnabled) { | ||
| SelectionContainer { | ||
| Text(state.transcript.ifBlank { stringResource(R.string.audio_listening) }, | ||
| modifier = Modifier.padding(top = 12.dp).heightIn(max = 180.dp) | ||
| .verticalScroll(transcriptScroll).testTag("audio.transcript")) | ||
| if (request.transcriptionEnabled) { | ||
| Column(modifier = Modifier.padding(top = 12.dp).height(180.dp).verticalScroll(transcriptScroll)) { | ||
| if (state.preparing || state.preparingTranscription) { | ||
| LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) | ||
| Text(stringResource(R.string.audio_preparing), modifier = Modifier.padding(top = 8.dp)) | ||
| } | ||
|
Comment on lines
+220
to
+225
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Recording setup has no progress feedback With transcription disabled, Learn moreThe service enters Example: Turn transcription off, close the dialog, then open a new recording. While the microphone initializes, Start cannot be pressed and only the idle 0:00 display appears; previously a preparation indicator appeared. Recommended fix: Show recording preparation status independently of the transcription toggle, while keeping the timer and action layout stable. Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| if (state.transcript.isNotBlank() || (!state.preparing && !state.preparingTranscription)) { | ||
| SelectionContainer { | ||
| Text(state.transcript.ifBlank { stringResource(R.string.audio_listening) }, | ||
| modifier = Modifier.testTag("audio.transcript")) | ||
|
Comment on lines
+226
to
+229
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Failed recordings display a listening status When microphone access fails, Learn moreThe dialog shows a placeholder whenever both preparation flags are false and the transcript is blank. AndroidAudioRecorder.start can fail before calling the recording service, leaving Example: On Android 12 or later, leave transcription enabled and deny microphone permission on first use. The dialog shows the microphone-access error and “Listening…” despite never starting capture. Recommended fix: Require Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
| } | ||
| } | ||
| } | ||
| (request.error ?: state.error)?.let { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -245,6 +245,10 @@ final class AudioRecorderController: NSObject, ObservableObject { | |
| outputURL = url | ||
| let engine = AVAudioEngine() | ||
| let format = engine.inputNode.outputFormat(forBus: 0) | ||
| guard format.sampleRate.isFinite, format.sampleRate > 0, format.channelCount > 0 else { | ||
| throw NSError(domain: "LogseqAudioRecording", code: 1, | ||
| userInfo: [NSLocalizedDescriptionKey: "The microphone input is unavailable."]) | ||
| } | ||
| let file = try AVAudioFile(forWriting: url, settings: [ | ||
| AVFormatIDKey: Int(kAudioFormatMPEG4AAC), | ||
| AVSampleRateKey: format.sampleRate, | ||
|
|
@@ -434,40 +438,32 @@ struct AudioRecorderSheet: View { | |
| } | ||
| .disabled(recorder.isPreparing || isSaving) | ||
| } | ||
| if recorder.isPreparingTranscription { | ||
| ProgressView(CaptureLocalization.text("Preparing transcription…", locale: locale)) | ||
| } | ||
| if recorder.hasRecording, transcriptionEnabled, !languages.isEmpty { | ||
| if recorder.hasRecording, transcriptionEnabled, !languages.isEmpty || recorder.isPreparingTranscription { | ||
| ScrollView { | ||
| Text(recorder.liveTranscript.isEmpty | ||
| ? CaptureLocalization.text("Listening…", locale: locale) : recorder.liveTranscript) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .textSelection(.enabled) | ||
| .accessibilityIdentifier("audio.transcript") | ||
| VStack(alignment: .leading, spacing: 12) { | ||
| if recorder.isPreparingTranscription { | ||
| ProgressView(CaptureLocalization.text("Preparing transcription…", locale: locale)) | ||
| } | ||
| if !recorder.liveTranscript.isEmpty || !recorder.isPreparingTranscription { | ||
| Text(recorder.liveTranscript.isEmpty | ||
| ? CaptureLocalization.text("Listening…", locale: locale) : recorder.liveTranscript) | ||
| .textSelection(.enabled) | ||
| .accessibilityIdentifier("audio.transcript") | ||
| } | ||
| } | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| } | ||
| .defaultScrollAnchor(.bottom) | ||
|
Comment on lines
+441
to
456
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| Button { | ||
| if recorder.hasRecording { | ||
| Task { await save() } | ||
| } else { | ||
| startRecording() | ||
| } | ||
| } label: { | ||
| ZStack { | ||
| Circle().fill(Color.red).frame(width: 72, height: 72) | ||
| Image(systemName: recorder.hasRecording ? "stop.fill" : "mic.fill").foregroundStyle(.white) | ||
| } | ||
| } | ||
| .disabled(recorder.isPreparing || isSaving) | ||
| .accessibilityIdentifier(recorder.hasRecording ? "button.audio.stop" : "button.audio.start") | ||
| .accessibilityLabel(CaptureLocalization.text(recorder.hasRecording ? "Stop recording" : "Start recording", locale: locale)) | ||
| if let error = saveError ?? recorder.errorMessage { | ||
| Text(error).foregroundStyle(.red).accessibilityIdentifier("audio.recording.error") | ||
| } | ||
| Spacer(minLength: 0) | ||
| } | ||
| .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) | ||
| .padding(24) | ||
| .safeAreaInset(edge: .bottom, spacing: 0) { | ||
| recordingButton.padding(.horizontal, 24).padding(.bottom, 24) | ||
| } | ||
| .navigationTitle(CaptureLocalization.text("Audio recording", locale: locale)) | ||
| .navigationBarTitleDisplayMode(.inline) | ||
| .toolbar { | ||
|
|
@@ -508,6 +504,24 @@ struct AudioRecorderSheet: View { | |
| AudioRecordingPolicy.transcriptionLocale(lastUsed: language) | ||
| } | ||
|
|
||
| private var recordingButton: some View { | ||
| Button { | ||
| if recorder.hasRecording { | ||
| Task { await save() } | ||
| } else { | ||
| startRecording() | ||
| } | ||
| } label: { | ||
| ZStack { | ||
| Circle().fill(Color.red).frame(width: 72, height: 72) | ||
| Image(systemName: recorder.hasRecording ? "stop.fill" : "mic.fill").foregroundStyle(.white) | ||
| } | ||
| } | ||
| .disabled(recorder.isPreparing || isSaving) | ||
| .accessibilityIdentifier(recorder.hasRecording ? "button.audio.stop" : "button.audio.start") | ||
| .accessibilityLabel(CaptureLocalization.text(recorder.hasRecording ? "Stop recording" : "Start recording", locale: locale)) | ||
| } | ||
|
|
||
| private func startRecording() { | ||
| startTask = Task { | ||
| await recorder.start(locale: locale, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Preparation status hidden by long transcripts
After a long transcript, changing languages places
preparingTranscriptionabove the current scroll position. The preparation status stays hidden while recognition starts; the iOSAudioRecorderSheetuses the same ordering.Learn more
The transcript scroll region preserves its position when its content changes. After enough text fills the 180 dp region, the user scrolls to the bottom automatically through
LaunchedEffect(state.transcript)in AudioRecordingDialog. Starting a new transcription inserts progress at the top of this same region without changingstate.transcript, leaving the status outside the viewport. AudioRecorderSheet also places its progress view before the existing transcript inside a scrolling region.Example: Record enough speech to fill the transcript, then choose another language. The old transcript stays in view, but the new preparation message sits above it and remains unseen.
Recommended fix: Keep the preparation indicator outside the scrollable transcript content, or explicitly keep it visible while retaining the transcript in the fixed-height region. Apply the same layout rule to both platform sheets.
Was this helpful? React with 👍 or 👎 to provide feedback.