Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions android/app/src/main/kotlin/com/logseq/logseq/ui/LogseqApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 +222 to +225

Copy link
Copy Markdown

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 preparingTranscription above the current scroll position. The preparation status stays hidden while recognition starts; the iOS AudioRecorderSheet uses 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 changing state.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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +220 to +225

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Recording setup has no progress feedback

With transcription disabled, state.preparing has no visible indicator because preparation now lives inside the transcription-only region. The disabled Start button and 0:00 timer give no feedback while recording initializes.

Learn more

The service enters preparing while starting capture in start. The dialog disables Start during that state in AudioRecordingDialog. The only progress indicator now sits inside the region gated by request.transcriptionEnabled, so disabling transcription removes all setup feedback, including on Android versions where transcription is unavailable.

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.

Devin Review


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failed recordings display a listening status

When microphone access fails, state.hasRecording stays false, but the transcript region displays “Listening…” once preparation ends. Users see a listening status beside the recording error.

Learn more

The 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 hasRecording false. The dialog then retains an enabled transcription option and shows its listening placeholder beside the error.

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 state.hasRecording before showing the listening placeholder; keep preparation and any existing transcript separately visible as needed.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
}
}
}
(request.error ?: state.error)?.let {
Expand Down
64 changes: 39 additions & 25 deletions apple/Sources/Logseq/AudioRecording.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 iOS transcript height remains adaptive

Unlike the Android region, the iOS ScrollView has no fixed height. Check whether transcript growth still resizes the sheet despite the pinned recording button.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
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 {
Expand Down Expand Up @@ -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,
Expand Down
Loading