Add background sync runner and rejected-sync UX
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:relationship_saver/features/shared/frosted_card.dart';
|
||||
import 'package:relationship_saver/features/sync/sync_coordinator.dart';
|
||||
import 'package:relationship_saver/features/sync/sync_queue_repository.dart';
|
||||
import 'package:relationship_saver/features/sync/sync_state.dart';
|
||||
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
|
||||
|
||||
class SyncView extends ConsumerStatefulWidget {
|
||||
const SyncView({super.key});
|
||||
@@ -157,6 +158,39 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (state.lastRejected.isNotEmpty) ...<Widget>[
|
||||
FrostedCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Rejected Changes',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _busy ? null : _dismissRejections,
|
||||
icon: const Icon(Icons.done_all_rounded),
|
||||
label: const Text('Dismiss'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...state.lastRejected.map(
|
||||
(MutationRejection rejection) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _rejectionTile(rejection),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
FrostedCard(
|
||||
child: Text(
|
||||
'Sync policy\n• Core usage never blocks on backend\n• Local changes queue first and remain safe offline\n• Pull applies backend envelopes into local state',
|
||||
@@ -210,6 +244,53 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _rejectionTile(MutationRejection rejection) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.72),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEAF7FB),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
rejection.code,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelMedium?.copyWith(color: AppTheme.primary),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Mutation ${rejection.clientMutationId}',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
rejection.message,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime? value) {
|
||||
if (value == null) {
|
||||
return 'never';
|
||||
@@ -254,6 +335,25 @@ class _SyncViewState extends ConsumerState<SyncView> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _dismissRejections() async {
|
||||
setState(() {
|
||||
_busy = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await ref.read(syncQueueRepositoryProvider.notifier).clearRejections();
|
||||
setState(() {
|
||||
_status = 'Dismissed latest rejected-mutation details.';
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_busy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _run(
|
||||
Future<SyncRunResult> Function(SyncCoordinator coordinator) action,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user