feat: Add auto-sync, fix partner linking UI, update sharing settings
- 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)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import '../models/user_profile.dart';
|
||||
@@ -5,6 +6,10 @@ 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 =
|
||||
@@ -19,12 +24,12 @@ class UserProfileNotifier extends StateNotifier<UserProfile?> {
|
||||
}
|
||||
|
||||
void _loadProfile() {
|
||||
final box = Hive.box<UserProfile>('user_profile');
|
||||
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');
|
||||
final box = Hive.box<UserProfile>('user_profile_v2');
|
||||
await box.put('current_user', profile);
|
||||
state = profile;
|
||||
}
|
||||
@@ -50,7 +55,7 @@ class UserProfileNotifier extends StateNotifier<UserProfile?> {
|
||||
}
|
||||
|
||||
Future<void> clearProfile() async {
|
||||
final box = Hive.box<UserProfile>('user_profile');
|
||||
final box = Hive.box<UserProfile>('user_profile_v2');
|
||||
await box.clear();
|
||||
state = null;
|
||||
}
|
||||
@@ -59,44 +64,58 @@ class UserProfileNotifier extends StateNotifier<UserProfile?> {
|
||||
/// Provider for cycle entries
|
||||
final cycleEntriesProvider =
|
||||
StateNotifierProvider<CycleEntriesNotifier, List<CycleEntry>>((ref) {
|
||||
return CycleEntriesNotifier();
|
||||
return CycleEntriesNotifier(ref);
|
||||
});
|
||||
|
||||
/// Notifier for cycle entries
|
||||
class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
CycleEntriesNotifier() : super([]) {
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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) {
|
||||
@@ -109,7 +128,7 @@ class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
}
|
||||
|
||||
Future<void> clearEntries() async {
|
||||
final box = Hive.box<CycleEntry>('cycle_entries');
|
||||
final box = Hive.box<CycleEntry>('cycle_entries_v2');
|
||||
await box.clear();
|
||||
state = [];
|
||||
_push();
|
||||
@@ -126,28 +145,96 @@ class CycleEntriesNotifier extends StateNotifier<List<CycleEntry>> {
|
||||
}
|
||||
|
||||
Future<void> _push() async {
|
||||
final userBox = Hive.box<UserProfile>('user_profile');
|
||||
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) {
|
||||
await SyncService().pushSyncData(user.id, state);
|
||||
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');
|
||||
final userBox = Hive.box<UserProfile>('user_profile_v2');
|
||||
final user = userBox.get('current_user');
|
||||
if (user == null) return;
|
||||
|
||||
final remoteEntries = await SyncService().pullSyncData(user.id);
|
||||
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');
|
||||
// Simple merge: Remote wins or Union?
|
||||
// Union: Upsert all.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user