Foot Detection
unknown
plain_text
a year ago
30 kB
9
Indexable
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:isolate';
import 'dart:math' as math;
import 'package:flutter/services.dart' show rootBundle;
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_mlkit_pose_detection/google_mlkit_pose_detection.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:tflite_flutter/tflite_flutter.dart';
import 'package:stride/config/theme/app_theme.dart';
import 'package:stride/config/theme/color_manager.dart';
import 'package:stride/core/common/constant/strings.dart';
import 'package:stride/core/common/helper/main_helper.dart';
import 'package:stride/core/common/shared_widgets/custom_bottom_sheet.dart';
import 'package:stride/core/common/shared_widgets/loading_widget.dart';
import 'package:stride/feature/camera_detection/presentation/notifier/bluetooth_notifier.dart';
import 'package:stride/feature/home_feature/presentation/home_page/notifier/home_notifier.dart';
import 'package:stride/feature/home_feature/presentation/home_page/page/home_page.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stride/feature/intake_feature/data/models/intake_param.dart';
import 'package:stride/feature/intake_feature/presentation/page/intake_questions_page.dart';
import '../../../../config/navigator/navigator.dart';
import '../../../../core/common/constant/assets.dart';
/// Persistent TFLite Isolate that owns the Interpreter and does inference.
class _TfLiteIsolate {
Isolate? _iso;
SendPort? _send;
late ReceivePort _recv;
Completer<double>? _pending;
Future<void> start({
required TransferableTypedData modelBytesTTD,
required int inW,
required int inH,
required bool channelFirst,
}) async {
_recv = ReceivePort();
_iso = await Isolate.spawn<_IsoInitMsg>(
_tfEntry,
_IsoInitMsg(_recv.sendPort, modelBytesTTD, inW, inH, channelFirst),
errorsAreFatal: true,
);
final ready = Completer<void>();
_recv.listen((msg) {
if (msg is SendPort) {
_send = msg;
if (!ready.isCompleted) ready.complete();
} else if (msg is double) {
_pending?.complete(msg);
_pending = null;
}
});
await ready.future;
}
Future<double> infer(Float32List tensor) async {
if (_send == null) return 0.0;
if (_pending != null) return await _pending!.future;
_pending = Completer<double>();
_send!.send(tensor);
return _pending!.future;
}
void dispose() {
try {
_send?.send(null);
} catch (_) {}
_recv.close();
_iso?.kill(priority: Isolate.immediate);
_iso = null;
_send = null;
}
}
class _IsoInitMsg {
final SendPort uiPort;
final TransferableTypedData modelBytesTTD;
final int inW, inH;
final bool channelFirst;
_IsoInitMsg(this.uiPort, this.modelBytesTTD, this.inW, this.inH, this.channelFirst);
}
void _tfEntry(_IsoInitMsg init) async {
final uiPort = init.uiPort;
final inW = init.inW, inH = init.inH;
// Load model bytes in isolate
final Uint8List modelBytes = init.modelBytesTTD.materialize().asUint8List();
final interpreter = Interpreter.fromBuffer(modelBytes);
// Output shape
final outT = interpreter.getOutputTensors().first;
final outShape = outT.shape; // [1,C,N] or [1,N,C]
final channelFirstOut =
(outShape.length == 3 && outShape[1] <= 32 && outShape[2] >= 1000);
final C = channelFirstOut ? outShape[1] : outShape[2];
final N = channelFirstOut ? outShape[2] : outShape[1];
final port = ReceivePort();
uiPort.send(port.sendPort);
await for (final msg in port) {
if (msg == null) {
try { interpreter.close(); } catch (_) {}
port.close();
break;
}
final Float32List flat = msg as Float32List;
// Build input [1,H,W,3]
final input = List.generate(
1,
(_) => List.generate(
inH,
(_) => List.generate(inW, (_) => List.filled(3, 0.0)),
),
);
int k = 0;
for (int y = 0; y < inH; y++) {
for (int x = 0; x < inW; x++) {
input[0][y][x][0] = flat[k++];
input[0][y][x][1] = flat[k++];
input[0][y][x][2] = flat[k++];
}
}
final output = channelFirstOut
? List.generate(1, (_) => List.generate(C, (_) => List.filled(N, 0.0)))
: List.generate(1, (_) => List.generate(N, (_) => List.filled(C, 0.0)));
try {
interpreter.run(input, output);
// ✅ Foot-only model: use objectness only
double maxConf = 0.0;
if (channelFirstOut) {
for (int i = 0; i < N; i++) {
final obj = 1.0 / (1.0 + math.exp(-output[0][4][i]));
if (obj > maxConf) maxConf = obj;
}
} else {
for (int i = 0; i < N; i++) {
final obj = 1.0 / (1.0 + math.exp(-output[0][i][4]));
if (obj > maxConf) maxConf = obj;
}
}
// 📢 Live logging from isolate
debugPrint('[Isolate] Foot confidence: ${(maxConf * 100).toStringAsFixed(2)}%');
uiPort.send(maxConf);
} catch (_) {
uiPort.send(0.0);
}
}
}
Future<Map<String, dynamic>?> _convertYUVToRGBInIsolate(
Map<String, dynamic> params,
) async {
try {
final int width = params['width'] as int;
final int height = params['height'] as int;
final Uint8List yBytes = params['yBytes'] as Uint8List;
final Uint8List uBytes = params['uBytes'] as Uint8List;
final Uint8List vBytes = params['vBytes'] as Uint8List;
final int yRowStride = params['yRowStride'] as int;
final int uvRowStride = params['uvRowStride'] as int;
final int uvPixelStride = params['uvPixelStride'] as int;
final int targetW = params['targetW'] as int;
final int targetH = params['targetH'] as int;
// 1) YUV420 → RGBA (no PNG to avoid extra CPU)
final rgbaBytes = Uint8List(width * height * 4);
int p = 0;
for (int y = 0; y < height; y++) {
final int yBase = y * yRowStride;
final int uvBase = (y >> 1) * uvRowStride;
for (int x = 0; x < width; x++) {
final int yp = yBytes[yBase + x];
final int uvIndex = uvBase + (x >> 1) * uvPixelStride;
final int up = uBytes[uvIndex];
final int vp = vBytes[uvIndex];
final double yf = yp.toDouble();
final double uf = up.toDouble() - 128.0;
final double vf = vp.toDouble() - 128.0;
int r = (yf + 1.13983 * vf).round();
int g = (yf - 0.39465 * uf - 0.58060 * vf).round();
int b = (yf + 2.03211 * uf).round();
if (r < 0) r = 0; else if (r > 255) r = 255;
if (g < 0) g = 0; else if (g > 255) g = 255;
if (b < 0) b = 0; else if (b > 255) b = 255;
rgbaBytes[p++] = r;
rgbaBytes[p++] = g;
rgbaBytes[p++] = b;
rgbaBytes[p++] = 255; // alpha
}
}
// 2) Downscale to model input size
final img.Image src = img.Image.fromBytes(
width: width,
height: height,
bytes: rgbaBytes.buffer,
numChannels: 4,
format: img.Format.uint8,
);
final img.Image resized =
img.copyResize(src, width: targetW, height: targetH);
// 3) Build normalized 4D tensor [1,H,W,3] (double 0..1)
final List input4d = List.generate(
1,
(_) => List.generate(
targetH,
(_) => List.generate(targetW, (_) => List.filled(3, 0.0)),
),
);
for (int y = 0; y < targetH; y++) {
for (int x = 0; x < targetW; x++) {
final px = resized.getPixel(x, y); // ColorRgb8 / Pixel with r,g,b
input4d[0][y][x][0] = px.r / 255.0;
input4d[0][y][x][1] = px.g / 255.0;
input4d[0][y][x][2] = px.b / 255.0;
}
}
return {
'rgba': rgbaBytes,
'width': width,
'height': height,
'input4d': input4d,
};
} catch (e) {
print('YUV conversion error: $e');
return null;
}
}
/// CPU crop for captured still image (used when saving photo)
Future<Uint8List?> _cropImageInBackground(Map<String, dynamic> params) async {
try {
final Uint8List imageBytes = params['imageBytes'];
final double screenWidth = params['screenWidth'];
final double screenHeight = params['screenHeight'];
final raw = img.decodeImage(imageBytes);
if (raw == null) return null;
final imageW = raw.width.toDouble();
final imageH = raw.height.toDouble();
final imageAspect = imageW / imageH;
final screenAspect = screenWidth / screenHeight;
late img.Image cropped;
if (screenAspect > imageAspect) {
final newH = (imageW / screenAspect).round();
final top = ((imageH - newH) / 2).round();
cropped = img.copyCrop(
raw,
x: 0,
y: top,
width: raw.width,
height: newH,
);
} else {
final newW = (imageH * screenAspect).round();
final left = ((imageW - newW) / 2).round();
cropped = img.copyCrop(
raw,
x: left,
y: 0,
width: newW,
height: raw.height,
);
}
final resized = img.copyResize(
cropped,
width: screenWidth.round(),
height: screenHeight.round(),
);
return Uint8List.fromList(img.encodePng(resized));
} catch (e) {
debugPrint('Error in _cropImageInBackground: $e');
return null;
}
}
class TakePictureScreen extends StatefulWidget {
const TakePictureScreen({super.key});
@override
State<TakePictureScreen> createState() => _TakePictureScreenState();
}
class _TakePictureScreenState extends State<TakePictureScreen>
with TickerProviderStateMixin {
late PoseDetector _poseDetector;
// Foot detection state
bool _footDetected = false;
bool _isTakingPicture = false;
bool _detectionTimeoutReached = false;
bool _isProcessing = false;
double _confidence = 0.0;
late AnimationController _streamingController;
Timer? _poseCheckTimer; // not used anymore for detection, kept for compatibility
late BluetoothNotifier _bluetoothNotifier;
late HomeNotifier _homeNotifier;
// Camera
CameraController? _cameraController;
bool _isCameraInitialized = false;
List<CameraDescription>? _cameras;
// Performance tracking
int _processedFrames = 0;
DateTime _lastFpsCheck = DateTime.now();
double _currentFps = 0.0;
final Duration frameInterval = const Duration(milliseconds: 33); // throttle ~5 FPS
DateTime lastProcessed = DateTime.now();
// TFLite model info
late Interpreter _interpreter; // only used to read shapes here (we run in isolate)
bool _tfliteInitialized = false;
int _inW = 640, _inH = 640; // you set 256x256 for speed
bool _channelFirst = true; // output parsing info (for isolate)
int _numChannels = 9;
int _numAnchors = 8400;
// Convert queue (only latest frame)
bool _isConvertingImage = false;
CameraImage? _pendingImage;
// Persistent inference isolate
late _TfLiteIsolate _tfIso;
@override
void initState() {
super.initState();
_initializeComponents();
}
Future<void> _initializeCamera() async {
try {
// Clean up if already initialized
if (_cameraController != null) {
try {
await _cameraController!.stopImageStream();
} catch (_) {}
await _cameraController!.dispose();
_cameraController = null;
}
_cameras = await availableCameras();
if (_cameras == null || _cameras!.isEmpty) {
debugPrint('No cameras available');
return;
}
// Prefer back camera
final CameraDescription camera = _cameras!
.firstWhere((c) => c.lensDirection == CameraLensDirection.back,
orElse: () => _cameras!.first);
_cameraController = CameraController(
camera,
ResolutionPreset.low,
enableAudio: false,
imageFormatGroup:
Platform.isIOS ? ImageFormatGroup.bgra8888 : ImageFormatGroup.yuv420,
);
await _cameraController!.initialize();
// Best-effort conservative settings
try {
await _cameraController!.setFlashMode(FlashMode.off);
await _cameraController!.setFocusMode(FocusMode.auto);
await _cameraController!.setExposureMode(ExposureMode.auto);
await _cameraController!.setZoomLevel(1.0);
} catch (_) {}
if (!mounted) return;
setState(() => _isCameraInitialized = true);
// Start stream; conversion is throttled in _processCameraImage()
await _cameraController!.startImageStream((CameraImage cameraImage) {
// Drop if conversion is running (prevents backlog)
_processCameraImage(cameraImage);
});
} catch (e) {
debugPrint('Error initializing camera: $e');
}
}
void _processCameraImage(CameraImage cameraImage) {
if (!_tfliteInitialized) return;
// Throttle frames
final now = DateTime.now();
if (now.difference(lastProcessed) < frameInterval) return;
lastProcessed = now;
_pendingImage = cameraImage;
_convertPendingImage();
}
void _convertPendingImage() async {
if (_pendingImage == null || _isConvertingImage) return;
_isConvertingImage = true;
final imageToProcess = _pendingImage!;
_pendingImage = null;
try {
final yPlane = imageToProcess.planes[0];
final uPlane = imageToProcess.planes[1];
final vPlane = imageToProcess.planes[2];
// Heavy work in isolate: YUV -> RGBA, resize to (_inW,_inH), normalize
final result = await compute(_convertYUVToRGBInIsolate, {
'width': imageToProcess.width,
'height': imageToProcess.height,
'yBytes': yPlane.bytes,
'uBytes': uPlane.bytes,
'vBytes': vPlane.bytes,
'yRowStride': yPlane.bytesPerRow,
'uvRowStride': uPlane.bytesPerRow,
'uvPixelStride': uPlane.bytesPerPixel ?? 1,
'targetW': _inW,
'targetH': _inH,
});
if (result != null) {
// result['input4d'] is [1][H][W][3] (double/num). Flatten to Float32List.
final List nested = result['input4d'] as List;
final flat = Float32List(_inH * _inW * 3);
int k = 0;
for (int y = 0; y < _inH; y++) {
final List row = nested[0][y] as List;
for (int x = 0; x < _inW; x++) {
final List px = row[x] as List; // [r,g,b] normalized 0..1
flat[k++] = (px[0] as num).toDouble();
flat[k++] = (px[1] as num).toDouble();
flat[k++] = (px[2] as num).toDouble();
}
}
final double conf = await _tfIso.infer(flat);
log('Foot confidence: ${(conf * 100).toStringAsFixed(2)}%');
if (mounted) {
setState(() {
_confidence = conf;
});
}
final bool detected = conf > 0.70;
if (mounted) {
// Only update the one boolean – cheap re-build.
setState(() => _footDetected = detected);
}
// FPS counter (update UI at most twice per second)
_processedFrames++;
final now = DateTime.now();
final elapsedMs = now.difference(_lastFpsCheck).inMilliseconds;
if (elapsedMs >= 500) {
_currentFps = (_processedFrames * 1000.0) / elapsedMs;
_processedFrames = 0;
_lastFpsCheck = now;
if (mounted) setState(() {}); // lightweight repaint for the chip text
}
}
} catch (e) {
debugPrint('Error converting frame: $e');
} finally {
_isConvertingImage = false;
// If a newer frame arrived while we were working, process it now.
if (_pendingImage != null) {
_convertPendingImage();
}
}
}
Future<void> _initTFLiteModel() async {
try {
final mainInterp = await Interpreter.fromAsset('assets/foot_detection_model.tflite');
_tfliteInitialized = true;
final inTensors = mainInterp.getInputTensors();
if (inTensors.isNotEmpty) {
final shape = inTensors.first.shape; // [1,H,W,3]
if (shape.length == 4) {
_inH = shape[1];
_inW = shape[2];
}
debugPrint('Model input shape: $shape');
}
final outTensors = mainInterp.getOutputTensors();
if (outTensors.isNotEmpty) {
final shape = outTensors.first.shape;
debugPrint('Model output shape: $shape');
if (shape.length == 3) {
if (shape[1] <= 32 && shape[2] >= 1000) {
_channelFirst = true;
_numChannels = shape[1];
_numAnchors = shape[2];
} else {
_channelFirst = false;
_numAnchors = shape[1];
_numChannels = shape[2];
}
}
}
debugPrint(
'TFLite ready. in: ${_inW}x${_inH}, channels=$_numChannels, anchors=$_numAnchors, channelFirst=$_channelFirst');
final byteData = await rootBundle.load('assets/foot_detection_model.tflite');
final ttd = TransferableTypedData.fromList([byteData.buffer.asUint8List()]);
_tfIso = _TfLiteIsolate();
await _tfIso.start(
modelBytesTTD: ttd,
inW: _inW,
inH: _inH,
channelFirst: _channelFirst,
);
mainInterp.close();
} catch (e) {
debugPrint('TFLite initialization error: $e');
}
}
void _initializeComponents() {
_bluetoothNotifier = Provider.of<BluetoothNotifier>(context, listen: false);
_homeNotifier = Provider.of<HomeNotifier>(context, listen: false);
// Which foot to scan
final scanFoot = _homeNotifier.statusEntity.scanData?.foot ?? 'left';
final isBothFeetScan = scanFoot == 'both';
final scanStage = _bluetoothNotifier.scanStage;
if (isBothFeetScan) {
if (scanStage == FootScanStage.leftScanned) {
_bluetoothNotifier.setCurrentFoot('right');
} else {
_bluetoothNotifier.setCurrentFoot('left');
}
} else {
_bluetoothNotifier.setCurrentFoot(scanFoot);
}
// Keep PoseDetector as requested (not used in this pipeline)
_poseDetector = PoseDetector(
options: PoseDetectorOptions(
mode: PoseDetectionMode.stream,
model: PoseDetectionModel.accurate,
),
);
_streamingController = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat();
_initTFLiteModel().then((_) {
_initializeCamera();
_startOptimizedDetectionLoop();
});
}
void _startOptimizedDetectionLoop() {
_poseCheckTimer?.cancel();
}
@override
void dispose() {
_streamingController.dispose();
_poseDetector.close();
_poseCheckTimer?.cancel();
if (_tfliteInitialized) {
try {
_interpreter.close();
} catch (_) {}
}
try {
_tfIso.dispose();
} catch (_) {}
try {
_cameraController?.stopImageStream();
} catch (_) {}
_cameraController?.dispose();
super.dispose();
}
Future<void> _capturePhoto(
BuildContext context,
BluetoothNotifier bluetoothNotifier,
HomeNotifier homeNotifier,
) async {
if (_isTakingPicture || _cameraController == null) return;
setState(() => _isTakingPicture = true);
try {
final XFile imageFile = await _cameraController!.takePicture();
final imageBytes = await imageFile.readAsBytes();
final croppedBytes = await compute(_cropImageInBackground, {
'imageBytes': imageBytes,
'screenWidth': MediaQuery.of(context).size.width,
'screenHeight': MediaQuery.of(context).size.height,
}).timeout(const Duration(seconds: 3), onTimeout: () => null);
if (croppedBytes == null) throw Exception('Failed to crop image');
final dir = await getTemporaryDirectory();
final path = '${dir.path}/${DateTime.now().millisecondsSinceEpoch}.png';
final file = File(path);
await file.writeAsBytes(croppedBytes);
final currentScanningFoot =
_homeNotifier.statusEntity.scanData?.foot ?? 'left';
final isBothFeetScan = currentScanningFoot == 'both';
if (_footDetected || _detectionTimeoutReached) {
if (!mounted) return;
if (isBothFeetScan) {
if (_bluetoothNotifier.currentFoot == 'left') {
await takePicture(context, bluetoothNotifier, path, 'left', false);
_bluetoothNotifier.setScanStage(FootScanStage.leftScanned);
navigatePushAndRemoveUntil(
context: context,
pageName: IntakeQuestionsPage(title: 'right'),
);
return;
} else {
await takePicture(context, bluetoothNotifier, path, 'right', true);
_bluetoothNotifier.setScanStage(FootScanStage.rightScanned);
navigatePushAndRemoveUntil(context: context, pageName: HomePage());
return;
}
} else {
await takePicture(context, bluetoothNotifier, path,
_bluetoothNotifier.currentFoot ?? 'left', true);
navigatePushAndRemoveUntil(context: context, pageName: HomePage());
}
} else {
Helper().handlelocalErrors(title: 'The feet is outside the shape');
}
} catch (e) {
debugPrint('Capture error: $e');
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Capture error: $e')));
}
} finally {
if (mounted) setState(() => _isTakingPicture = false);
}
}
String _getCurrentFootDisplayText() {
final scanFoot = _homeNotifier.statusEntity.scanData?.foot ?? 'left';
final isBothFeetScan = scanFoot == 'both';
final currentScanningFoot = _bluetoothNotifier.currentFoot ?? scanFoot;
if (isBothFeetScan) {
if (currentScanningFoot == 'left') return 'Position Your Left Foot';
if (currentScanningFoot == 'right') return 'Position Your Right Foot';
}
return 'Position Your Foot';
}
@override
Widget build(BuildContext context) {
final bluetoothNotifier = Provider.of<BluetoothNotifier>(context);
final homeNotifier = Provider.of<HomeNotifier>(context);
return Scaffold(
backgroundColor: ColorManager.white,
appBar: AppBar(
title: Text(
_getCurrentFootDisplayText(),
style: Theme.of(context).textTheme.headlineMedium,
),
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: () =>
navigatePushAndRemoveUntil(context: context, pageName: HomePage()),
icon: const Icon(Icons.close),
),
),
body: bluetoothNotifier.footLoading == true
? const LoadingWidget()
: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
if (!_isCameraInitialized)
const LoadingWidget()
else
AspectRatio(
aspectRatio: _cameraController!.value.previewSize!.height /
_cameraController!.value.previewSize!.width,
child: CameraPreview(_cameraController!),
),
// Foot overlay
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: _detectionTimeoutReached
? const EdgeInsets.only(bottom: 120, top: 10)
: const EdgeInsets.only(bottom: 40, top: 30),
child: ColorFiltered(
colorFilter: ColorFilter.mode(
_detectionTimeoutReached
? Colors.white
: _footDetected
? Colors.green
: Colors.red,
BlendMode.srcIn,
),
child: Image.asset(
(_bluetoothNotifier.currentFoot ?? 'left') == 'left'
? Assets.rightFoot
: Assets.leftFoot,
width: MediaQuery.sizeOf(context).width * 0.9,
height: MediaQuery.sizeOf(context).height,
fit: BoxFit.fill,
),
),
),
),
if (_isProcessing)
const Positioned(
top: 100,
left: 20,
child: _ProcessingChipStatic(),
),
Positioned(
top: 160,
left: 20,
child: _ConfidenceBadge(value: _confidence),
),
if (_detectionTimeoutReached)
Positioned(
bottom: 0,
child: _TimeoutSheet(
onCapture: _isTakingPicture
? null
: () => _capturePhoto(
context,
bluetoothNotifier,
homeNotifier,
),
isTakingPicture: _isTakingPicture,
),
),
],
),
);
}
}
class _ConfidenceBadge extends StatelessWidget {
final double value;
const _ConfidenceBadge({required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Conf: ${(value * 100).toStringAsFixed(1)}%',
style: const TextStyle(color: Colors.white, fontSize: 11),
),
);
}
}
class _ProcessingChipStatic extends StatelessWidget {
const _ProcessingChipStatic();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Row(mainAxisSize: MainAxisSize.min, children: const [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 8),
Text(
'Scanning…',
style: TextStyle(color: Colors.white, fontSize: 12),
),
]),
);
}
}
class _TimeoutSheet extends StatelessWidget {
final VoidCallback? onCapture;
final bool isTakingPicture;
const _TimeoutSheet({required this.onCapture, required this.isTakingPicture});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
width: MediaQuery.sizeOf(context).width,
height: MediaQuery.sizeOf(context).height * 0.15,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
child: Column(
children: [
Text(
'Failed to detect the foot, please capture manually',
style: Theme.of(context)
.textTheme
.displaySmall!
.copyWith(fontSize: 11),
),
const SizedBox(height: 12),
InkWell(
onTap: onCapture,
child: Center(
child: CircleAvatar(
radius: 32,
backgroundColor: isTakingPicture
? Colors.grey
: ColorManager.primaryBlue.withOpacity(0.5),
child: isTakingPicture
? const CircularProgressIndicator(color: Colors.white)
: CircleAvatar(
backgroundColor: ColorManager.primaryBlue,
radius: 28,
),
),
),
),
],
),
);
}
}
Future<void> takePicture(
BuildContext context,
BluetoothNotifier bluetoothNotifier,
String path,
String foot,
bool isLastFoot,
) async {
try {
final file = File(path);
if (!await file.exists()) {
throw Exception('Image file not found at path: $path');
}
final originalBytes = await file.readAsBytes();
final dir = await getTemporaryDirectory();
final fullPath =
'${dir.path}/full_${DateTime.now().millisecondsSinceEpoch}.png';
final fullImageFile = await File(fullPath).writeAsBytes(originalBytes);
await bluetoothNotifier.executeFootPic(
fullImageFile.path,
foot,
bluetoothNotifier.intakeFormParam ?? IntakeParam(),
);
if (context.mounted) {
_showBottomSheet(context);
await Future.delayed(const Duration(seconds: 2), () {
if (context.mounted) {
Provider.of<HomeNotifier>(context, listen: false).goToReview();
if (isLastFoot) {
navigatePushAndRemoveUntil(context: context, pageName: HomePage());
}
}
});
}
} catch (e) {
debugPrint('Error in takePicture: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to save image: $e')),
);
}
}
}
void _showBottomSheet(BuildContext context) {
CustomBottomSheet.show(
context: context,
isDismissible: true,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Container(
width: 50,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 20),
SvgPicture.asset(Assets.checkMarkIcon),
Text(
'Scan Complete',
style: AppThemes.lightTheme.textTheme.displayMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
Strings.verificationSentSMS,
textAlign: TextAlign.center,
style: AppThemes.lightTheme.textTheme.displaySmall!
.copyWith(fontWeight: FontWeight.w500, letterSpacing: 0.4),
),
],
),
);
}
Editor is loading...
Leave a Comment