feat(meditate): dreams spoken: a microphone in the dream's editor, and Today's Speak it

The editor dictates where the cursor stands with halkyon_app's Dictation,
showing it listens and the words heard, listening again after each pause
until stopped or three silent listens, in the app's language. No
microphone where the device has no recogniser (Linux). On Android it asks
for recognition on the device; the privacy page says what each platform
does. Android's, iOS's and macOS's microphone declarations as Learn's.
ADR 0038, D15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 14:47:35 +01:00
commit 433e544d44
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -31,6 +31,7 @@ void main() {
late MemorySealedRowGateway settingsRows;
late MemorySealedRowGateway sittingRows;
late MemorySealedRowGateway dreamRows;
late FakeSpeechInput speech;
late RecordingBells bells;
late RecordingScreen screen;
@ -54,6 +55,7 @@ void main() {
settingsRows = MemorySealedRowGateway();
sittingRows = MemorySealedRowGateway();
dreamRows = MemorySealedRowGateway();
speech = FakeSpeechInput();
bells = RecordingBells();
screen = RecordingScreen();
store = MemoryInstanceStore();
@ -150,6 +152,7 @@ void main() {
dreams: dreams(),
bells: bells,
screen: screen,
speech: speech,
);
},
),
@ -931,6 +934,113 @@ void main() {
});
});
group('dictating a dream (ADR 0038, D15)', () {
/// Opens a new dream from the Journal's +.
Future<void> newDream(WidgetTester tester) async {
await start(tester, usedBefore: true);
await goTab(tester, 'Journal');
await tester.tap(find.byTooltip('Write down a dream'));
await tester.pumpAndSettle();
}
final text = find.byType(TextField).first;
testWidgets('the microphone writes what is said where the cursor stands, '
"in the app's language, showing it listens; tapped again, it stops", (
tester,
) async {
await newDream(tester);
await tester.enterText(text, 'I was there.');
tester.widget<TextField>(text).controller!.selection =
const TextSelection.collapsed(offset: 5);
await tester.tap(find.byTooltip('Dictate'));
await tester.pump();
expect(speech.languages, [startsWith('en')]);
expect(find.byTooltip('Stop dictating'), findsOneWidget);
expect(find.text('Listening…'), findsOneWidget);
speech.say('at the sea');
await tester.pump();
expect(find.text('Listening… at the sea'), findsOneWidget);
expect(
tester.widget<TextField>(text).controller!.text,
'I was at the sea there.',
);
await tester.tap(find.byTooltip('Stop dictating'));
await tester.pump();
expect(speech.listening, isFalse);
expect(find.byTooltip('Dictate'), findsOneWidget);
await tester.tap(find.widgetWithText(TextButton, 'Save'));
await tester.pumpAndSettle();
expect(
(await dreams().all()).single.dream.text,
'I was at the sea there.',
);
});
testWidgets("the recogniser's pause doesn't end it: it listens on, the "
'next words after the last, until stopped', (tester) async {
await newDream(tester);
await tester.tap(find.byTooltip('Dictate'));
await tester.pump();
speech
..say('A house')
..finish();
await tester.pump();
expect(speech.listening, isTrue);
expect(find.byTooltip('Stop dictating'), findsOneWidget);
speech.say('by the sea');
await tester.pump();
expect(
tester.widget<TextField>(text).controller!.text,
'A house by the sea',
);
});
testWidgets("where the device can't listen now, it says so", (
tester,
) async {
speech.listens = false;
await newDream(tester);
await tester.tap(find.byTooltip('Dictate'));
await tester.pumpAndSettle();
expect(find.textContaining("can't listen now"), findsOneWidget);
expect(find.byTooltip('Dictate'), findsOneWidget);
});
testWidgets('where the device recognises no speech, there is no '
'microphone, here or on Today', (tester) async {
speech.supported = false;
await withClock(Clock.fixed(DateTime(2026, 11, 3, 8)), () async {
await start(tester, usedBefore: true);
expect(find.text('Speak it'), findsNothing);
});
await goTab(tester, 'Journal');
await tester.tap(find.byTooltip('Write down a dream'));
await tester.pumpAndSettle();
expect(find.byTooltip('Dictate'), findsNothing);
});
testWidgets("Today's Speak it opens a new dream already listening", (
tester,
) async {
await withClock(Clock.fixed(DateTime(2026, 11, 3, 8)), () async {
await start(tester, usedBefore: true);
await tester.tap(find.text('Speak it'));
await tester.pumpAndSettle();
});
expect(find.widgetWithText(AppBar, 'New dream'), findsOneWidget);
expect(speech.listening, isTrue);
expect(find.byTooltip('Stop dictating'), findsOneWidget);
});
});
testWidgets('signed out, it asks how to start, with an account to create '
'where passwords are offered (ADR 0017), and no GitHub (ADR 0020, '
'D36)', (tester) async {