wearable_health 0.0.2 copy "wearable_health: ^0.0.2" to clipboard
wearable_health: ^0.0.2 copied to clipboard

A flutter plugin for reading health data

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:wearable_health/controller/wearable_health.dart';
import 'package:wearable_health/model/health_connect/enums/hc_health_metric.dart';
import 'package:wearable_health/model/health_kit/enums/hk_health_metric.dart';
import 'package:wearable_health/service/converters/json/json_converter.dart';
import 'package:wearable_health/service/converters/json/json_converter_interface.dart';
import 'package:wearable_health/service/health_connect/data_factory.dart';
import 'package:wearable_health/service/health_connect/data_factory_interface.dart';
import 'package:wearable_health/service/health_kit/data_factory.dart';
import 'package:wearable_health/service/health_kit/data_factory_interface.dart';
import 'package:wearable_health_example/models/conversion_validity_result.dart';
import 'package:wearable_health_example/models/experimentation_result.dart';
import 'package:wearable_health_example/models/performance_test_result.dart';
import 'package:wearable_health_example/models/record_count_result.dart';
import 'package:wearable_health_example/services/data_export.dart';
import 'package:wearable_health_example/services/health_connect/hc_data_conversion_validation.dart';
import 'package:wearable_health_example/services/health_connect/hc_performance_test.dart';
import 'package:wearable_health_example/services/health_connect/hc_record_count.dart';
import 'package:wearable_health_example/services/health_kit/hk_data_conversion_validation.dart';
import 'package:wearable_health_example/services/health_kit/hk_performance_test.dart';
import 'package:wearable_health_example/services/health_kit/hk_record_count.dart';
import 'package:wearable_health_example/widgets/display_data.dart';
import 'package:wearable_health_example/widgets/performance_module.dart';

import 'widgets/data_conversion.dart';
import 'widgets/data_retrieval.dart';

enum ExperimentMode { historical, realTime }

void main() {
  runApp(const HealthPluginExampleApp());
}

class HealthPluginExampleApp extends StatelessWidget {
  const HealthPluginExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Health Plugin Experiment',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const ExperimentPage(),
    );
  }
}

class ExperimentPage extends StatefulWidget {
  const ExperimentPage({super.key});

  @override
  State<ExperimentPage> createState() => _ExperimentPageState();
}

class _ExperimentPageState extends State<ExperimentPage> {
  int _selectedPageIndex = 0;
  DateTime _startDate = DateTime.now().subtract(const Duration(days: 7));
  DateTime _endDate = DateTime.now();
  bool _dataAvailable = false;
  bool _isLoading = false;
  bool _showExperimentControls = false;
  Map<String, List<Map<String, dynamic>>>? _data;
  final Stopwatch _stopWatch = Stopwatch();

  ConversionValidityResult? conversionValidityResult;
  PerformanceTestResult? performanceTestResult;
  RecordCountResult? recordCountResult;

  late HCDataConversionValidation hcConversionValidator;
  late HCPerformanceTest hcPerformanceTester;
  late HCRecordCount hcRecordCounter;
  late HCDataFactory hcDataFactory;

  late HKDataConversionValidation hkConversionValidator;
  late HKPerformanceTest hkPerformanceTester;
  late HKRecordCount hkRecordCounter;
  late HKDataFactory hkDataFactory;

  late ResultExporter resultExporter;

  ExperimentMode _currentMode = ExperimentMode.historical;
  bool _isRealTimeSessionRunning = false;
  Timer? _realTimeTimer;
  List<dynamic> _activeDataTypes = [];

  static const Duration _realTimePollingInterval = Duration(minutes: 1);
  static const Duration _realTimeFetchWindow = Duration(minutes: 5);

