Haptic on meal log, VIBRATE permission
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run

Fires on the tap before the write; rejected logs stay silent. The
dopamine-ux TODO now lists only the remaining work.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWVUTaKFW8m7YrvWbzV
This commit is contained in:
Krzysztof Rudnicki 2026-11-02 08:09:24 +01:00
parent 2a674774bb
commit a30628ab96
5 changed files with 101 additions and 1 deletions

View File

@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Sync (github_client.dart) needs raw socket access. -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- HapticFeedback on a successful meal log (log_meal_screen.dart). -->
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- Android 13+ requires runtime opt-in to post notifications; requested
at launch via NotificationService.requestPermission(). -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

View File

@ -25,6 +25,7 @@ import 'package:diet_guard_app/widgets/macro_input_row.dart';
import 'package:diet_guard_app/widgets/sync_health_banner.dart';
import 'package:diet_guard_app/widgets/today_progress_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
/// Lets the user log one food item, with food-bank autocomplete and
@ -146,6 +147,8 @@ class _LogMealScreenState extends State<LogMealScreen>
});
return;
}
// On the tap, before the write/sync: waiting for them is just latency.
unawaited(HapticFeedback.mediumImpact());
await LogStorageService.instance.logMeal(
desc,
nutritionFromControllers(_macros, _source),

View File

@ -2,7 +2,7 @@ name: diet_guard_app
description: "Companion phone app for diet_guard: log meals on the go."
publish_to: 'none'
version: 1.0.127+1
version: 1.0.128+1
environment:
sdk: ^3.12.2

View File

@ -0,0 +1,79 @@
import 'dart:io';
import 'package:diet_guard_app/models/meal_schedule.dart';
import 'package:diet_guard_app/screens/log_meal_screen.dart';
import 'package:diet_guard_app/services/app_settings_service.dart';
import 'package:diet_guard_app/services/budget_history_service.dart';
import 'package:diet_guard_app/services/document_store_io.dart';
import 'package:diet_guard_app/services/foodbank_service.dart';
import 'package:diet_guard_app/services/log_storage_service.dart';
import 'package:diet_guard_app/services/meal_schedule_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
late Directory dir;
final haptics = <String>[];
setUp(() async {
haptics.clear();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'HapticFeedback.vibrate') {
haptics.add(call.arguments as String);
}
return null;
});
dir = await Directory.systemTemp.createTemp('diet_guard_haptic_');
LogStorageService.resetForTesting(store: FileDocumentStore(dir));
FoodBankService.resetForTesting(store: FileDocumentStore(dir));
AppSettingsService.resetForTesting(store: FileDocumentStore(dir));
BudgetHistoryService.resetForTesting(store: FileDocumentStore(dir));
await MealScheduleService.initForTesting(FileDocumentStore(dir));
});
tearDown(() async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null);
LogStorageService.resetForTesting();
FoodBankService.resetForTesting();
AppSettingsService.resetForTesting();
BudgetHistoryService.resetForTesting();
MealScheduleService.resetForTesting();
await dir.delete(recursive: true);
});
Future<void> settle(WidgetTester tester) async {
await Future<void>.delayed(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
}
testWidgets('logging a meal buzzes once, on the tap', (tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(home: LogMealScreen()));
await settle(tester);
await tester.enterText(find.byType(TextField).at(0), 'toast');
await settle(tester);
await tester.tap(find.byTooltip('Log meal'));
// One frame, no I/O wait: the haptic must not wait for the write.
await tester.pump();
expect(haptics, ['HapticFeedbackType.mediumImpact']);
await settle(tester);
expect(haptics, hasLength(1));
});
});
testWidgets('a rejected log (empty description) does not buzz', (
tester,
) async {
await tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(home: LogMealScreen()));
await settle(tester);
await tester.tap(find.byTooltip('Log meal'));
await settle(tester);
expect(find.text('Type what you ate first.'), findsOneWidget);
expect(haptics, isEmpty);
});
});
}

View File

@ -36,6 +36,22 @@ from the true state. Partial completion must never be recorded as DONE.
---
## status 2026-11-02 (audit session)
DONE and committed: the haptic (`HapticFeedback.mediumImpact()` fired on the
tap, before the write, only for a log that passed validation; test
`app/test/screens/log_meal_haptic_test.dart`) and the VIBRATE permission.
REMAINING (everything below this block that is not the haptic/VIBRATE):
- needs kuhy: approved text for the explicit confirmation (the old
`Logged "<meal>".` line was removed on purpose 2026-08-16, so the wording
and whether to bring it back is his call), and the sound toggle label.
- animated progress card / count-up numbers using motion tokens
(`~/src/utils/unified-design-system/DOCS-motion.md`), honouring
`disableAnimations`.
- step 2 sound: audio dependency, asset, opt-out `SwitchListTile`.
- on-phone check (haptic feel, remove-animations toggle): needs kuhy.
Make logging a meal feel satisfying in diet_guard_app
## findings (4, ranked by value over effort)