app/lib/screens/lifestyle_screen.dart
John Mizerek 247388c61a Add Skip Forever, app icon, About screen links, release signing
- Add Skip/Skip Forever links to onboarding screens only
- Remove Skip links from questionnaire screens (baseline, behavioral, lifestyle)
- Add app icon (1024x1024) and generate all platform sizes
- Update About screen with correct URLs (privacy, terms, disclaimer, how it works)
- Add "No ads. Ever." section to About screen
- Configure Android release signing with upload keystore
- Add URL intent query for Android 11+ link launching
- Add skipForever preference to LocalStorage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-21 13:21:17 -08:00

365 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,
),
),
);
}
}
}