  @override
  void initState() {
    super.initState();
    JsonConverter jsonConverter = JsonConverterImpl();
    hcDataFactory = HCDataFactoryImpl(jsonConverter);
    hkDataFactory = HKDataFactoryImpl(jsonConverter);
    hcConversionValidator = HCDataConversionValidation(hcDataFactory);
    hcPerformanceTester = HCPerformanceTest(hcDataFactory);
    hcRecordCounter = HCRecordCount();
    resultExporter = ResultExporter();
    hkConversionValidator = HKDataConversionValidation(hkDataFactory);
    hkPerformanceTester = HKPerformanceTest(hkDataFactory);
    hkRecordCounter = HKRecordCount();
  }

  @override
  void dispose() {
    _realTimeTimer?.cancel();
    super.dispose();
  }

  Widget _buildCurrentPageWidget() {
    switch (_selectedPageIndex) {
      case 0:
        return DataRetrievalModule(data: recordCountResult);
      case 1:
        return ConversionModule(stats: conversionValidityResult);
      case 2:
        return PerformanceModule(data: performanceTestResult);
      case 3:
        return DataDisplayModule(
          data: _data,
          hcDataFactory: hcDataFactory,
          hkDataFactory: hkDataFactory,
        );
      default:
        return const Center(child: Text("Page not found."));
    }
  }

