- Add 10-second periodic auto-sync to CycleEntriesNotifier - Fix husband_devotional_screen: use partnerId for isConnected check, navigate to SharingSettingsScreen instead of legacy mock dialog - Remove obsolete _showConnectDialog method and mock data import - Update husband_settings_screen: show 'Partner Settings' with linked partner name when connected - Add SharingSettingsScreen: Pad Supplies toggle (disabled when pad tracking off), Intimacy always enabled - Add CORS OPTIONS handler to backend server - Add _ensureServerRegistration for reliable partner linking - Add copy button to Invite Partner dialog - Dynamic base URL for web (uses window.location.hostname)
249 lines
7.2 KiB
Dart
249 lines
7.2 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import '../models/user_profile.dart';
|
|
import '../models/cycle_entry.dart';
|
|
|
|
import '../services/cycle_service.dart';
|
|
import '../services/sync_service.dart';
|
|
import 'prayer_provider.dart';
|
|
import 'teaching_plan_provider.dart';
|
|
import '../models/prayer_request.dart';
|
|
import '../models/teaching_plan.dart';
|
|
|
|
/// Provider for the user profile
|
|
final userProfileProvider =
|
|
StateNotifierProvider<UserProfileNotifier, UserProfile?>((ref) {
|
|
return UserProfileNotifier();
|
|
});
|
|
|
|
/// Notifier for the user profile
|
|
class UserProfileNotifier extends StateNotifier<UserProfile?> {
|
|
UserProfileNotifier() : super(null) {
|
|
_loadProfile();
|
|
}
|
|
|
|
void _loadProfile() {
|
|
final box = Hive.box<UserProfile>('user_profile_v2');
|
|
state = box.get('current_user');
|
|
}
|
|
|
|
Future<void> updateProfile(UserProfile profile) async {
|
|
final box = Hive.box<UserProfile>('user_profile_v2');
|
|
await box.put('current_user', profile);
|
|
state = profile;
|
|
}
|
|
|
|
Future<void> updateThemeMode(AppThemeMode themeMode) async {
|
|
if (state != null) {
|
|
await updateProfile(state!.copyWith(themeMode: themeMode));
|
|
}
|
|
}
|
|
|
|
Future<void> updateAccentColor(String accentColor) async {
|
|
if (state != null) {
|
|
await updateProfile(state!.copyWith(accentColor: accentColor));
|
|
}
|
|
}
|
|
|
|
Future<void> updateRelationshipStatus(
|
|
RelationshipStatus relationshipStatus) async {
|
|
if (state != null) {
|
|
await updateProfile(
|
|
state!.copyWith(relationshipStatus: relationshipStatus));
|
|
}
|
|
}
|
|
|
|
Future<void> clearProfile() async {
|
|
final box = Hive.box<UserProfile>('user_profile_v2');
|
|
await box.clear();
|
|
state = null;
|
|
}
|
|
}
|
|
|
|
/// Provider for cycle entries
|
|
final cycleEntriesProvider =
|
|
StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
|
|
return CycleEntriesNotifier(ref);
|
|
});
|
|
|
|
/// Notifier for cycle entries
|
|
class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
|
final Ref ref;
|
|
Timer? _syncTimer;
|
|
|
|
CycleEntriesNotifier(this.ref) : super([]) {
|
|
_loadEntries();
|
|
syncData(); // Auto-sync on load
|
|
|
|
// Start periodic sync every 10 seconds
|
|
_syncTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
|
|
_pull();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_syncTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _loadEntries() {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
state = box.values.toList()..sort((a, b) => b.date.compareTo(a.date));
|
|
}
|
|
|
|
Future<void> addEntry(CycleEntry entry) async {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
await box.put(entry.id, entry);
|
|
_loadEntries();
|
|
_push();
|
|
}
|
|
|
|
Future<void> updateEntry(CycleEntry entry) async {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
await box.put(entry.id, entry);
|
|
_loadEntries();
|
|
_push();
|
|
}
|
|
|
|
Future<void> deleteEntry(String id) async {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
await box.delete(id);
|
|
_loadEntries();
|
|
_push();
|
|
}
|
|
|
|
Future<void> deleteEntriesForMonth(int year, int month) async {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
final keysToDelete = <dynamic>[];
|
|
for (var entry in box.values) {
|
|
if (entry.date.year == year && entry.date.month == month) {
|
|
keysToDelete.add(entry.key);
|
|
}
|
|
}
|
|
await box.deleteAll(keysToDelete);
|
|
_loadEntries();
|
|
_push();
|
|
}
|
|
|
|
Future<void> clearEntries() async {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
await box.clear();
|
|
state = [];
|
|
_push();
|
|
}
|
|
|
|
// Sync Logic
|
|
|
|
Future<void> syncData() async {
|
|
await _pull();
|
|
// After pull, we might want to push any local changes not in remote?
|
|
// For now, simpler consistency: Pull then Push current state?
|
|
// Or just Pull. Push happens on edit.
|
|
// Let's just Pull.
|
|
}
|
|
|
|
Future<void> _push() async {
|
|
final userBox = Hive.box<UserProfile>('user_profile_v2');
|
|
final user = userBox.get('current_user');
|
|
|
|
// Read current state from other providers
|
|
// Note: This relies on the providers being initialized.
|
|
final plans = ref.read(teachingPlansProvider);
|
|
final prayers = ref.read(prayerRequestsProvider);
|
|
|
|
if (user != null) {
|
|
final userDetails = {
|
|
'name': user.name,
|
|
'role': user.role.name,
|
|
'partnerId': user.partnerId,
|
|
'createdAt': user.createdAt.toIso8601String(),
|
|
// Add other relevant fields if needed for display on partner's side
|
|
};
|
|
|
|
await SyncService().pushSyncData(
|
|
userId: user.id,
|
|
entries: state,
|
|
teachingPlans: plans,
|
|
prayerRequests: prayers,
|
|
userDetails: userDetails,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _pull() async {
|
|
final userBox = Hive.box<UserProfile>('user_profile_v2');
|
|
final user = userBox.get('current_user');
|
|
if (user == null) return;
|
|
|
|
final syncResult = await SyncService().pullSyncData(
|
|
user.id,
|
|
partnerId: user.partnerId,
|
|
);
|
|
|
|
// 0. Check for Server-Side Profile Updates (Auto-Link)
|
|
if (syncResult.containsKey('userProfile')) {
|
|
final serverProfile = syncResult['userProfile'] as Map<String, dynamic>;
|
|
final serverPartnerId = serverProfile['partnerId'];
|
|
|
|
// If server has a partner ID and we don't (or it's different), update!
|
|
if (serverPartnerId != null && serverPartnerId != user.partnerId) {
|
|
// Update local profile
|
|
final updatedProfile = user.copyWith(partnerId: serverPartnerId);
|
|
await Hive.box<UserProfile>('user_profile_v2')
|
|
.put('current_user', updatedProfile);
|
|
|
|
// Refresh provider state if needed, but important is to RE-SYNC with new ID
|
|
// so we get the partner's data immediately.
|
|
return _pull(); // Recursive call will now use new partnerId
|
|
}
|
|
}
|
|
|
|
final remoteEntries = syncResult['entries'] as List<CycleEntry>? ?? [];
|
|
final remotePlans =
|
|
syncResult['teachingPlans'] as List<TeachingPlan>? ?? [];
|
|
final remotePrayers =
|
|
syncResult['prayerRequests'] as List<PrayerRequest>? ?? [];
|
|
|
|
// 1. Cycle Entries
|
|
if (remoteEntries.isNotEmpty) {
|
|
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
|
// Simple merge: remote wins for id collisions
|
|
for (var entry in remoteEntries) {
|
|
await box.put(entry.id, entry);
|
|
}
|
|
_loadEntries();
|
|
}
|
|
|
|
// 2. Teaching Plans
|
|
if (remotePlans.isNotEmpty) {
|
|
final box = Hive.box<TeachingPlan>('teaching_plans_v2');
|
|
for (var plan in remotePlans) {
|
|
await box.put(plan.id, plan);
|
|
}
|
|
// Refresh provider
|
|
ref.invalidate(teachingPlansProvider);
|
|
}
|
|
|
|
// 3. Prayer Requests
|
|
if (remotePrayers.isNotEmpty) {
|
|
final box = Hive.box<PrayerRequest>('prayer_requests_v2');
|
|
for (var req in remotePrayers) {
|
|
await box.put(req.id, req);
|
|
}
|
|
// Refresh provider
|
|
ref.invalidate(prayerRequestsProvider);
|
|
}
|
|
}
|
|
|
|
// Example data generation removed
|
|
}
|
|
|
|
/// Computed provider for current cycle info
|
|
final currentCycleInfoProvider = Provider((ref) {
|
|
final user = ref.watch(userProfileProvider);
|
|
final entries = ref.watch(cycleEntriesProvider);
|
|
return CycleService.calculateCycleInfo(user, entries);
|
|
});
|