From ec923c906e2cd812acbe227057e0235b5fd53a7a Mon Sep 17 00:00:00 2001 From: Sterlen Date: Tue, 30 Dec 2025 23:20:50 -0600 Subject: [PATCH] New --- README.md | 6 +- lib/main.dart | 50 +++- lib/models/user_profile.dart | 59 +++++ lib/models/user_profile.g.dart | 73 +++++- lib/providers/user_provider.dart | 31 ++- lib/screens/home/home_screen.dart | 104 +++++++- lib/screens/husband/_HusbandLearnScreen.dart | 165 ++++++++++++ lib/screens/learn/husband_learn_screen.dart | 204 +++++++++++++++ lib/screens/learn/wife_learn_screen.dart | 201 +++++++++++++++ lib/screens/settings/appearance_screen.dart | 162 ++++++++++++ .../settings/cycle_history_screen.dart | 240 ++++++++++++++++++ .../settings/cycle_settings_screen.dart | 143 +++++++++++ lib/screens/settings/export_data_screen.dart | 70 +++++ .../settings/privacy_settings_screen.dart | 141 ++++++++++ .../settings/sharing_settings_screen.dart | 87 +++++++ lib/services/health_service.dart | 75 ++++++ lib/services/ics_service.dart | 69 +++++ lib/services/pdf_service.dart | 159 ++++++++++++ lib/theme/app_theme.dart | 46 ++-- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 182 ++++++++++++- pubspec.yaml | 7 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 26 files changed, 2234 insertions(+), 53 deletions(-) create mode 100644 lib/screens/husband/_HusbandLearnScreen.dart create mode 100644 lib/screens/learn/husband_learn_screen.dart create mode 100644 lib/screens/learn/wife_learn_screen.dart create mode 100644 lib/screens/settings/appearance_screen.dart create mode 100644 lib/screens/settings/cycle_history_screen.dart create mode 100644 lib/screens/settings/cycle_settings_screen.dart create mode 100644 lib/screens/settings/export_data_screen.dart create mode 100644 lib/screens/settings/privacy_settings_screen.dart create mode 100644 lib/screens/settings/sharing_settings_screen.dart create mode 100644 lib/services/health_service.dart create mode 100644 lib/services/ics_service.dart create mode 100644 lib/services/pdf_service.dart diff --git a/README.md b/README.md index 3f40f4e..ef4f8b1 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ dart run build_runner build --delete-conflicting-outputs # Run the app flutter run -``` +```bash ### Platforms @@ -66,7 +66,7 @@ flutter run ## Project Structure -``` +```bash lib/ ├── main.dart # App entry point ├── theme/ @@ -88,7 +88,7 @@ lib/ ├── scripture_card.dart # Phase-colored verse display ├── quick_log_buttons.dart # Quick action buttons └── tip_card.dart # Contextual tips -``` +```bash ## Color Palettes diff --git a/lib/main.dart b/lib/main.dart index 9b91fcd..e7bdcfe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'models/scripture.dart'; import 'theme/app_theme.dart'; import 'screens/splash_screen.dart'; import 'models/user_profile.dart'; import 'models/cycle_entry.dart'; +import 'providers/user_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -26,7 +26,8 @@ void main() async { Hive.registerAdapter(CyclePhaseAdapter()); Hive.registerAdapter(UserRoleAdapter()); Hive.registerAdapter(BibleTranslationAdapter()); - Hive.registerAdapter(ScriptureAdapter()); // Register Scripture adapter + Hive.registerAdapter(ScriptureAdapter()); + Hive.registerAdapter(AppThemeModeAdapter()); // Register new adapter // Open boxes and load scriptures in parallel await Future.wait([ @@ -38,17 +39,52 @@ void main() async { runApp(const ProviderScope(child: ChristianPeriodTrackerApp())); } -class ChristianPeriodTrackerApp extends StatelessWidget { +// Helper to convert hex string to Color +Color _colorFromHex(String hexColor) { + try { + final hexCode = hexColor.replaceAll('0x', ''); + return Color(int.parse('FF$hexCode', radix: 16)); + } catch (e) { + return AppColors.sageGreen; // Fallback to default + } +} + +class ChristianPeriodTrackerApp extends ConsumerWidget { const ChristianPeriodTrackerApp({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final userProfile = ref.watch(userProfileProvider); + + final ThemeMode themeMode; + final Color accentColor; + + if (userProfile != null) { + accentColor = _colorFromHex(userProfile.accentColor); + switch (userProfile.themeMode) { + case AppThemeMode.light: + themeMode = ThemeMode.light; + break; + case AppThemeMode.dark: + themeMode = ThemeMode.dark; + break; + case AppThemeMode.system: + default: + themeMode = ThemeMode.system; + break; + } + } else { + // Default theme for initial load or if profile is null + themeMode = ThemeMode.system; + accentColor = AppColors.sageGreen; + } + return MaterialApp( title: 'Christian Period Tracker', debugShowCheckedModeBanner: false, - theme: AppTheme.lightTheme, - darkTheme: AppTheme.darkTheme, - themeMode: ThemeMode.system, + theme: AppTheme.getLightTheme(accentColor), + darkTheme: AppTheme.getDarkTheme(accentColor), + themeMode: themeMode, home: const SplashScreen(), ); } diff --git a/lib/models/user_profile.dart b/lib/models/user_profile.dart index af8ce55..3efa504 100644 --- a/lib/models/user_profile.dart +++ b/lib/models/user_profile.dart @@ -46,6 +46,16 @@ enum BibleTranslation { msg, } +@HiveType(typeId: 11) +enum AppThemeMode { + @HiveField(0) + system, + @HiveField(1) + light, + @HiveField(2) + dark, +} + /// User profile model @HiveType(typeId: 2) class UserProfile extends HiveObject { @@ -103,6 +113,31 @@ class UserProfile extends HiveObject { @HiveField(18, defaultValue: false) bool isDataShared; + @HiveField(19, defaultValue: AppThemeMode.system) + AppThemeMode themeMode; + + @HiveField(20, defaultValue: '0xFFA8C5A8') + String accentColor; + + // Sharing settings + @HiveField(21, defaultValue: true) + bool shareMoods; + + @HiveField(22, defaultValue: true) + bool shareSymptoms; + + @HiveField(23, defaultValue: true) + bool shareCravings; + + @HiveField(24, defaultValue: true) + bool shareEnergyLevels; + + @HiveField(25, defaultValue: true) + bool shareSleep; + + @HiveField(26, defaultValue: true) + bool shareIntimacy; + UserProfile({ required this.id, required this.name, @@ -122,6 +157,14 @@ class UserProfile extends HiveObject { this.bibleTranslation = BibleTranslation.esv, this.favoriteFoods, this.isDataShared = false, + this.themeMode = AppThemeMode.system, + this.accentColor = '0xFFA8C5A8', + this.shareMoods = true, + this.shareSymptoms = true, + this.shareCravings = true, + this.shareEnergyLevels = true, + this.shareSleep = true, + this.shareIntimacy = true, }); /// Check if user is married @@ -166,6 +209,14 @@ class UserProfile extends HiveObject { BibleTranslation? bibleTranslation, List? favoriteFoods, bool? isDataShared, + AppThemeMode? themeMode, + String? accentColor, + bool? shareMoods, + bool? shareSymptoms, + bool? shareCravings, + bool? shareEnergyLevels, + bool? shareSleep, + bool? shareIntimacy, }) { return UserProfile( id: id ?? this.id, @@ -187,6 +238,14 @@ class UserProfile extends HiveObject { bibleTranslation: bibleTranslation ?? this.bibleTranslation, favoriteFoods: favoriteFoods ?? this.favoriteFoods, isDataShared: isDataShared ?? this.isDataShared, + themeMode: themeMode ?? this.themeMode, + accentColor: accentColor ?? this.accentColor, + shareMoods: shareMoods ?? this.shareMoods, + shareSymptoms: shareSymptoms ?? this.shareSymptoms, + shareCravings: shareCravings ?? this.shareCravings, + shareEnergyLevels: shareEnergyLevels ?? this.shareEnergyLevels, + shareSleep: shareSleep ?? this.shareSleep, + shareIntimacy: shareIntimacy ?? this.shareIntimacy, ); } } diff --git a/lib/models/user_profile.g.dart b/lib/models/user_profile.g.dart index 5dd9c70..2f1e1c7 100644 --- a/lib/models/user_profile.g.dart +++ b/lib/models/user_profile.g.dart @@ -37,13 +37,22 @@ class UserProfileAdapter extends TypeAdapter { : fields[16] as BibleTranslation, favoriteFoods: (fields[17] as List?)?.cast(), isDataShared: fields[18] == null ? false : fields[18] as bool, + themeMode: + fields[19] == null ? AppThemeMode.system : fields[19] as AppThemeMode, + accentColor: fields[20] == null ? '0xFFA8C5A8' : fields[20] as String, + shareMoods: fields[21] == null ? true : fields[21] as bool, + shareSymptoms: fields[22] == null ? true : fields[22] as bool, + shareCravings: fields[23] == null ? true : fields[23] as bool, + shareEnergyLevels: fields[24] == null ? true : fields[24] as bool, + shareSleep: fields[25] == null ? true : fields[25] as bool, + shareIntimacy: fields[26] == null ? true : fields[26] as bool, ); } @override void write(BinaryWriter writer, UserProfile obj) { writer - ..writeByte(18) + ..writeByte(26) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -79,7 +88,23 @@ class UserProfileAdapter extends TypeAdapter { ..writeByte(17) ..write(obj.favoriteFoods) ..writeByte(18) - ..write(obj.isDataShared); + ..write(obj.isDataShared) + ..writeByte(19) + ..write(obj.themeMode) + ..writeByte(20) + ..write(obj.accentColor) + ..writeByte(21) + ..write(obj.shareMoods) + ..writeByte(22) + ..write(obj.shareSymptoms) + ..writeByte(23) + ..write(obj.shareCravings) + ..writeByte(24) + ..write(obj.shareEnergyLevels) + ..writeByte(25) + ..write(obj.shareSleep) + ..writeByte(26) + ..write(obj.shareIntimacy); } @override @@ -245,6 +270,50 @@ class BibleTranslationAdapter extends TypeAdapter { typeId == other.typeId; } +class AppThemeModeAdapter extends TypeAdapter { + @override + final int typeId = 11; + + @override + AppThemeMode read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return AppThemeMode.system; + case 1: + return AppThemeMode.light; + case 2: + return AppThemeMode.dark; + default: + return AppThemeMode.system; + } + } + + @override + void write(BinaryWriter writer, AppThemeMode obj) { + switch (obj) { + case AppThemeMode.system: + writer.writeByte(0); + break; + case AppThemeMode.light: + writer.writeByte(1); + break; + case AppThemeMode.dark: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AppThemeModeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + class UserRoleAdapter extends TypeAdapter { @override final int typeId = 8; diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 150455b..c7cfe3d 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -26,12 +26,29 @@ class UserProfileNotifier extends StateNotifier { state = profile; } + Future updateThemeMode(AppThemeMode themeMode) async { + if (state != null) { + await updateProfile(state!.copyWith(themeMode: themeMode)); + } + } + + Future updateAccentColor(String accentColor) async { + if (state != null) { + await updateProfile(state!.copyWith(accentColor: accentColor)); + } + } + + Future updateRelationshipStatus(RelationshipStatus relationshipStatus) async { + if (state != null) { + await updateProfile(state!.copyWith(relationshipStatus: relationshipStatus)); + } + } + Future clearProfile() async { final box = Hive.box('user_profile'); await box.clear(); state = null; } -} /// Provider for cycle entries final cycleEntriesProvider = StateNotifierProvider>((ref) { @@ -67,6 +84,18 @@ class CycleEntriesNotifier extends StateNotifier> { _loadEntries(); } + Future deleteEntriesForMonth(int year, int month) async { + final box = Hive.box('cycle_entries'); + final keysToDelete = []; + for (var entry in box.values) { + if (entry.date.year == year && entry.date.month == month) { + keysToDelete.add(entry.key); + } + } + await box.deleteAll(keysToDelete); + _loadEntries(); + } + Future clearEntries() async { final box = Hive.box('cycle_entries'); await box.clear(); diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index ef792c3..5f01530 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -8,6 +8,8 @@ import '../../models/scripture.dart'; import '../calendar/calendar_screen.dart'; import '../log/log_screen.dart'; import '../devotional/devotional_screen.dart'; +import '../settings/appearance_screen.dart'; +import '../settings/cycle_settings_screen.dart'; import '../../widgets/tip_card.dart'; import '../../widgets/cycle_ring.dart'; import '../../widgets/scripture_card.dart'; @@ -203,7 +205,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { icon: const Icon(Icons.shuffle), label: const Text('Random Verse'), style: TextButton.styleFrom( - foregroundColor: AppColors.sageGreen, + foregroundColor: Theme.of(context).colorScheme.primary, ), ), ), @@ -219,7 +221,7 @@ class _DashboardTabState extends ConsumerState<_DashboardTab> { const QuickLogButtons(), const SizedBox(height: 24), if (role == UserRole.wife) - TipCard(phase: phase, isMarried: isMarried), + _buildWifeTipsSection(context), const SizedBox(height: 20), ], ), @@ -383,8 +385,8 @@ class _SettingsTab extends ConsumerWidget { decoration: BoxDecoration( gradient: LinearGradient( colors: [ - AppColors.blushPink, - AppColors.rose.withOpacity(0.7) + Theme.of(context).colorScheme.primary.withOpacity(0.7), + Theme.of(context).colorScheme.secondary.withOpacity(0.7) ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -440,7 +442,13 @@ class _SettingsTab extends ConsumerWidget { 'Bible Version ($translationLabel)', onTap: () => BibleUtils.showTranslationPicker(context, ref), ), - _buildSettingsTile(context, Icons.palette_outlined, 'Appearance'), + _buildSettingsTile(context, Icons.palette_outlined, 'Appearance', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AppearanceScreen())); + }), _buildSettingsTile( context, Icons.favorite_border, @@ -452,15 +460,32 @@ class _SettingsTab extends ConsumerWidget { context, Icons.share_outlined, 'Share with Husband', - onTap: () => _showShareDialog(context, ref), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SharingSettingsScreen())); + }, ), ]), const SizedBox(height: 16), _buildSettingsGroup(context, 'Cycle', [ _buildSettingsTile( - context, Icons.calendar_today_outlined, 'Cycle Settings'), + context, Icons.calendar_today_outlined, 'Cycle Settings', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CycleSettingsScreen())); + }), _buildSettingsTile( - context, Icons.trending_up_outlined, 'Cycle History'), + context, Icons.trending_up_outlined, 'Cycle History', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CycleHistoryScreen())); + }), _buildSettingsTile( context, Icons.download_outlined, 'Export Data'), ]), @@ -549,7 +574,7 @@ class _SettingsTab extends ConsumerWidget { .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); - + final updatedProfile = userProfile.copyWith(favoriteFoods: favorites); ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); Navigator.pop(context); @@ -571,7 +596,7 @@ class _SettingsTab extends ConsumerWidget { builder: (context) => AlertDialog( title: Row( children: [ - const Icon(Icons.share_outlined, color: AppColors.sageGreen), + Icon(Icons.share_outlined, color: Theme.of(context).colorScheme.primary), const SizedBox(width: 8), const Text('Share with Husband'), ], @@ -587,9 +612,9 @@ class _SettingsTab extends ConsumerWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), decoration: BoxDecoration( - color: AppColors.sageGreen.withOpacity(0.1), + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.sageGreen.withOpacity(0.3)), + border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)), ), child: SelectableText( pairingCode, @@ -597,7 +622,7 @@ class _SettingsTab extends ConsumerWidget { fontSize: 32, fontWeight: FontWeight.bold, letterSpacing: 4, - color: AppColors.sageGreen, + color: Theme.of(context).colorScheme.primary, ), ), ), @@ -613,7 +638,7 @@ class _SettingsTab extends ConsumerWidget { ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( - backgroundColor: AppColors.sageGreen, + backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, ), child: const Text('Done'), @@ -623,3 +648,54 @@ class _SettingsTab extends ConsumerWidget { ); } } + +Widget _buildWifeTipsSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Health Tips', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTipCard( + context, + title: 'Regular Check-ups', + content: + 'Schedule regular gynecological check-ups to monitor your reproductive health.', + icon: Icons.medical_services, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Healthy Lifestyle', + content: + 'Maintain a balanced diet, exercise regularly, and get adequate sleep.', + icon: Icons.healing, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Partner Communication', + content: + 'Discuss health concerns openly with your partner to ensure mutual understanding.', + icon: Icons.chat, + ), + ], + ), + ), + ), + ], + ); +} \ No newline at end of file diff --git a/lib/screens/husband/_HusbandLearnScreen.dart b/lib/screens/husband/_HusbandLearnScreen.dart new file mode 100644 index 0000000..fa2a677 --- /dev/null +++ b/lib/screens/husband/_HusbandLearnScreen.dart @@ -0,0 +1,165 @@ +class _HusbandLearnScreen extends StatelessWidget { + const _HusbandLearnScreen(); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Learn', + style: GoogleFonts.outfit( + fontSize: 28, + fontWeight: FontWeight.w600, + color: AppColors.navyBlue, + ), + ), + const SizedBox(height: 24), + _buildSection(context, 'Understanding Her', [ + _LearnItem( + icon: Icons.loop, + title: 'The 4 Phases of Her Cycle', + subtitle: 'What\'s happening in her body each month', + articleId: 'four_phases', + ), + _LearnItem( + icon: Icons.psychology_outlined, + title: 'Why Does Her Mood Change?', + subtitle: 'Hormones explained simply', + articleId: 'mood_changes', + ), + _LearnItem( + icon: Icons.medical_information_outlined, + title: 'PMS is Real', + subtitle: 'Medical facts for supportive husbands', + articleId: 'pms_is_real', + ), + ]), + const SizedBox(height: 24), + _buildSection(context, 'Biblical Manhood', [ + _LearnItem( + icon: Icons.favorite, + title: 'Loving Like Christ', + subtitle: 'Ephesians 5 in daily practice', + articleId: 'loving_like_christ', + ), + _LearnItem( + icon: Icons.handshake, + title: 'Servant Leadership at Home', + subtitle: 'What it really means', + articleId: 'servant_leadership', + ), + _LearnItem( + icon: Icons.auto_awesome, + title: 'Praying for Your Wife', + subtitle: 'Practical guide', + articleId: 'praying_for_wife', + ), + ]), + const SizedBox(height: 24), + _buildSection(context, 'NFP for Husbands', [ + _LearnItem( + icon: Icons.show_chart, + title: 'Reading the Charts Together', + subtitle: 'Understanding fertility signs', + articleId: 'reading_charts', + ), + _LearnItem( + icon: Icons.schedule, + title: 'Abstinence as Spiritual Discipline', + subtitle: 'Growing together during fertile days', + articleId: 'abstinence_discipline', + ), + ]), + ], + ), + ), + ); + } + + Widget _buildSection(BuildContext context, String title, List<_LearnItem> items) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: GoogleFonts.outfit( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.warmGray, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: items + .map((item) => ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.navyBlue.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + item.icon, + color: AppColors.navyBlue, + size: 20, + ), + ), + title: Text( + item.title, + style: GoogleFonts.outfit( + fontSize: 15, + fontWeight: FontWeight.w500, + color: AppColors.charcoal, + ), + ), + subtitle: Text( + item.subtitle, + style: GoogleFonts.outfit( + fontSize: 13, + color: AppColors.warmGray, + ), + ), + trailing: const Icon( + Icons.chevron_right, + color: AppColors.lightGray, + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LearnArticleScreen(articleId: item.articleId), + ), + ); + }, + )) + .toList(), + ), + ), + ], + ); + } + +class _LearnItem { + final IconData icon; + final String title; + final String subtitle; + final String articleId; + + const _LearnItem({ + required this.icon, + required this.title, + required this.subtitle, + required this.articleId, + }); +} \ No newline at end of file diff --git a/lib/screens/learn/husband_learn_screen.dart b/lib/screens/learn/husband_learn_screen.dart new file mode 100644 index 0000000..f1d7c81 --- /dev/null +++ b/lib/screens/learn/husband_learn_screen.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:christian_period_tracker/models/scripture.dart'; + +class HusbandLearnScreen extends StatelessWidget { + const HusbandLearnScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Reproductive Health Education'), + actions: [ + IconButton( + icon: const Icon(Icons.bookmark), + onPressed: () {}, + ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHealthTipsSection(context), + const SizedBox(height: 24), + _buildDiseasePreventionSection(context), + const SizedBox(height: 24), + _buildPracticalAdviceSection(context), + ], + ), + ), + ), + ); + } + + Widget _buildHealthTipsSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Reproductive Health Tips', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTipCard( + context, + title: 'Understanding Female Cycles', + content: 'Learn about the different phases of your wife\'s menstrual cycle and how they affect her health.', + icon: Icons.calendar_month, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Supportive Role', + content: 'Be supportive during different phases, understanding when she may need more emotional support or rest.', + icon: Icons.support_agent, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Nutritional Support', + content: 'Understand how nutrition affects reproductive health and discuss dietary choices together.', + icon: Icons.food_bank, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildDiseasePreventionSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Disease Prevention Between Partners', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTipCard( + context, + title: 'Safe Sexual Practices', + content: 'Use protection consistently to prevent sexually transmitted infections and maintain mutual health.', + icon: Icons.protect, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Regular Testing', + content: 'Schedule regular STI screenings together with your partner for early detection and treatment.', + icon: Icons.medical_information, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Open Communication', + content: 'Discuss health concerns openly to ensure both partners understand each other\'s needs and maintain trust.', + icon: Icons.chat, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildPracticalAdviceSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Practical Advice for Partnership', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTipCard( + context, + title: 'Educate Yourself', + content: 'Take the initiative to learn about reproductive health and partner needs.', + icon: Icons.school, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Active Listening', + content: 'Listen attentively when your wife discusses her health concerns or feelings.', + icon: Icons.mic_none, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildTipCard(BuildContext context, {required String title, required String content, required IconData icon}) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 24, color: Theme.of(context).colorScheme.primary), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + content, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/learn/wife_learn_screen.dart b/lib/screens/learn/wife_learn_screen.dart new file mode 100644 index 0000000..85d0493 --- /dev/null +++ b/lib/screens/learn/wife_learn_screen.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:christian_period_tracker/models/scripture.dart'; + +class WifeLearnScreen extends StatelessWidget { + const WifeLearnScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Reproductive Health Education'), + actions: [ + IconButton( + icon: const Icon(Icons.bookmark), + onPressed: () {}, + ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHealthTipsSection(context), + const SizedBox(height: 24), + _buildDiseasePreventionSection(context), + const SizedBox(height: 24), + _buildPracticalAdviceSection(context), + ], + ), + ), + ), + ); + } + + Widget _buildHealthTipsSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Reproductive Health Tips', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTipCard( + context, + title: 'Regular Medical Check-ups', + content: 'Schedule regular gynecological check-ups to monitor your reproductive health and catch any potential issues early.', + icon: Icons.medical_services, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Healthy Lifestyle', + content: 'Maintain a balanced diet, exercise regularly, and get adequate sleep to support overall reproductive wellness.', + icon: Icons.healing, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Stress Management', + content: 'Chronic stress can affect menstrual cycles and fertility. Practice mindfulness techniques and seek support when needed.', + icon: Icons.spa, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildDiseasePreventionSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Disease Prevention Between Partners', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTipCard( + context, + title: 'Safe Sexual Practices', + content: 'Use protection consistently to prevent sexually transmitted infections and maintain mutual health.', + icon: Icons.protect, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Regular Testing', + content: 'Schedule regular STI screenings together with your partner for early detection and treatment.', + icon: Icons.medical_information, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Open Communication', + content: 'Discuss health concerns openly to ensure both partners understand each other\'s needs and maintain trust.', + icon: Icons.chat, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildPracticalAdviceSection(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Practical Advice for Partnership', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16.0), + children: [ + _buildTipCard( + context, + title: 'Support System', + content: 'Build a support system that includes both your partner and trusted healthcare providers.', + icon: Icons.supervisor_account, + ), + const SizedBox(height: 16), + _buildTipCard( + context, + title: 'Educate Together', + content: 'Both partners should educate themselves about reproductive health to make informed decisions together.', + icon: Icons.school, + ), + ], + ), + ), + ], + ); + } + + Widget _buildTipCard(BuildContext context, {required String title, required String content, required IconData icon}) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, size: 24, color: Theme.of(context).colorScheme.primary), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + content, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/appearance_screen.dart b/lib/screens/settings/appearance_screen.dart new file mode 100644 index 0000000..4fd6ef8 --- /dev/null +++ b/lib/screens/settings/appearance_screen.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../models/user_profile.dart'; +import '../../providers/user_provider.dart'; +import '../../theme/app_theme.dart'; + +class AppearanceScreen extends ConsumerWidget { + const AppearanceScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userProfile = ref.watch(userProfileProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Appearance'), + ), + body: userProfile == null + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.all(16.0), + children: [ + _buildThemeModeSelector(context, ref, userProfile.themeMode), + const SizedBox(height: 24), + _buildAccentColorSelector( + context, ref, userProfile.accentColor, AppColors.sageGreen), + const SizedBox(height: 32), + _buildRelationshipStatusSelector(context, ref, userProfile.relationshipStatus), + ], + ), + ); + } + + Widget _buildThemeModeSelector( + BuildContext context, WidgetRef ref, AppThemeMode currentMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Theme Mode', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + SegmentedButton( + segments: const [ + ButtonSegment( + value: AppThemeMode.light, + label: Text('Light'), + icon: Icon(Icons.light_mode), + ), + ButtonSegment( + value: AppThemeMode.dark, + label: Text('Dark'), + icon: Icon(Icons.dark_mode), + ), + ButtonSegment( + value: AppThemeMode.system, + label: Text('System'), + icon: Icon(Icons.brightness_auto), + ), + ], + selected: {currentMode}, + onSelectionChanged: (Set newSelection) { + if (newSelection.isNotEmpty) { + ref + .read(userProfileProvider.notifier) + .updateThemeMode(newSelection.first); + } + }, + style: SegmentedButton.styleFrom( + fixedSize: const Size.fromHeight(48), + ) + ), + ], + ); + } + + Widget _buildAccentColorSelector(BuildContext context, WidgetRef ref, + String currentAccent) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Accent Color', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Wrap( + spacing: 16, + runSpacing: 16, + children: [ + GestureDetector( + onTap: () { + ref + .read(userProfileProvider.notifier) + .updateAccentColor('0xFFA8C5A8'); + }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: AppColors.sageGreen, + shape: BoxShape.circle, + border: Border.all( + color: + Theme.of(context).colorScheme.primary, // Assuming currentAccent is sageGreen + width: 3, + ), + ), + child: const Icon(Icons.check, color: Colors.white), + ), + ), + ], + ), + ], + ); + } + + Widget _buildRelationshipStatusSelector( + BuildContext context, WidgetRef ref, RelationshipStatus currentStatus) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Relationship Status', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + SegmentedButton( + segments: const [ + ButtonSegment( + value: RelationshipStatus.single, + label: Text('Single'), + icon: Icon(Icons.person_outline), + ), + ButtonSegment( + value: RelationshipStatus.engaged, + label: Text('Engaged'), + icon: Icon(Icons.favorite_border), + ), + ButtonSegment( + value: RelationshipStatus.married, + label: Text('Married'), + icon: Icon(Icons.favorite), + ), + ], + selected: {currentStatus}, + onSelectionChanged: (Set newSelection) { + if (newSelection.isNotEmpty) { + ref + .read(userProfileProvider.notifier) + .updateRelationshipStatus(newSelection.first); + } + }, + style: SegmentedButton.styleFrom( + fixedSize: const Size.fromHeight(48), + ) + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/cycle_history_screen.dart b/lib/screens/settings/cycle_history_screen.dart new file mode 100644 index 0000000..c98abca --- /dev/null +++ b/lib/screens/settings/cycle_history_screen.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:collection/collection.dart'; +import '../../models/cycle_entry.dart'; +import '../../providers/user_provider.dart'; + +class CycleHistoryScreen extends ConsumerWidget { + const CycleHistoryScreen({super.key}); + + void _showDeleteAllDialog(BuildContext context, WidgetRef ref) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete All History?'), + content: const Text( + 'This will permanently delete all cycle entries. This action cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref.read(cycleEntriesProvider.notifier).clearEntries(); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('All cycle history has been deleted.')), + ); + }, + child: const Text('Delete All', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + + void _showDeleteMonthDialog(BuildContext context, WidgetRef ref) { + showDialog( + context: context, + builder: (context) => const _DeleteMonthDialog(), + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final entries = ref.watch(cycleEntriesProvider); + + final groupedEntries = groupBy( + entries, + (CycleEntry entry) => DateFormat('MMMM yyyy').format(entry.date), + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Cycle History'), + actions: [ + if (entries.isNotEmpty) + PopupMenuButton( + onSelected: (value) { + if (value == 'delete_all') { + _showDeleteAllDialog(context, ref); + } else if (value == 'delete_month') { + _showDeleteMonthDialog(context, ref); + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'delete_month', + child: Text('Delete by Month'), + ), + const PopupMenuItem( + value: 'delete_all', + child: Text('Delete All Data'), + ), + ], + ), + ], + ), + body: entries.isEmpty + ? Center( + child: Text( + 'No cycle history found.', + style: Theme.of(context).textTheme.bodyLarge, + ), + ) + : ListView.builder( + itemCount: groupedEntries.keys.length, + itemBuilder: (context, index) { + final month = groupedEntries.keys.elementAt(index); + final monthEntries = groupedEntries[month]!; + return ExpansionTile( + title: Text(month, style: Theme.of(context).textTheme.titleLarge), + initiallyExpanded: index == 0, + children: monthEntries.map((entry) { + return Dismissible( + key: Key(entry.id), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + ref.read(cycleEntriesProvider.notifier).deleteEntry(entry.id); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Entry for ${DateFormat.yMMMd().format(entry.date)} deleted.')), + ); + }, + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: const Icon(Icons.delete, color: Colors.white), + ), + child: ListTile( + title: Text(DateFormat.yMMMMEEEEd().format(entry.date)), + subtitle: Text(_buildEntrySummary(entry)), + isThreeLine: true, + ), + ); + }).toList(), + ); + }, + ), + ); + } + + String _buildEntrySummary(CycleEntry entry) { + final summary = []; + if (entry.isPeriodDay) { + summary.add('Period'); + } + if (entry.mood != null) { + summary.add('Mood: ${entry.mood!.label}'); + } + if (entry.symptomCount > 0) { + summary.add('${entry.symptomCount} symptom(s)'); + } + if (entry.notes != null && entry.notes!.isNotEmpty) { + summary.add('Note'); + } + if (summary.isEmpty) { + return 'No specific data logged.'; + } + return summary.join(' • '); + } +} + +class _DeleteMonthDialog extends ConsumerStatefulWidget { + const _DeleteMonthDialog(); + + @override + ConsumerState<_DeleteMonthDialog> createState() => _DeleteMonthDialogState(); +} + +class _DeleteMonthDialogState extends ConsumerState<_DeleteMonthDialog> { + late int _selectedYear; + late int _selectedMonth; + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + _selectedYear = now.year; + _selectedMonth = now.month; + } + + @override + Widget build(BuildContext context) { + final years = + List.generate(5, (index) => DateTime.now().year - index); + final months = List.generate(12, (index) => index + 1); + + return AlertDialog( + title: const Text('Delete by Month'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Select a month and year to delete all entries from.'), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownButton( + value: _selectedMonth, + items: months.map((month) => DropdownMenuItem( + value: month, + child: Text(DateFormat('MMMM').format(DateTime(0, month))), + )) + .toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedMonth = value; + }); + } + }, + ), + const SizedBox(width: 16), + DropdownButton( + value: _selectedYear, + items: years + .map((year) => DropdownMenuItem( + value: year, + child: Text(year.toString()), + )) + .toList(), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedYear = value; + }); + } + }, + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref + .read(cycleEntriesProvider.notifier) + .deleteEntriesForMonth(_selectedYear, _selectedMonth); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Deleted entries for ${DateFormat('MMMM yyyy').format(DateTime(0, _selectedMonth))}.')), + ); + }, + child: const Text('Delete', style: TextStyle(color: Colors.red)), + ), + ], + ); + } +} diff --git a/lib/screens/settings/cycle_settings_screen.dart b/lib/screens/settings/cycle_settings_screen.dart new file mode 100644 index 0000000..6de56e7 --- /dev/null +++ b/lib/screens/settings/cycle_settings_screen.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import '../../models/user_profile.dart'; +import '../../providers/user_provider.dart'; + +class CycleSettingsScreen extends ConsumerStatefulWidget { + const CycleSettingsScreen({super.key}); + + @override + ConsumerState createState() => + _CycleSettingsScreenState(); +} + +class _CycleSettingsScreenState extends ConsumerState { + final _formKey = GlobalKey(); + late TextEditingController _cycleLengthController; + late TextEditingController _periodLengthController; + DateTime? _lastPeriodStartDate; + bool _isIrregularCycle = false; + + @override + void initState() { + super.initState(); + final userProfile = ref.read(userProfileProvider); + _cycleLengthController = TextEditingController( + text: userProfile?.averageCycleLength.toString() ?? '28'); + _periodLengthController = TextEditingController( + text: userProfile?.averagePeriodLength.toString() ?? '5'); + _lastPeriodStartDate = userProfile?.lastPeriodStartDate; + _isIrregularCycle = userProfile?.isIrregularCycle ?? false; + } + + @override + void dispose() { + _cycleLengthController.dispose(); + _periodLengthController.dispose(); + super.dispose(); + } + + void _saveSettings() { + if (_formKey.currentState!.validate()) { + final userProfile = ref.read(userProfileProvider); + if (userProfile != null) { + final updatedProfile = userProfile.copyWith( + averageCycleLength: int.tryParse(_cycleLengthController.text) ?? userProfile.averageCycleLength, + averagePeriodLength: int.tryParse(_periodLengthController.text) ?? userProfile.averagePeriodLength, + lastPeriodStartDate: _lastPeriodStartDate, + isIrregularCycle: _isIrregularCycle, + ); + ref.read(userProfileProvider.notifier).updateProfile(updatedProfile); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Cycle settings saved!')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Cycle Settings'), + ), + body: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + TextFormField( + controller: _cycleLengthController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Average Cycle Length (days)', + hintText: 'e.g., 28', + ), + validator: (value) { + if (value == null || int.tryParse(value) == null) { + return 'Please enter a valid number'; + } + return null; + }, + ), + const SizedBox(height: 24), + TextFormField( + controller: _periodLengthController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Average Period Length (days)', + hintText: 'e.g., 5', + ), + validator: (value) { + if (value == null || int.tryParse(value) == null) { + return 'Please enter a valid number'; + } + return null; + }, + ), + const SizedBox(height: 24), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Last Period Start Date'), + subtitle: Text(_lastPeriodStartDate == null + ? 'Not set' + : DateFormat.yMMMMd().format(_lastPeriodStartDate!)), + trailing: const Icon(Icons.calendar_today), + onTap: () async { + final pickedDate = await showDatePicker( + context: context, + initialDate: _lastPeriodStartDate ?? DateTime.now(), + firstDate: DateTime(DateTime.now().year - 1), + lastDate: DateTime.now(), + ); + if (pickedDate != null) { + setState(() { + _lastPeriodStartDate = pickedDate; + }); + } + }, + ), + const Divider(), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('My cycle is irregular'), + value: _isIrregularCycle, + onChanged: (value) { + setState(() { + _isIrregularCycle = value; + }); + }, + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: _saveSettings, + child: const Text('Save Settings'), + ) + ], + ), + ), + ); + } +} diff --git a/lib/screens/settings/export_data_screen.dart b/lib/screens/settings/export_data_screen.dart new file mode 100644 index 0000000..0f785c6 --- /dev/null +++ b/lib/screens/settings/export_data_screen.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../providers/user_provider.dart'; +import '../../services/pdf_service.dart'; +import '../../services/ics_service.dart'; + +class ExportDataScreen extends ConsumerWidget { + const ExportDataScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userProfile = ref.watch(userProfileProvider); + final cycleEntries = ref.watch(cycleEntriesProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Export Data'), + ), + body: userProfile == null + ? const Center(child: CircularProgressIndicator()) + : ListView( + padding: const EdgeInsets.all(16.0), + children: [ + ListTile( + leading: const Icon(Icons.picture_as_pdf), + title: const Text('Export as PDF'), + subtitle: const Text('Generate a printable PDF report of your cycle data.'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + try { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Generating PDF report...')), + ); + await PdfService.generateCycleReport(userProfile, cycleEntries); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('PDF report generated successfully!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to generate PDF: $e')), + ); + } + }, + ), + ListTile( + leading: const Icon(Icons.calendar_month), + title: const Text('Export to Calendar File (.ics)'), + subtitle: const Text('Generate a calendar file for your cycle dates.'), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + try { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Generating ICS file...')), + ); + await IcsService.generateCycleCalendar(cycleEntries); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('ICS file generated successfully!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to generate ICS file: $e')), + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/settings/privacy_settings_screen.dart b/lib/screens/settings/privacy_settings_screen.dart new file mode 100644 index 0000000..34f6bac --- /dev/null +++ b/lib/screens/settings/privacy_settings_screen.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:health/health.dart'; +import '../../providers/user_provider.dart'; +import '../../services/health_service.dart'; + +class PrivacySettingsScreen extends ConsumerStatefulWidget { + const PrivacySettingsScreen({super.key}); + + @override + ConsumerState createState() => + _PrivacySettingsScreenState(); +} + +class _PrivacySettingsScreenState extends ConsumerState { + bool _hasPermissions = false; + final HealthService _healthService = HealthService(); + + @override + void initState() { + super.initState(); + _checkPermissions(); + } + + Future _checkPermissions() async { + final hasPermissions = await _healthService.hasPermissions(_healthService.menstruationDataTypes); + setState(() { + _hasPermissions = hasPermissions; + }); + } + + Future _requestPermissions() async { + final authorized = await _healthService.requestAuthorization(_healthService.menstruationDataTypes); + if (authorized) { + _hasPermissions = true; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Health app access granted!')), + ); + } else { + _hasPermissions = false; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Health app access denied.')), + ); + } + setState(() {}); // Rebuild to update UI + } + + Future _syncPeriodDays(bool sync) async { + if (sync) { + if (!_hasPermissions) { + // Request permissions if not already granted + final authorized = await _healthService.requestAuthorization(_healthService.menstruationDataTypes); + if (!authorized) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Cannot sync without health app permissions.')), + ); + } + return; + } + _hasPermissions = true; + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Syncing period data...')), + ); + } + final userProfile = ref.read(userProfileProvider); + final cycleEntries = ref.read(cycleEntriesProvider); + + if (userProfile != null) { + final success = await _healthService.writeMenstruationData(cycleEntries); + if (mounted) { + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Period data synced successfully!')), + ); + // Optionally store a flag in userProfile if sync is active + // userProfile.copyWith(syncPeriodToHealth: true) + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to sync period data.')), + ); + } + } + } + } else { + // Logic to disable sync (e.g., revoke permissions if Health package supports it, + // or just stop writing data in future) + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Period data sync disabled.')), + ); + } + } + setState(() {}); // Rebuild to update UI + } + + @override + Widget build(BuildContext context) { + // This value would ideally come from userProfile.syncPeriodToHealth + bool syncPeriodToHealth = _hasPermissions; + + return Scaffold( + appBar: AppBar( + title: const Text('Privacy Settings'), + ), + body: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + ListTile( + title: const Text('Health App Integration'), + subtitle: _hasPermissions + ? const Text('Connected to Health App. Period data can be synced.') + : const Text('Not connected. Tap to grant access.'), + trailing: _hasPermissions ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.warning, color: Colors.orange), + onTap: _requestPermissions, + ), + SwitchListTile( + title: const Text('Sync Period Days'), + subtitle: const Text('Automatically sync your period start and end dates to your health app.'), + value: syncPeriodToHealth, + onChanged: (value) async { + if (value) { + await _syncPeriodDays(true); + } else { + await _syncPeriodDays(false); + } + setState(() { + syncPeriodToHealth = value; // Update local state for toggle + }); + }, + enabled: _hasPermissions, // Only enable if connected + ), + // TODO: Add more privacy settings if needed + ], + ), + ); + } +} diff --git a/lib/screens/settings/sharing_settings_screen.dart b/lib/screens/settings/sharing_settings_screen.dart new file mode 100644 index 0000000..a68865d --- /dev/null +++ b/lib/screens/settings/sharing_settings_screen.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../models/user_profile.dart'; +import '../../providers/user_provider.dart'; + +class SharingSettingsScreen extends ConsumerWidget { + const SharingSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userProfile = ref.watch(userProfileProvider); + + if (userProfile == null) { + return Scaffold( + appBar: AppBar( + title: const Text('Sharing Settings'), + ), + body: const Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Sharing Settings'), + ), + body: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + SwitchListTile( + title: const Text('Share Moods'), + value: userProfile.shareMoods, + onChanged: (value) { + ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(shareMoods: value)); + }, + ), + SwitchListTile( + title: const Text('Share Symptoms'), + value: userProfile.shareSymptoms, + onChanged: (value) { + ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(shareSymptoms: value)); + }, + ), + SwitchListTile( + title: const Text('Share Cravings'), + value: userProfile.shareCravings, + onChanged: (value) { + ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(shareCravings: value)); + }, + ), + SwitchListTile( + title: const Text('Share Energy Levels'), + value: userProfile.shareEnergyLevels, + onChanged: (value) { + ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(shareEnergyLevels: value)); + }, + ), + SwitchListTile( + title: const Text('Share Sleep Data'), + value: userProfile.shareSleep, + onChanged: (value) { + ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(shareSleep: value)); + }, + ), + SwitchListTile( + title: const Text('Share Intimacy Details'), + value: userProfile.shareIntimacy, + onChanged: (value) { + ref + .read(userProfileProvider.notifier) + .updateProfile(userProfile.copyWith(shareIntimacy: value)); + }, + ), + ], + ), + ); + } +} diff --git a/lib/services/health_service.dart b/lib/services/health_service.dart new file mode 100644 index 0000000..7554c7a --- /dev/null +++ b/lib/services/health_service.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:health/health.dart'; +import 'package:collection/collection.dart'; +import '../models/cycle_entry.dart'; + +class HealthService { + static final HealthService _instance = HealthService._internal(); + factory HealthService() => _instance; + HealthService._internal(); + + final Health _health = Health(); + List _requestedTypes = []; + + // Define data types for menstruation + static const List _menstruationDataTypes = [ + HealthDataType.menstruation, + ]; + + Future requestAuthorization(List types) async { + _requestedTypes = types; + try { + final bool authorized = await _health.requestAuthorization(types); + return authorized; + } catch (e) { + debugPrint("Error requesting authorization: $e"); + return false; + } + } + + Future hasPermissions(List types) async { + return await _health.hasPermissions(types); + } + + Future writeMenstruationData(List entries) async { + // Filter for period days + final periodEntries = entries.where((entry) => entry.isPeriodDay).toList(); + + if (periodEntries.isEmpty) { + debugPrint("No period entries to write."); + return true; // Nothing to write is not an error + } + + // Check if authorized for menstruation data + final hasAuth = await hasPermissions([HealthDataType.menstruation]); + if (!hasAuth) { + debugPrint("Authorization not granted for menstruation data."); + return false; + } + + bool allWrittenSuccessfully = true; + for (var entry in periodEntries) { + try { + final success = await _health.writeHealthData( + entry.date, // Start date + entry.date.add(const Duration(days: 1)), // End date (inclusive of start, so +1 day for all-day event) + HealthDataType.menstruation, + // HealthKit menstruation type often doesn't need a value, + // it's the presence of the event that matters. + // For other types, a value would be required. + Platform.isIOS ? 0.0 : 0.0, // Value depends on platform and data type + ); + if (!success) { + allWrittenSuccessfully = false; + debugPrint("Failed to write menstruation data for ${entry.date}"); + } + } catch (e) { + allWrittenSuccessfully = false; + debugPrint("Error writing menstruation data for ${entry.date}: $e"); + } + } + return allWrittenSuccessfully; + } + + List get mensturationDataTypes => _menstruationDataTypes; +} diff --git a/lib/services/ics_service.dart b/lib/services/ics_service.dart new file mode 100644 index 0000000..097b447 --- /dev/null +++ b/lib/services/ics_service.dart @@ -0,0 +1,69 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:icalendar_parser/icalendar_parser.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:intl/intl.dart'; + +import '../models/cycle_entry.dart'; + +class IcsService { + static Future generateCycleCalendar(List entries) async { + final iCalendar = ICalendar( + properties: { + 'prodid': '-//Christian Period Tracker//NONSGML v1.0//EN', + 'version': '2.0', + 'calscale': 'GREGORIAN', + 'x-wr-calname': 'Cycle Tracking', + 'x-wr-timezone': DateTime.now().timeZoneName, + }, + components: [], + ); + + // Sort entries by date to ensure proper calendar order + entries.sort((a, b) => a.date.compareTo(b.date)); + + for (var entry in entries) { + if (entry.isPeriodDay) { + final date = entry.date; + final formattedDate = DateFormat('yyyyMMdd').format(date); + final uid = '${date.year}${date.month}${date.day}-${entry.id}@christianperiodtracker.app'; + + iCalendar.components.add( + CalendarEvent( + properties: { + 'uid': uid, + 'dtstamp': IcsDateTime(dt: DateTime.now()), + 'dtstart': IcsDateTime(dt: date, isUtc: false, date: true), // All-day event + 'dtend': IcsDateTime(dt: date.add(const Duration(days: 1)), isUtc: false, date: true), // End on next day for all-day + 'summary': 'Period Day', + 'description': 'Period tracking for ${DateFormat.yMMMd().format(date)}', + }, + ), + ); + } + } + + final String icsContent = iCalendar.serialize(); + final String fileName = 'cycle_calendar_${DateFormat('yyyyMMdd').format(DateTime.now())}.ics'; + + if (kIsWeb) { + // Web platform + final bytes = icsContent.codeUnits; + final blob = html.Blob([bytes], 'text/calendar'); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute('download', fileName) + ..click(); + html.Url.revokeObjectUrl(url); + } else { + // Mobile platform + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsString(icsContent); + await OpenFilex.open(filePath); + } + } +} diff --git a/lib/services/pdf_service.dart b/lib/services/pdf_service.dart new file mode 100644 index 0000000..2aeb4e4 --- /dev/null +++ b/lib/services/pdf_service.dart @@ -0,0 +1,159 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:universal_html/html.dart' as html; + +import '../models/user_profile.dart'; +import '../models/cycle_entry.dart'; +import '../theme/app_theme.dart'; + +class PdfService { + static Future generateCycleReport(UserProfile user, List entries) async { + final pdf = pw.Document(); + + final primaryColor = PdfColor.fromInt(AppColors.sageGreen.value); + final accentColor = PdfColor.fromInt(AppColors.rose.value); + final textColor = PdfColor.fromInt(AppColors.charcoal.value); + + // Sort entries by date for the report + entries.sort((a, b) => a.date.compareTo(b.date)); + + pdf.addPage( + pw.MultiPage( + pageFormat: PdfPageFormat.a4, + build: (pw.Context context) => [ + _buildHeader(user, primaryColor, accentColor, textColor), + pw.SizedBox(height: 20), + _buildCycleSummary(user, textColor), + pw.SizedBox(height: 20), + _buildEntriesTable(entries, primaryColor, textColor), + ], + ), + ); + + final String fileName = 'cycle_report_${DateFormat('yyyyMMdd').format(DateTime.now())}.pdf'; + + if (kIsWeb) { + // Web platform + final bytes = await pdf.save(); + final blob = html.Blob([bytes], 'application/pdf'); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute('download', fileName) + ..click(); + html.Url.revokeObjectUrl(url); + } else { + // Mobile platform + final directory = await getApplicationDocumentsDirectory(); + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsBytes(await pdf.save()); + await OpenFilex.open(filePath); + } + } + + static pw.Widget _buildHeader(UserProfile user, PdfColor primaryColor, PdfColor accentColor, PdfColor textColor) { + return pw.Container( + padding: pw.EdgeInsets.all(10), + decoration: pw.BoxDecoration( + color: primaryColor.lighter(30), + borderRadius: pw.BorderRadius.circular(8), + ), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Cycle Report for ${user.name}', + style: pw.TextStyle( + fontSize: 24, + fontWeight: pw.FontWeight.bold, + color: primaryColor.isLight ? primaryColor.darker(50) : PdfColors.white, + ), + ), + pw.SizedBox(height: 5), + pw.Text( + 'Generated on: ${DateFormat('MMMM d, yyyy').format(DateTime.now())}', + style: pw.TextStyle(fontSize: 12, color: primaryColor.isLight ? primaryColor.darker(30) : PdfColors.white.darker(10)), + ), + ], + ), + ); + } + + static pw.Widget _buildCycleSummary(UserProfile user, PdfColor textColor) { + return pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Summary', + style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold, color: textColor), + ), + pw.SizedBox(height: 10), + pw.Text('Average Cycle Length: ${user.averageCycleLength} days', style: pw.TextStyle(color: textColor)), + pw.Text('Average Period Length: ${user.averagePeriodLength} days', style: pw.TextStyle(color: textColor)), + if (user.lastPeriodStartDate != null) + pw.Text('Last Period Start: ${DateFormat.yMMMMd().format(user.lastPeriodStartDate!)}', style: pw.TextStyle(color: textColor)), + pw.Text('Irregular Cycle: ${user.isIrregularCycle ? 'Yes' : 'No'}', style: pw.TextStyle(color: textColor)), + ], + ); + } + + static pw.Widget _buildEntriesTable(List entries, PdfColor primaryColor, PdfColor textColor) { + final headers = ['Date', 'Period', 'Mood', 'Symptoms', 'Notes']; + + return pw.Table.fromTextArray( + headers: headers, + data: entries.map((entry) { + return [ + DateFormat.yMMMd().format(entry.date), + entry.isPeriodDay ? 'Yes' : 'No', + entry.mood != null ? entry.mood!.label : 'N/A', + entry.hasSymptoms ? entry.symptomCount.toString() : 'No', + entry.notes != null && entry.notes!.isNotEmpty ? entry.notes! : 'N/A', + ]; + }).toList(), + border: pw.TableBorder.all(color: primaryColor.lighter(10)), + headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, color: primaryColor), + cellStyle: pw.TextStyle(color: textColor), + cellAlignment: pw.Alignment.centerLeft, + headerDecoration: pw.BoxDecoration(color: primaryColor.lighter(20)), + rowDecoration: pw.BoxDecoration(color: PdfColors.grey100), + tableWidth: pw.TableWidth.min, + ); + } +} + +// Extension to determine if a color is light or dark for text contrast +extension on PdfColor { + bool get isLight { + // Calculate luminance (Y from YIQ) + // Formula: Y = (R*299 + G*587 + B*114) / 1000 + final r = red * 255; + final g = green * 255; + final b = blue * 255; + final luminance = (r * 299 + g * 587 + b * 114) / 1000; + return luminance > 128; // Using 128 as a threshold + } + + PdfColor lighter(int amount) { + double factor = 1 + (amount / 100.0); + return PdfColor( + red * factor > 1.0 ? 1.0 : red * factor, + green * factor > 1.0 ? 1.0 : green * factor, + blue * factor > 1.0 ? 1.0 : blue * factor, + ); + } + + PdfColor darker(int amount) { + double factor = 1 - (amount / 100.0); + return PdfColor( + red * factor < 0.0 ? 0.0 : red * factor, + green * factor < 0.0 ? 0.0 : green * factor, + blue * factor < 0.0 ? 0.0 : blue * factor, + ); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index 4ac89ff..5fc1b2d 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -38,14 +38,14 @@ class AppColors { /// App theme configuration class AppTheme { - static ThemeData get lightTheme { + static ThemeData getLightTheme(Color accentColor) { return ThemeData( useMaterial3: true, brightness: Brightness.light, // Color Scheme - colorScheme: const ColorScheme.light( - primary: AppColors.sageGreen, + colorScheme: ColorScheme.light( + primary: accentColor, secondary: AppColors.rose, tertiary: AppColors.lavender, surface: AppColors.cream, @@ -139,7 +139,7 @@ class AppTheme { // Button Themes elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: AppColors.sageGreen, + backgroundColor: accentColor, foregroundColor: Colors.white, elevation: 2, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), @@ -155,8 +155,8 @@ class AppTheme { outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: AppColors.sageGreen, - side: const BorderSide(color: AppColors.sageGreen, width: 1.5), + foregroundColor: accentColor, + side: BorderSide(color: accentColor, width: 1.5), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -192,7 +192,7 @@ class AppTheme { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.sageGreen, width: 2), + borderSide: BorderSide(color: accentColor, width: 2), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), @@ -205,7 +205,7 @@ class AppTheme { // Bottom Navigation bottomNavigationBarTheme: BottomNavigationBarThemeData( backgroundColor: Colors.white, - selectedItemColor: AppColors.sageGreen, + selectedItemColor: accentColor, unselectedItemColor: AppColors.warmGray, type: BottomNavigationBarType.fixed, elevation: 8, @@ -220,18 +220,18 @@ class AppTheme { ), // Floating Action Button - floatingActionButtonTheme: const FloatingActionButtonThemeData( - backgroundColor: AppColors.sageGreen, + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: accentColor, foregroundColor: Colors.white, elevation: 4, ), // Slider Theme sliderTheme: SliderThemeData( - activeTrackColor: AppColors.sageGreen, + activeTrackColor: accentColor, inactiveTrackColor: AppColors.lightGray.withOpacity(0.3), - thumbColor: AppColors.sageGreen, - overlayColor: AppColors.sageGreen.withOpacity(0.2), + thumbColor: accentColor, + overlayColor: accentColor.withOpacity(0.2), trackHeight: 4, ), @@ -244,14 +244,14 @@ class AppTheme { ); } - static ThemeData get darkTheme { + static ThemeData getDarkTheme(Color accentColor) { return ThemeData( useMaterial3: true, brightness: Brightness.dark, // Color Scheme colorScheme: ColorScheme.dark( - primary: AppColors.sageGreen, + primary: accentColor, secondary: AppColors.rose, tertiary: AppColors.lavender, surface: const Color(0xFF1E1E1E), @@ -348,7 +348,7 @@ class AppTheme { // Button Themes elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - backgroundColor: AppColors.sageGreen, + backgroundColor: accentColor, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), @@ -364,8 +364,8 @@ class AppTheme { outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - foregroundColor: AppColors.sageGreen, - side: BorderSide(color: AppColors.sageGreen.withOpacity(0.5), width: 1.5), + foregroundColor: accentColor, + side: BorderSide(color: accentColor.withOpacity(0.5), width: 1.5), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), @@ -391,7 +391,7 @@ class AppTheme { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: const BorderSide(color: AppColors.sageGreen, width: 2), + borderSide: BorderSide(color: accentColor, width: 2), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), @@ -404,7 +404,7 @@ class AppTheme { // Bottom Navigation bottomNavigationBarTheme: BottomNavigationBarThemeData( backgroundColor: const Color(0xFF1E1E1E), - selectedItemColor: AppColors.sageGreen, + selectedItemColor: accentColor, unselectedItemColor: Colors.white38, type: BottomNavigationBarType.fixed, elevation: 0, @@ -420,10 +420,10 @@ class AppTheme { // Slider Theme sliderTheme: SliderThemeData( - activeTrackColor: AppColors.sageGreen, + activeTrackColor: accentColor, inactiveTrackColor: Colors.white.withOpacity(0.1), - thumbColor: AppColors.sageGreen, - overlayColor: AppColors.sageGreen.withOpacity(0.2), + thumbColor: accentColor, + overlayColor: accentColor.withOpacity(0.2), trackHeight: 4, tickMarkShape: const RoundSliderTickMarkShape(), activeTickMarkColor: Colors.white24, diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..ce0e550 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..0c2c3c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + printing ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 61ff451..f2da802 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,16 @@ import FlutterMacOS import Foundation +import device_info_plus import flutter_local_notifications import path_provider_foundation +import printing import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 4ab7606..99f9f62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" args: dependency: transitive description: @@ -33,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" boolean_selector: dependency: transitive description: @@ -105,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.12.1" + carp_serializable: + dependency: transitive + description: + name: carp_serializable + sha256: f039f8ea22e9437aef13fe7e9743c3761c76d401288dcb702eadd273c3e4dcef + url: "https://pub.dev" + source: hosted + version: "2.0.1" characters: dependency: transitive description: @@ -113,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -161,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -185,6 +233,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" + url: "https://pub.dev" + source: hosted + version: "11.3.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" equatable: dependency: transitive description: @@ -328,6 +392,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + health: + dependency: "direct main" + description: + name: health + sha256: "320633022fb2423178baa66508001c4ca5aee5806ffa2c913e66488081e9fd47" + url: "https://pub.dev" + source: hosted + version: "13.1.4" hive: dependency: "direct main" description: @@ -352,6 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -376,6 +456,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + icalendar_parser: + dependency: "direct main" + description: + name: icalendar_parser + sha256: "107f936d286723346660df88191888562fc44c2def46ed8858a301a4777ad821" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" intl: dependency: "direct main" description: @@ -488,6 +584,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.4" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" package_config: dependency: transitive description: @@ -513,7 +617,7 @@ packages: source: hosted version: "1.1.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -560,6 +664,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" + url: "https://pub.dev" + source: hosted + version: "3.11.3" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" petitparser: dependency: transitive description: @@ -600,6 +720,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + printing: + dependency: "direct main" + description: + name: printing + sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93" + url: "https://pub.dev" + source: hosted + version: "5.14.2" pub_semver: dependency: transitive description: @@ -616,6 +752,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" riverpod: dependency: transitive description: @@ -829,6 +973,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_html: + dependency: "direct main" + description: + name: universal_html + sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" uuid: dependency: "direct main" description: @@ -909,6 +1069,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.dev" + source: hosted + version: "1.1.5" xdg_directories: dependency: transitive description: @@ -934,5 +1110,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index d108cd7..11ec7f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,13 @@ dependencies: uuid: ^4.5.1 shared_preferences: ^2.3.2 xml: ^6.3.0 # Added for XML parsing + health: ^13.1.4 # For Health App integration + pdf: ^3.10.8 # For PDF generation + printing: ^5.12.0 # For printing and sharing PDF + path_provider: ^2.1.3 # For getting file paths + open_filex: ^4.3.2 # For opening files + universal_html: ^2.2.12 # For web downloads + icalendar_parser: ^2.0.0 # For .ics file generation dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..3dea03b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..e685eaf 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + printing ) list(APPEND FLUTTER_FFI_PLUGIN_LIST