From 818919483b44b4f802ba621b7417490a2283f983 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 5 Nov 2026 00:57:49 +0800 Subject: [PATCH] Keep recording controls steady during transcription setup --- .../kotlin/com/logseq/logseq/ui/LogseqApp.kt | 31 ++++----- apple/Sources/Logseq/AudioRecording.swift | 64 +++++++++++-------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/android/app/src/main/kotlin/com/logseq/logseq/ui/LogseqApp.kt b/android/app/src/main/kotlin/com/logseq/logseq/ui/LogseqApp.kt index 427d279a..805480fa 100644 --- a/android/app/src/main/kotlin/com/logseq/logseq/ui/LogseqApp.kt +++ b/android/app/src/main/kotlin/com/logseq/logseq/ui/LogseqApp.kt @@ -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)) + } + if (state.transcript.isNotBlank() || (!state.preparing && !state.preparingTranscription)) { + SelectionContainer { + Text(state.transcript.ifBlank { stringResource(R.string.audio_listening) }, + modifier = Modifier.testTag("audio.transcript")) + } + } } } (request.error ?: state.error)?.let { diff --git a/apple/Sources/Logseq/AudioRecording.swift b/apple/Sources/Logseq/AudioRecording.swift index ba521cc6..96a7a1d8 100644 --- a/apple/Sources/Logseq/AudioRecording.swift +++ b/apple/Sources/Logseq/AudioRecording.swift @@ -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) } - 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,