Foot Detection

 avatar
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