Features: - Save calculation runs with custom labels for later retrieval - View saved runs list with dominant factor summary - View detailed results of any saved run - View original inputs in read-only mode - Use any saved run as starting point for new calculation - Compare two saved runs side-by-side - About screen with app info, help links, privacy policy - Metric/Imperial unit toggle persisted across sessions - Info icon in app bar on all screens Technical: - New SavedRun model with full serialization - SQLite saved_runs table with CRUD operations - readOnly mode for all input screens - Unit preference stored in LocalStorage - Database schema upgraded to v2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
364 lines
11 KiB
Dart
364 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../models/models.dart';
|
|
import '../storage/local_storage.dart';
|
|
import '../theme.dart';
|
|
import 'about_screen.dart';
|
|
import 'results_screen.dart';
|
|
|
|
class LifestyleScreen extends StatefulWidget {
|
|
final UserProfile profile;
|
|
final SmokingStatus smoking;
|
|
final int cigarettesPerDay;
|
|
final AlcoholLevel alcohol;
|
|
final double sleepHours;
|
|
final bool sleepConsistent;
|
|
final ActivityLevel activity;
|
|
final bool readOnly;
|
|
final BehavioralInputs? initialBehaviors;
|
|
|
|
const LifestyleScreen({
|
|
super.key,
|
|
required this.profile,
|
|
required this.smoking,
|
|
required this.cigarettesPerDay,
|
|
required this.alcohol,
|
|
required this.sleepHours,
|
|
required this.sleepConsistent,
|
|
required this.activity,
|
|
this.readOnly = false,
|
|
this.initialBehaviors,
|
|
});
|
|
|
|
@override
|
|
State<LifestyleScreen> createState() => _LifestyleScreenState();
|
|
}
|
|
|
|
class _LifestyleScreenState extends State<LifestyleScreen> {
|
|
DietQuality _diet = DietQuality.fair;
|
|
ProcessedFoodLevel _processedFood = ProcessedFoodLevel.frequent;
|
|
DrugUse _drugUse = DrugUse.none;
|
|
SocialConnection _social = SocialConnection.moderate;
|
|
StressLevel _stress = StressLevel.moderate;
|
|
DrivingExposure _driving = DrivingExposure.low;
|
|
WorkHoursLevel _workHours = WorkHoursLevel.normal;
|
|
bool _useMetric = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadInitialData();
|
|
}
|
|
|
|
Future<void> _loadInitialData() async {
|
|
// Load unit preference
|
|
final useMetric = await LocalStorage.getUseMetricUnits();
|
|
setState(() => _useMetric = useMetric);
|
|
|
|
// If initial behaviors provided, use those
|
|
if (widget.initialBehaviors != null) {
|
|
_applyBehaviors(widget.initialBehaviors!);
|
|
return;
|
|
}
|
|
|
|
// Otherwise load from storage
|
|
final behaviors = await LocalStorage.getBehaviors();
|
|
if (behaviors != null) {
|
|
_applyBehaviors(behaviors);
|
|
}
|
|
}
|
|
|
|
void _applyBehaviors(BehavioralInputs behaviors) {
|
|
setState(() {
|
|
_diet = behaviors.diet;
|
|
_processedFood = behaviors.processedFood;
|
|
_drugUse = behaviors.drugUse;
|
|
_social = behaviors.social;
|
|
_stress = behaviors.stress;
|
|
_driving = behaviors.driving;
|
|
_workHours = behaviors.workHours;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.readOnly ? 'Lifestyle (View Only)' : 'Lifestyle'),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.info_outline),
|
|
onPressed: () => Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const AboutScreen()),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Lifestyle Factors',
|
|
style: Theme.of(context).textTheme.headlineSmall,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Diet, social life, stress, and daily exposures.',
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
const SizedBox(height: 32),
|
|
|
|
// Diet Quality
|
|
_buildSectionLabel('Diet Quality'),
|
|
_buildSectionHint('Vegetables, whole foods, variety'),
|
|
const SizedBox(height: 12),
|
|
_buildDietSelector(),
|
|
const SizedBox(height: 28),
|
|
|
|
// Processed Food
|
|
_buildSectionLabel('Processed Food'),
|
|
_buildSectionHint('Fast food, packaged snacks, sugary drinks'),
|
|
const SizedBox(height: 12),
|
|
_buildProcessedFoodSelector(),
|
|
const SizedBox(height: 28),
|
|
|
|
// Drug Use
|
|
_buildSectionLabel('Recreational Drugs'),
|
|
_buildSectionHint('Cannabis, stimulants, other substances'),
|
|
const SizedBox(height: 12),
|
|
_buildDrugUseSelector(),
|
|
const SizedBox(height: 28),
|
|
|
|
// Social Connection
|
|
_buildSectionLabel('Social Connection'),
|
|
_buildSectionHint('Time with friends, family, community'),
|
|
const SizedBox(height: 12),
|
|
_buildSocialSelector(),
|
|
const SizedBox(height: 28),
|
|
|
|
// Stress Level
|
|
_buildSectionLabel('Stress Level'),
|
|
_buildSectionHint('Work pressure, anxiety, life demands'),
|
|
const SizedBox(height: 12),
|
|
_buildStressSelector(),
|
|
const SizedBox(height: 28),
|
|
|
|
// Driving Exposure
|
|
_buildSectionLabel(_useMetric ? 'Driving (km/week)' : 'Driving (mi/week)'),
|
|
const SizedBox(height: 12),
|
|
_buildDrivingSelector(),
|
|
const SizedBox(height: 28),
|
|
|
|
// Work Hours
|
|
_buildSectionLabel('Work Hours'),
|
|
const SizedBox(height: 12),
|
|
_buildWorkHoursSelector(),
|
|
const SizedBox(height: 40),
|
|
|
|
// Calculate button (hidden in readOnly mode)
|
|
if (!widget.readOnly)
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: _calculate,
|
|
child: const Text('Calculate'),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionLabel(String label) {
|
|
return Text(
|
|
label,
|
|
style: Theme.of(context).textTheme.labelLarge,
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionHint(String hint) {
|
|
return Text(
|
|
hint,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
);
|
|
}
|
|
|
|
Widget _buildDietSelector() {
|
|
return _buildSegmentedControl<DietQuality>(
|
|
value: _diet,
|
|
options: [
|
|
(DietQuality.poor, 'Poor'),
|
|
(DietQuality.fair, 'Fair'),
|
|
(DietQuality.good, 'Good'),
|
|
(DietQuality.excellent, 'Excellent'),
|
|
],
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _diet = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildProcessedFoodSelector() {
|
|
return _buildSegmentedControl<ProcessedFoodLevel>(
|
|
value: _processedFood,
|
|
options: [
|
|
(ProcessedFoodLevel.daily, 'Daily'),
|
|
(ProcessedFoodLevel.frequent, 'Often'),
|
|
(ProcessedFoodLevel.occasional, 'Sometimes'),
|
|
(ProcessedFoodLevel.rarely, 'Rarely'),
|
|
],
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _processedFood = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildDrugUseSelector() {
|
|
return _buildSegmentedControl<DrugUse>(
|
|
value: _drugUse,
|
|
options: [
|
|
(DrugUse.none, 'None'),
|
|
(DrugUse.occasional, 'Occasional'),
|
|
(DrugUse.regular, 'Regular'),
|
|
(DrugUse.daily, 'Daily'),
|
|
],
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _drugUse = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildSocialSelector() {
|
|
return _buildSegmentedControl<SocialConnection>(
|
|
value: _social,
|
|
options: [
|
|
(SocialConnection.isolated, 'Isolated'),
|
|
(SocialConnection.limited, 'Limited'),
|
|
(SocialConnection.moderate, 'Moderate'),
|
|
(SocialConnection.strong, 'Strong'),
|
|
],
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _social = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildStressSelector() {
|
|
return _buildSegmentedControl<StressLevel>(
|
|
value: _stress,
|
|
options: [
|
|
(StressLevel.low, 'Low'),
|
|
(StressLevel.moderate, 'Moderate'),
|
|
(StressLevel.high, 'High'),
|
|
(StressLevel.chronic, 'Chronic'),
|
|
],
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _stress = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildDrivingSelector() {
|
|
// Show metric (km) or imperial (mi) based on preference
|
|
final options = _useMetric
|
|
? [
|
|
(DrivingExposure.low, '<80 km'),
|
|
(DrivingExposure.moderate, '80-240'),
|
|
(DrivingExposure.high, '240-480'),
|
|
(DrivingExposure.veryHigh, '480+'),
|
|
]
|
|
: [
|
|
(DrivingExposure.low, '<50 mi'),
|
|
(DrivingExposure.moderate, '50-150'),
|
|
(DrivingExposure.high, '150-300'),
|
|
(DrivingExposure.veryHigh, '300+'),
|
|
];
|
|
|
|
return _buildSegmentedControl<DrivingExposure>(
|
|
value: _driving,
|
|
options: options,
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _driving = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildWorkHoursSelector() {
|
|
return _buildSegmentedControl<WorkHoursLevel>(
|
|
value: _workHours,
|
|
options: [
|
|
(WorkHoursLevel.normal, '<40'),
|
|
(WorkHoursLevel.elevated, '40-55'),
|
|
(WorkHoursLevel.high, '55-70'),
|
|
(WorkHoursLevel.extreme, '70+'),
|
|
],
|
|
onChanged: widget.readOnly ? null : (value) => setState(() => _workHours = value),
|
|
);
|
|
}
|
|
|
|
Widget _buildSegmentedControl<T>({
|
|
required T value,
|
|
required List<(T, String)> options,
|
|
required ValueChanged<T>? onChanged,
|
|
}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surfaceVariant,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
children: options.map((option) {
|
|
final isSelected = value == option.$1;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: onChanged == null ? null : () => onChanged(option.$1),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? AppColors.primary : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
option.$2,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: isSelected ? Colors.white : AppColors.textSecondary,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _calculate() async {
|
|
final behaviors = BehavioralInputs(
|
|
smoking: widget.smoking,
|
|
cigarettesPerDay: widget.cigarettesPerDay,
|
|
alcohol: widget.alcohol,
|
|
sleepHours: widget.sleepHours,
|
|
sleepConsistent: widget.sleepConsistent,
|
|
activity: widget.activity,
|
|
diet: _diet,
|
|
processedFood: _processedFood,
|
|
drugUse: _drugUse,
|
|
social: _social,
|
|
stress: _stress,
|
|
driving: _driving,
|
|
workHours: _workHours,
|
|
);
|
|
|
|
await LocalStorage.saveBehaviors(behaviors);
|
|
|
|
if (mounted) {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (_) => ResultsScreen(
|
|
profile: widget.profile,
|
|
behaviors: behaviors,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|