  Future<void> _selectDate(BuildContext context, bool isStartDate) async {
    final DateTime initialDateTime = isStartDate ? _startDate : _endDate;

    final DateTime? pickedDate = await showDatePicker(
      context: context,
      initialDate: initialDateTime,
      firstDate: DateTime(2000),
      lastDate: DateTime.now().add(const Duration(days: 365)),
    );

    if (pickedDate == null || !mounted) return;

    final TimeOfDay? pickedTime = await showTimePicker(
      context: context,
      initialTime: TimeOfDay.fromDateTime(initialDateTime),
    );

    if (pickedTime == null || !mounted) return;

    final DateTime finalDateTime = DateTime(
      pickedDate.year,
      pickedDate.month,
      pickedDate.day,
      pickedTime.hour,
      pickedTime.minute,
    );

    setState(() {
      if (isStartDate) {
        if (finalDateTime.isAfter(_endDate)) {
          _startDate = finalDateTime;
          _endDate = finalDateTime.add(const Duration(hours: 1));
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text(
                'Start date was after end date. End date adjusted accordingly.',
              ),
            ),
          );
        } else {
          _startDate = finalDateTime;
        }
      } else {
        if (finalDateTime.isBefore(_startDate)) {
          _endDate = finalDateTime;
          _startDate = finalDateTime.subtract(const Duration(hours: 1));
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text(
                'End date was before start date. Start date adjusted accordingly.',
              ),
            ),
          );
        } else {
          _endDate = finalDateTime;
        }
      }
    });
  }

  void _handlePrimaryButtonAction() {
    if (_currentMode == ExperimentMode.historical) {
      _runHistoricalExperiment();
    } else {
      if (_isRealTimeSessionRunning) {
        _stopRealTimeSession();
      } else {
        _startRealTimeSession();
      }
    }
  }

  Future<bool> _requestPlatformPermissions(List<dynamic> dataTypes) async {
    var wearableHealthController = WearableHealth();
    try {
      if (Platform.isAndroid) {
        final sut = wearableHealthController.getGoogleHealthConnect();
        await sut.requestPermissions(
          List<HealthConnectHealthMetric>.from(dataTypes),
        );
      } else {
        final sut = wearableHealthController.getAppleHealthKit();
        await sut.requestPermissions(
          List<HealthKitHealthMetric>.from(dataTypes),
        );
      }
      return true;
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Permission error: ${e.toString()}')),
        );
      }
      if (kDebugMode) {
        print("Error requesting permissions: $e");
      }
      return false;
    }
  }

  Future<Map<String, List<Map<String, dynamic>>>?> _fetchDataForRange(
    List<dynamic> dataTypes,
    DateTimeRange range,
  ) async {
    var wearableHealthController = WearableHealth();
    Map<String, List<Map<String, dynamic>>>? resultData;
    try {
      if (Platform.isAndroid) {
        final sut = wearableHealthController.getGoogleHealthConnect();
        final platformResult = await sut.getRawData(
          List<HealthConnectHealthMetric>.from(dataTypes),
          range,
        );
        resultData = platformResult.data;
      } else {
        final sut = wearableHealthController.getAppleHealthKit();
        final platformResult = await sut.getRawData(
          List<HealthKitHealthMetric>.from(dataTypes),
          range,
        );
        if (kDebugMode) {
          print('--- _fetchDataForRange DEBUG (Android) ---');
          print('platformResult from sut.getRawData: $platformResult');
        }
        resultData = platformResult.data;
      }
      return resultData;
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error fetching data: ${e.toString()}')),
        );
      }
      if (kDebugMode) {
        print("Error in _fetchDataForRange: $e");
      }
      return null;
    }
  }

  Future<void> _runHistoricalExperiment() async {
    if (_currentMode == ExperimentMode.realTime && _isRealTimeSessionRunning) {
      if (mounted) {
        setState(() {
          _stopRealTimeSession();
        });
      }
    }
    if (_endDate.isBefore(_startDate)) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('End date must be after start date')),
      );
      return;
    }

    setState(() {
      _isLoading = true;
      _dataAvailable = false;
      _data = null;
      recordCountResult = null;
      conversionValidityResult = null;
      performanceTestResult = null;
    });

    _activeDataTypes =
        Platform.isAndroid
            ? [
              HealthConnectHealthMetric.heartRate,
              HealthConnectHealthMetric.heartRateVariability,
            ]
            : [
              HealthKitHealthMetric.heartRate,
              HealthKitHealthMetric.heartRateVariability,
            ];

    bool permissionsGranted = await _requestPlatformPermissions(
      _activeDataTypes,
    );
    if (!permissionsGranted) {
      if (mounted) setState(() => _isLoading = false);
      return;
    }

    _stopWatch.reset();
    _stopWatch.start();
    final fetchedData = await _fetchDataForRange(
      _activeDataTypes,
      DateTimeRange(start: _startDate, end: _endDate),
    );
    _stopWatch.stop();

    if (mounted) {
      if (fetchedData != null) {
        _data = fetchedData;
        if (Platform.isAndroid) {
          recordCountResult = hcRecordCounter.calculateRecordCount(_data!);
          conversionValidityResult = hcConversionValidator
              .performConversionValidation(_data!);
          performanceTestResult = hcPerformanceTester.getPerformanceResults(
            _data!,
            _stopWatch.elapsedMilliseconds,
            _stopWatch,
          );
        } else {
          recordCountResult = hkRecordCounter.calculateRecordCount(_data!);
          conversionValidityResult = hkConversionValidator
              .performConversionValidation(_data!);
          performanceTestResult = hkPerformanceTester.getPerformanceResults(
            _data!,
            _stopWatch.elapsedMilliseconds,
            _stopWatch,
          );
        }
        _dataAvailable = _data!.values.any((list) => list.isNotEmpty);
      } else {
        _dataAvailable = false;
      }
      setState(() => _isLoading = false);
    }
  }

  Future<void> _startRealTimeSession() async {
    setState(() {
      _isLoading = true;
    });

    _activeDataTypes =
        Platform.isAndroid
            ? [
              HealthConnectHealthMetric.heartRate,
              HealthConnectHealthMetric.heartRateVariability,
            ]
            : [
              HealthKitHealthMetric.heartRate,
              HealthKitHealthMetric.heartRateVariability,
            ];

    bool permissionsGranted = await _requestPlatformPermissions(
      _activeDataTypes,
    );
    if (!permissionsGranted) {
      if (mounted) setState(() => _isLoading = false);
      return;
    }

    if (mounted) {
      setState(() {
        _data = {};
        recordCountResult = null;
        conversionValidityResult = null;
        performanceTestResult =
            null;
        _dataAvailable = false;
        _isRealTimeSessionRunning = true;
        _isLoading = false;
      });
    }

    await _fetchAndProcessRealTimeData();

    _realTimeTimer?.cancel();
    _realTimeTimer = Timer.periodic(_realTimePollingInterval, (timer) {
      if (!_isRealTimeSessionRunning || !mounted) {
        timer.cancel();
        return;
      }
      _fetchAndProcessRealTimeData();
    });
  }

  Future<void> _fetchAndProcessRealTimeData() async {
    if (!mounted || !_isRealTimeSessionRunning) {
      return;
    }

    final DateTime endTime = DateTime.now();
    final DateTime startTime = endTime.subtract(_realTimeFetchWindow);

    final Map<String, List<Map<String, dynamic>>>? newlyFetchedChunk =
    await _fetchDataForRange(
      _activeDataTypes,
      DateTimeRange(start: startTime, end: endTime),
    );

    if (kDebugMode) {
      print('--- REAL-TIME FETCH DEBUG ---');
      print('Platform: ${Platform.operatingSystem}');
      print('Timestamp: ${DateTime.now()}');
      print('Newly Fetched Chunk: $newlyFetchedChunk');
    }

    if (mounted && newlyFetchedChunk != null && newlyFetchedChunk.isNotEmpty) {
      Map<String, List<Map<String, dynamic>>> workingDataCopy = Map.from(
        _data ?? {},
      );
      bool newUniqueDataWasAddedThisPollOverall = false;

      newlyFetchedChunk.forEach((dataTypeKey, incomingSampleList) {
        workingDataCopy[dataTypeKey] ??= [];

        final Set<String> existingUUIDs = workingDataCopy[dataTypeKey]!
            .map((sample) {
          String uuid;
          if (Platform.isIOS) {
            uuid = sample['uuid'] as String? ?? '';
          } else {
            final metadataRaw = sample['metadata'];
            final Map<String, dynamic>? metadata = metadataRaw == null
                ? null
                : Map<String, dynamic>.from(metadataRaw as Map);
            uuid = metadata?['id'] as String? ?? '';
          }
          return uuid;
        })
            .where((uuid) => uuid.isNotEmpty)
            .toSet();

        int addedThisKeyCount = 0;
        for (final newSample in incomingSampleList) {
          String newSampleUUID;
          if (Platform.isIOS) {
            newSampleUUID = newSample['uuid'] as String? ?? '';
          } else {
            final metadataRaw = newSample['metadata'];
            final Map<String, dynamic>? metadata = metadataRaw == null
                ? null
                : Map<String, dynamic>.from(metadataRaw as Map);
            newSampleUUID = metadata?['id'] as String? ?? '';
          }

          bool isNewAndValid =
              newSampleUUID.isNotEmpty && !existingUUIDs.contains(newSampleUUID);

          if (isNewAndValid) {
            workingDataCopy[dataTypeKey]!.add(newSample);
            addedThisKeyCount++;
          }
        }

        if (addedThisKeyCount > 0) {
          newUniqueDataWasAddedThisPollOverall = true;
        }
      });

      if (newUniqueDataWasAddedThisPollOverall) {
        setState(() {
          _data = workingDataCopy;
          final bool hasAnyDataNow = _data!.values.any((list) => list.isNotEmpty);

          if (hasAnyDataNow) {
            _dataAvailable = true;
            if (Platform.isAndroid) {
              recordCountResult = hcRecordCounter.calculateRecordCount(_data!);
              conversionValidityResult =
                  hcConversionValidator.performConversionValidation(_data!);
            } else {
              recordCountResult = hkRecordCounter.calculateRecordCount(_data!);
              conversionValidityResult =
                  hkConversionValidator.performConversionValidation(_data!);
            }
          } else {
            _dataAvailable = false;
            recordCountResult = null;
            conversionValidityResult = null;
          }
          performanceTestResult = null;
        });
      }
    } else if (mounted) {
      bool shouldClearDisplayedData =
          _data != null && _data!.values.any((list) => list.isNotEmpty);

      if (shouldClearDisplayedData &&
          (_dataAvailable || recordCountResult != null)) {
        setState(() {
          _data = {};
          _dataAvailable = false;
          recordCountResult = null;
          conversionValidityResult = null;
          performanceTestResult = null;
        });
      }
    }
  }

  void _stopRealTimeSession() {
    _realTimeTimer?.cancel();
    _realTimeTimer = null;
    if (mounted) {
      setState(() {
        _isRealTimeSessionRunning = false;
      });
    }
  }

  Future<void> _exportDataToFile() async {
    bool canExportHistorical =
        _currentMode == ExperimentMode.historical &&
        recordCountResult != null &&
        conversionValidityResult != null &&
        performanceTestResult != null;
    bool canExportRealTime =
        _currentMode == ExperimentMode.realTime &&
        _dataAvailable &&
        recordCountResult != null &&
        conversionValidityResult != null;

    if (!canExportHistorical && !canExportRealTime) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text(
              'No complete data set available to export for the current mode.',
            ),
          ),
        );
      }
      return;
    }

    String exportMessage = "Exporting results...";
    if (_currentMode == ExperimentMode.realTime &&
        performanceTestResult == null &&
        canExportRealTime) {
      exportMessage =
          'Exporting accumulated real-time data (session performance metrics not applicable).';
    }
    if (mounted) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text(exportMessage)));
    }

    try {
      var results = ExperimentationResult(
        amountOfRecords: recordCountResult!.totalAmountOfRecords,
        amountOfHRRecords: recordCountResult!.amountOfHRRecords,
        amountOfValidatedHR:
            conversionValidityResult!.correctlyConvertedHeartRateObjects,
        amountOfHRVRecords: recordCountResult!.amountOfHRVRecords,
        amountOfValidatedHRV:
            conversionValidityResult!
                .correctlyConvertedHeartRateVariabilityObjects,
        totalFetchTimeMs:
            performanceTestResult?.totalExecutionTimeMs ??
            0,
        rawDataFetchTimeMs:
            performanceTestResult?.dataFetchExecutionInMs ??
            0,
        conversionFetchTimeMs:
            performanceTestResult?.conversionExecutionInMs ??
            0,
      );
      await resultExporter.createAndShareResults(results, context);
    } catch (e) {
      if (kDebugMode) {
        print("Error exporting data: $e");
      }
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text('Error exporting results: $e')));
      }
    }
  }

  Future<void> _exportDataToFileWithFeedback() async {
    await _exportDataToFile();
  }

  @override
  Widget build(BuildContext context) {
    final List<Map<String, dynamic>> pageDestinations = [
      {'title': 'Data Retrieval', 'icon': Icons.storage_rounded},
      {'title': 'Conversion Metrics', 'icon': Icons.transform_rounded},
      {'title': 'Performance Metrics', 'icon': Icons.speed_rounded},
      {'title': 'Inspect Data', 'icon': Icons.table_chart_outlined},
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Health Plugin Experiment'),
        actions: [
          IconButton(
            icon: Icon(
              _showExperimentControls ? Icons.expand_less : Icons.expand_more,
            ),
            tooltip: _showExperimentControls ? 'Hide Setup' : 'Show Setup',
            onPressed: () {
              setState(() {
                _showExperimentControls = !_showExperimentControls;
              });
            },
          ),
        ],
      ),
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: <Widget>[
            DrawerHeader(
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primaryContainer,
              ),
              child: Text(
                'Experiment Modules',
                style: TextStyle(
                  fontSize: 24,
                  color: Theme.of(context).colorScheme.onPrimaryContainer,
                ),
              ),
            ),
            for (int i = 0; i < pageDestinations.length; i++)
              ListTile(
                leading: Icon(
                  pageDestinations[i]['icon'] as IconData,
                  color:
                      _selectedPageIndex == i
                          ? Theme.of(context).colorScheme.primary
                          : (_dataAvailable
                              ? Theme.of(
                                context,
                              ).textTheme.bodyLarge?.color?.withOpacity(0.8)
                              : Colors.grey[600]),
                ),
                title: Text(
                  pageDestinations[i]['title'] as String,
                  style: TextStyle(
                    fontWeight:
                        _selectedPageIndex == i
                            ? FontWeight.bold
                            : FontWeight.normal,
                    color:
                        _selectedPageIndex == i
                            ? Theme.of(context).colorScheme.primary
                            : (_dataAvailable
                                ? Theme.of(context).textTheme.bodyLarge?.color
                                : Colors.grey[700]),
                  ),
                ),
                selected: _selectedPageIndex == i,
                selectedTileColor: Theme.of(
                  context,
                ).colorScheme.primary.withOpacity(0.1),
                onTap: () {
                  if (mounted) {
                    setState(() {
                      _selectedPageIndex = i;
                    });
                  }
                  Navigator.pop(context);
                },
              ),
          ],
        ),
      ),
      body: Column(
        children: [
          Expanded(child: _buildCurrentPageWidget()),
          AnimatedCrossFade(
            crossFadeState:
                _showExperimentControls
                    ? CrossFadeState.showFirst
                    : CrossFadeState.showSecond,
            duration: const Duration(milliseconds: 300),
            firstChild: Padding(
              padding: const EdgeInsets.symmetric(
                horizontal: 16.0,
                vertical: 12.0,
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const SizedBox(height: 8),
                  Padding(
                    padding: const EdgeInsets.symmetric(vertical: 12.0),
                    child: SegmentedButton<ExperimentMode>(
                      segments: const <ButtonSegment<ExperimentMode>>[
                        ButtonSegment<ExperimentMode>(
                          value: ExperimentMode.historical,
                          label: Text('Historical'),
                          icon: Icon(Icons.history),
                        ),
                        ButtonSegment<ExperimentMode>(
                          value: ExperimentMode.realTime,
                          label: Text('Real-time'),
                          icon: Icon(Icons.timer_sharp),
                        ),
                      ],
                      selected: <ExperimentMode>{_currentMode},
                      onSelectionChanged: (Set<ExperimentMode> newSelection) {
                        setState(() {
                          _currentMode = newSelection.first;
                          if (_currentMode == ExperimentMode.historical &&
                              _isRealTimeSessionRunning) {
                            _stopRealTimeSession();
                          }
                          _data = null;
                          _dataAvailable = false;
                          recordCountResult = null;
                          conversionValidityResult = null;
                          performanceTestResult = null;
                          _isLoading = false;
                        });
                      },
                      style: SegmentedButton.styleFrom(
                        selectedBackgroundColor: Theme.of(
                          context,
                        ).colorScheme.primary.withOpacity(0.2),
                        selectedForegroundColor:
                            Theme.of(context).colorScheme.primary,
                      ),
                    ),
                  ),
                  if (_currentMode == ExperimentMode.historical)
                    Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Row(
                          children: [
                            Expanded(
                              child: _buildDateSelector(
                                context,
                                'Start Date',
                                _startDate,
                                () => _selectDate(context, true),
                              ),
                            ),
                            const SizedBox(width: 16),
                            Expanded(
                              child: _buildDateSelector(
                                context,
                                'End Date',
                                _endDate,
                                () => _selectDate(context, false),
                              ),
                            ),
                          ],
                        ),
                        const SizedBox(height: 16.0),
                      ],
                    ),
                  SizedBox(
                    width: double.infinity,
                    height: 50,
                    child: ElevatedButton.icon(
                      icon: Icon(
                        _isLoading
                            ? Icons.hourglass_empty_rounded
                            : _currentMode == ExperimentMode.historical
                            ? Icons.play_arrow_rounded
                            : _isRealTimeSessionRunning
                            ? Icons.stop_rounded
                            : Icons.play_circle_filled_rounded,
                      ),
                      onPressed: _isLoading ? null : _handlePrimaryButtonAction,
                      style: ElevatedButton.styleFrom(
                        backgroundColor:
                            _isRealTimeSessionRunning &&
                                    _currentMode == ExperimentMode.realTime
                                ? Colors.redAccent.shade200
                                : Theme.of(context).colorScheme.primary,
                        foregroundColor: Colors.white,
                        textStyle: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.w500,
                        ),
                      ),
                      label: Text(
                        _isLoading
                            ? 'Processing...'
                            : _currentMode == ExperimentMode.historical
                            ? 'Fetch Historical Data'
                            : _isRealTimeSessionRunning
                            ? 'Stop Real-time Session'
                            : 'Start Real-time Session',
                      ),
                    ),
                  ),
                  const SizedBox(height: 12),
                  SizedBox(
                    width: double.infinity,
                    height: 40,
                    child: OutlinedButton.icon(
                      onPressed:
                          (_dataAvailable &&
                                  recordCountResult != null &&
                                  conversionValidityResult != null)
                              ? _exportDataToFileWithFeedback
                              : null,
                      icon: const Icon(Icons.file_download_outlined),
                      label: const Text('Export Results'),
                      style: OutlinedButton.styleFrom(
                        foregroundColor: Theme.of(context).colorScheme.primary,
                        disabledForegroundColor: Colors.grey.shade400,
                        side: BorderSide(
                          color:
                              (_dataAvailable &&
                                      recordCountResult != null &&
                                      conversionValidityResult != null)
                                  ? Theme.of(context).colorScheme.primary
                                  : Colors.grey.shade300,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            secondChild: const SizedBox.shrink(),
          ),
        ],
      ),
    );
  }

  Widget _buildDateSelector(
    BuildContext context,
    String label,
    DateTime date,
    VoidCallback onTap,
  ) {
    final formattedDate = DateFormat('MMM dd, yy HH:mm').format(date);
    bool dateSelectorEnabled =
        _currentMode == ExperimentMode.historical &&
        !_isRealTimeSessionRunning &&
        !_isLoading;

    return InkWell(
      onTap: dateSelectorEnabled ? onTap : null,
      child: Opacity(
        opacity: dateSelectorEnabled ? 1.0 : 0.5,
        child: Container(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
          decoration: BoxDecoration(
            color: Theme.of(context).cardColor,
            border: Border.all(color: Colors.grey.shade300),
            borderRadius: BorderRadius.circular(8),
            boxShadow: [
              BoxShadow(
                color: Colors.black.withOpacity(0.05),
                spreadRadius: 1,
                blurRadius: 3,
                offset: const Offset(0, 1),
              ),
            ],
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                label,
                style: TextStyle(fontSize: 11, color: Colors.grey.shade700),
              ),
              const SizedBox(height: 4),
              Row(
                children: [
                  Icon(
                    Icons.calendar_month_outlined,
                    size: 18,
                    color:
                        dateSelectorEnabled
                            ? Theme.of(context).colorScheme.primary
                            : Colors.grey,
                  ),
                  const SizedBox(width: 6),
                  Expanded(
                    child: Text(
                      formattedDate,
                      style: TextStyle(
                        fontSize: 13,
                        fontWeight: FontWeight.w500,
                        color:
                            dateSelectorEnabled
                                ? Theme.of(context).textTheme.bodyLarge?.color
                                : Colors.grey,
                      ),
                      overflow: TextOverflow.ellipsis,
                      maxLines: 1,
                      softWrap: false,
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}