Add sync queue orchestration and local mutation envelopes
This commit is contained in:
@@ -2,6 +2,8 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/features/local/local_models.dart';
|
||||
import 'package:relationship_saver/features/sync/sync_queue_repository.dart';
|
||||
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -10,6 +12,7 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
static const String _storageKey = 'local_data_state_v1';
|
||||
static const String _schemaVersionKey = 'local_data_schema_version';
|
||||
static const int _schemaVersion = 2;
|
||||
static const int _syncSchemaVersion = 1;
|
||||
|
||||
static const Uuid _uuid = Uuid();
|
||||
|
||||
@@ -64,6 +67,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
...current.people,
|
||||
];
|
||||
await _setState(current.copyWith(people: people));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'person',
|
||||
entityId: person.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _personPayload(person),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updatePerson(PersonProfile person) async {
|
||||
@@ -76,10 +87,28 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.map((PersonProfile item) => item.id == person.id ? person : item)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(people: updated));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'person',
|
||||
entityId: person.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _personPayload(person),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deletePerson(String personId) async {
|
||||
final LocalDataState current = _requireState();
|
||||
final List<RelationshipMoment> removedMoments = current.moments
|
||||
.where((RelationshipMoment moment) => moment.personId == personId)
|
||||
.toList(growable: false);
|
||||
final List<RelationshipIdea> removedIdeas = current.ideas
|
||||
.where((RelationshipIdea idea) => idea.personId == personId)
|
||||
.toList(growable: false);
|
||||
final List<ReminderRule> removedReminders = current.reminders
|
||||
.where((ReminderRule reminder) => reminder.personId == personId)
|
||||
.toList(growable: false);
|
||||
|
||||
final List<PersonProfile> people = current.people
|
||||
.where((PersonProfile person) => person.id != personId)
|
||||
.toList(growable: false);
|
||||
@@ -101,6 +130,35 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
reminders: reminders,
|
||||
),
|
||||
);
|
||||
|
||||
await _enqueueChanges(<ChangeEnvelope>[
|
||||
_buildEnvelope(
|
||||
entityType: 'person',
|
||||
entityId: personId,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
...removedMoments.map(
|
||||
(RelationshipMoment moment) => _buildEnvelope(
|
||||
entityType: 'capture',
|
||||
entityId: moment.id,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
),
|
||||
...removedIdeas.map(
|
||||
(RelationshipIdea idea) => _buildEnvelope(
|
||||
entityType: _ideaEntityType(idea.type),
|
||||
entityId: idea.id,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
),
|
||||
...removedReminders.map(
|
||||
(ReminderRule reminder) => _buildEnvelope(
|
||||
entityType: 'reminderRule',
|
||||
entityId: reminder.id,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> addMoment({
|
||||
@@ -132,6 +190,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
...current.moments,
|
||||
];
|
||||
await _setState(current.copyWith(moments: moments));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'capture',
|
||||
entityId: moment.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _momentPayload(moment),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateMoment(RelationshipMoment moment) async {
|
||||
@@ -144,6 +210,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.map((RelationshipMoment item) => item.id == moment.id ? moment : item)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(moments: updated));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'capture',
|
||||
entityId: moment.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _momentPayload(moment),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteMoment(String momentId) async {
|
||||
@@ -152,6 +226,13 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.where((RelationshipMoment moment) => moment.id != momentId)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(moments: moments));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'capture',
|
||||
entityId: momentId,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addIdea({
|
||||
@@ -180,6 +261,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
...current.ideas,
|
||||
];
|
||||
await _setState(current.copyWith(ideas: ideas));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: _ideaEntityType(idea.type),
|
||||
entityId: idea.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _ideaPayload(idea),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateIdea(RelationshipIdea idea) async {
|
||||
@@ -192,18 +281,42 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.map((RelationshipIdea item) => item.id == idea.id ? idea : item)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(ideas: ideas));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: _ideaEntityType(idea.type),
|
||||
entityId: idea.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _ideaPayload(idea),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleIdeaArchived(String ideaId) async {
|
||||
final LocalDataState current = _requireState();
|
||||
RelationshipIdea? updatedIdea;
|
||||
final List<RelationshipIdea> ideas = current.ideas
|
||||
.map(
|
||||
(RelationshipIdea item) => item.id == ideaId
|
||||
? item.copyWith(isArchived: !item.isArchived)
|
||||
: item,
|
||||
)
|
||||
.map((RelationshipIdea item) {
|
||||
if (item.id != ideaId) {
|
||||
return item;
|
||||
}
|
||||
final RelationshipIdea toggled = item.copyWith(
|
||||
isArchived: !item.isArchived,
|
||||
);
|
||||
updatedIdea = toggled;
|
||||
return toggled;
|
||||
})
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(ideas: ideas));
|
||||
if (updatedIdea != null) {
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: _ideaEntityType(updatedIdea!.type),
|
||||
entityId: updatedIdea!.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _ideaPayload(updatedIdea!),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteIdea(String ideaId) async {
|
||||
@@ -212,6 +325,13 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.where((RelationshipIdea idea) => idea.id != ideaId)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(ideas: ideas));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: _ideaEntityTypeFromId(current.ideas, ideaId),
|
||||
entityId: ideaId,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> addReminder({
|
||||
@@ -240,6 +360,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
...current.reminders,
|
||||
];
|
||||
await _setState(current.copyWith(reminders: reminders));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'reminderRule',
|
||||
entityId: reminder.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _reminderPayload(reminder),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateReminder(ReminderRule reminder) async {
|
||||
@@ -252,18 +380,40 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.map((ReminderRule item) => item.id == reminder.id ? reminder : item)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(reminders: reminders));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'reminderRule',
|
||||
entityId: reminder.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _reminderPayload(reminder),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleReminderEnabled(String reminderId) async {
|
||||
final LocalDataState current = _requireState();
|
||||
ReminderRule? updatedReminder;
|
||||
final List<ReminderRule> reminders = current.reminders
|
||||
.map(
|
||||
(ReminderRule item) => item.id == reminderId
|
||||
? item.copyWith(enabled: !item.enabled)
|
||||
: item,
|
||||
)
|
||||
.map((ReminderRule item) {
|
||||
if (item.id != reminderId) {
|
||||
return item;
|
||||
}
|
||||
final ReminderRule toggled = item.copyWith(enabled: !item.enabled);
|
||||
updatedReminder = toggled;
|
||||
return toggled;
|
||||
})
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(reminders: reminders));
|
||||
if (updatedReminder != null) {
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'reminderRule',
|
||||
entityId: updatedReminder!.id,
|
||||
op: ChangeOperation.upsert,
|
||||
payload: _reminderPayload(updatedReminder!),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteReminder(String reminderId) async {
|
||||
@@ -272,6 +422,13 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
.where((ReminderRule reminder) => reminder.id != reminderId)
|
||||
.toList(growable: false);
|
||||
await _setState(current.copyWith(reminders: reminders));
|
||||
await _enqueueChange(
|
||||
_buildEnvelope(
|
||||
entityType: 'reminderRule',
|
||||
entityId: reminderId,
|
||||
op: ChangeOperation.delete,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleTaskDone(String taskId) async {
|
||||
@@ -286,6 +443,431 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
|
||||
await _setState(current.copyWith(tasks: tasks));
|
||||
}
|
||||
|
||||
Future<void> applyRemoteChanges(List<ChangeEnvelope> changes) async {
|
||||
if (changes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDataState next = _requireState();
|
||||
for (final ChangeEnvelope change in changes) {
|
||||
next = _applyRemoteChange(next, change);
|
||||
}
|
||||
await _setState(next);
|
||||
}
|
||||
|
||||
LocalDataState _applyRemoteChange(
|
||||
LocalDataState current,
|
||||
ChangeEnvelope change,
|
||||
) {
|
||||
switch (change.entityType) {
|
||||
case 'person':
|
||||
return _applyPersonChange(current, change);
|
||||
case 'capture':
|
||||
return _applyMomentChange(current, change);
|
||||
case 'giftIdea':
|
||||
case 'eventIdea':
|
||||
return _applyIdeaChange(current, change);
|
||||
case 'reminderRule':
|
||||
return _applyReminderChange(current, change);
|
||||
default:
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
LocalDataState _applyPersonChange(
|
||||
LocalDataState current,
|
||||
ChangeEnvelope change,
|
||||
) {
|
||||
if (change.op == ChangeOperation.delete) {
|
||||
final String personId = change.entityId;
|
||||
return current.copyWith(
|
||||
people: current.people
|
||||
.where((PersonProfile person) => person.id != personId)
|
||||
.toList(growable: false),
|
||||
moments: current.moments
|
||||
.where((RelationshipMoment moment) => moment.personId != personId)
|
||||
.toList(growable: false),
|
||||
ideas: current.ideas
|
||||
.where((RelationshipIdea idea) => idea.personId != personId)
|
||||
.toList(growable: false),
|
||||
reminders: current.reminders
|
||||
.where((ReminderRule reminder) => reminder.personId != personId)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
final int existingIndex = current.people.indexWhere(
|
||||
(PersonProfile person) => person.id == change.entityId,
|
||||
);
|
||||
final PersonProfile? existing = existingIndex >= 0
|
||||
? current.people[existingIndex]
|
||||
: null;
|
||||
final Map<String, dynamic> payload = change.payload ?? <String, dynamic>{};
|
||||
final PersonProfile next = PersonProfile(
|
||||
id: change.entityId,
|
||||
name: _stringField(payload, 'name', existing?.name ?? 'Unknown'),
|
||||
relationship: _stringField(
|
||||
payload,
|
||||
'relationship',
|
||||
existing?.relationship ?? 'Relationship',
|
||||
),
|
||||
affinityScore: _intField(
|
||||
payload,
|
||||
'affinityScore',
|
||||
existing?.affinityScore ?? 70,
|
||||
),
|
||||
nextMoment: _dateField(
|
||||
payload,
|
||||
'nextMoment',
|
||||
existing?.nextMoment ?? DateTime.now().add(const Duration(days: 3)),
|
||||
),
|
||||
tags: _stringListField(payload, 'tags', existing?.tags ?? <String>[]),
|
||||
notes: _stringField(payload, 'notes', existing?.notes ?? ''),
|
||||
);
|
||||
final List<PersonProfile> people = _upsertItem(
|
||||
items: current.people,
|
||||
item: next,
|
||||
idOf: (PersonProfile person) => person.id,
|
||||
);
|
||||
return current.copyWith(people: people);
|
||||
}
|
||||
|
||||
LocalDataState _applyMomentChange(
|
||||
LocalDataState current,
|
||||
ChangeEnvelope change,
|
||||
) {
|
||||
if (change.op == ChangeOperation.delete) {
|
||||
return current.copyWith(
|
||||
moments: current.moments
|
||||
.where((RelationshipMoment moment) => moment.id != change.entityId)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
final int existingIndex = current.moments.indexWhere(
|
||||
(RelationshipMoment moment) => moment.id == change.entityId,
|
||||
);
|
||||
final RelationshipMoment? existing = existingIndex >= 0
|
||||
? current.moments[existingIndex]
|
||||
: null;
|
||||
final Map<String, dynamic> payload = change.payload ?? <String, dynamic>{};
|
||||
final String summary = _stringField(
|
||||
payload,
|
||||
'summary',
|
||||
existing?.summary ?? '',
|
||||
);
|
||||
final RelationshipMoment next = RelationshipMoment(
|
||||
id: change.entityId,
|
||||
personId: _stringField(
|
||||
payload,
|
||||
'personId',
|
||||
existing?.personId ??
|
||||
(current.people.isEmpty
|
||||
? 'person-unknown'
|
||||
: current.people.first.id),
|
||||
),
|
||||
title: _stringField(
|
||||
payload,
|
||||
'title',
|
||||
existing?.title ?? _titleFromSummary(summary),
|
||||
),
|
||||
summary: summary,
|
||||
at: _dateField(payload, 'at', existing?.at ?? DateTime.now()),
|
||||
type: _stringField(payload, 'type', existing?.type ?? 'capture'),
|
||||
);
|
||||
final List<RelationshipMoment> moments = _upsertItem(
|
||||
items: current.moments,
|
||||
item: next,
|
||||
idOf: (RelationshipMoment moment) => moment.id,
|
||||
);
|
||||
return current.copyWith(moments: moments);
|
||||
}
|
||||
|
||||
LocalDataState _applyIdeaChange(
|
||||
LocalDataState current,
|
||||
ChangeEnvelope change,
|
||||
) {
|
||||
if (change.op == ChangeOperation.delete) {
|
||||
return current.copyWith(
|
||||
ideas: current.ideas
|
||||
.where((RelationshipIdea idea) => idea.id != change.entityId)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
final int existingIndex = current.ideas.indexWhere(
|
||||
(RelationshipIdea idea) => idea.id == change.entityId,
|
||||
);
|
||||
final RelationshipIdea? existing = existingIndex >= 0
|
||||
? current.ideas[existingIndex]
|
||||
: null;
|
||||
final Map<String, dynamic> payload = change.payload ?? <String, dynamic>{};
|
||||
final IdeaType fallbackType = _ideaTypeFromEntityType(
|
||||
change.entityType,
|
||||
fallback: existing?.type ?? IdeaType.gift,
|
||||
);
|
||||
final IdeaType type = _ideaTypeFromRaw(
|
||||
payload['type'],
|
||||
fallback: fallbackType,
|
||||
);
|
||||
final RelationshipIdea next = RelationshipIdea(
|
||||
id: change.entityId,
|
||||
personId: payload['personId'] as String? ?? existing?.personId,
|
||||
type: type,
|
||||
title: _stringField(payload, 'title', existing?.title ?? 'Untitled idea'),
|
||||
details: _stringField(payload, 'details', existing?.details ?? ''),
|
||||
createdAt: _dateField(
|
||||
payload,
|
||||
'createdAt',
|
||||
existing?.createdAt ?? DateTime.now(),
|
||||
),
|
||||
isArchived: _boolField(
|
||||
payload,
|
||||
'isArchived',
|
||||
existing?.isArchived ?? false,
|
||||
),
|
||||
);
|
||||
final List<RelationshipIdea> ideas = _upsertItem(
|
||||
items: current.ideas,
|
||||
item: next,
|
||||
idOf: (RelationshipIdea idea) => idea.id,
|
||||
);
|
||||
return current.copyWith(ideas: ideas);
|
||||
}
|
||||
|
||||
LocalDataState _applyReminderChange(
|
||||
LocalDataState current,
|
||||
ChangeEnvelope change,
|
||||
) {
|
||||
if (change.op == ChangeOperation.delete) {
|
||||
return current.copyWith(
|
||||
reminders: current.reminders
|
||||
.where((ReminderRule reminder) => reminder.id != change.entityId)
|
||||
.toList(growable: false),
|
||||
);
|
||||
}
|
||||
|
||||
final int existingIndex = current.reminders.indexWhere(
|
||||
(ReminderRule reminder) => reminder.id == change.entityId,
|
||||
);
|
||||
final ReminderRule? existing = existingIndex >= 0
|
||||
? current.reminders[existingIndex]
|
||||
: null;
|
||||
final Map<String, dynamic> payload = change.payload ?? <String, dynamic>{};
|
||||
final ReminderRule next = ReminderRule(
|
||||
id: change.entityId,
|
||||
personId: payload['personId'] as String? ?? existing?.personId,
|
||||
title: _stringField(payload, 'title', existing?.title ?? 'Reminder'),
|
||||
cadence: _cadenceFromRaw(
|
||||
payload['cadence'],
|
||||
fallback: existing?.cadence ?? ReminderCadence.weekly,
|
||||
),
|
||||
nextAt: _dateField(payload, 'nextAt', existing?.nextAt ?? DateTime.now()),
|
||||
enabled: _boolField(payload, 'enabled', existing?.enabled ?? true),
|
||||
);
|
||||
final List<ReminderRule> reminders = _upsertItem(
|
||||
items: current.reminders,
|
||||
item: next,
|
||||
idOf: (ReminderRule reminder) => reminder.id,
|
||||
);
|
||||
return current.copyWith(reminders: reminders);
|
||||
}
|
||||
|
||||
Future<void> _enqueueChange(ChangeEnvelope change) async {
|
||||
await ref.read(syncQueueRepositoryProvider.notifier).enqueue(change);
|
||||
}
|
||||
|
||||
Future<void> _enqueueChanges(List<ChangeEnvelope> changes) async {
|
||||
await ref.read(syncQueueRepositoryProvider.notifier).enqueueAll(changes);
|
||||
}
|
||||
|
||||
ChangeEnvelope _buildEnvelope({
|
||||
required String entityType,
|
||||
required String entityId,
|
||||
required ChangeOperation op,
|
||||
Map<String, dynamic>? payload,
|
||||
}) {
|
||||
return ChangeEnvelope(
|
||||
schemaVersion: _syncSchemaVersion,
|
||||
entityType: entityType,
|
||||
entityId: entityId,
|
||||
op: op,
|
||||
modifiedAt: DateTime.now().toUtc(),
|
||||
clientMutationId: _uuid.v4(),
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _personPayload(PersonProfile person) {
|
||||
return <String, dynamic>{
|
||||
'name': person.name,
|
||||
'relationship': person.relationship,
|
||||
'affinityScore': person.affinityScore,
|
||||
'nextMoment': person.nextMoment.toUtc().toIso8601String(),
|
||||
'tags': person.tags,
|
||||
'notes': person.notes,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _momentPayload(RelationshipMoment moment) {
|
||||
return <String, dynamic>{
|
||||
'personId': moment.personId,
|
||||
'title': moment.title,
|
||||
'summary': moment.summary,
|
||||
'at': moment.at.toUtc().toIso8601String(),
|
||||
'type': moment.type,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _ideaPayload(RelationshipIdea idea) {
|
||||
return <String, dynamic>{
|
||||
'personId': idea.personId,
|
||||
'type': idea.type.name,
|
||||
'title': idea.title,
|
||||
'details': idea.details,
|
||||
'createdAt': idea.createdAt.toUtc().toIso8601String(),
|
||||
'isArchived': idea.isArchived,
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _reminderPayload(ReminderRule reminder) {
|
||||
return <String, dynamic>{
|
||||
'personId': reminder.personId,
|
||||
'title': reminder.title,
|
||||
'cadence': reminder.cadence.name,
|
||||
'nextAt': reminder.nextAt.toUtc().toIso8601String(),
|
||||
'enabled': reminder.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
String _ideaEntityType(IdeaType type) {
|
||||
return type == IdeaType.event ? 'eventIdea' : 'giftIdea';
|
||||
}
|
||||
|
||||
String _ideaEntityTypeFromId(List<RelationshipIdea> ideas, String ideaId) {
|
||||
final int index = ideas.indexWhere(
|
||||
(RelationshipIdea idea) => idea.id == ideaId,
|
||||
);
|
||||
final RelationshipIdea? existing = index >= 0 ? ideas[index] : null;
|
||||
if (existing == null) {
|
||||
return 'giftIdea';
|
||||
}
|
||||
return _ideaEntityType(existing.type);
|
||||
}
|
||||
|
||||
IdeaType _ideaTypeFromEntityType(
|
||||
String entityType, {
|
||||
required IdeaType fallback,
|
||||
}) {
|
||||
switch (entityType) {
|
||||
case 'eventIdea':
|
||||
return IdeaType.event;
|
||||
case 'giftIdea':
|
||||
return IdeaType.gift;
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
IdeaType _ideaTypeFromRaw(dynamic value, {required IdeaType fallback}) {
|
||||
if (value is! String || value.isEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return IdeaType.values.firstWhere(
|
||||
(IdeaType type) => type.name == value,
|
||||
orElse: () => fallback,
|
||||
);
|
||||
}
|
||||
|
||||
ReminderCadence _cadenceFromRaw(
|
||||
dynamic value, {
|
||||
required ReminderCadence fallback,
|
||||
}) {
|
||||
if (value is! String || value.isEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return ReminderCadence.values.firstWhere(
|
||||
(ReminderCadence cadence) => cadence.name == value,
|
||||
orElse: () => fallback,
|
||||
);
|
||||
}
|
||||
|
||||
List<T> _upsertItem<T>({
|
||||
required List<T> items,
|
||||
required T item,
|
||||
required String Function(T value) idOf,
|
||||
}) {
|
||||
final int index = items.indexWhere((T value) => idOf(value) == idOf(item));
|
||||
if (index < 0) {
|
||||
return <T>[item, ...items];
|
||||
}
|
||||
|
||||
final List<T> copy = items.toList();
|
||||
copy[index] = item;
|
||||
return copy;
|
||||
}
|
||||
|
||||
String _stringField(
|
||||
Map<String, dynamic> payload,
|
||||
String key,
|
||||
String fallback,
|
||||
) {
|
||||
final dynamic value = payload[key];
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
return value.trim();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
int _intField(Map<String, dynamic> payload, String key, int fallback) {
|
||||
final dynamic value = payload[key];
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool _boolField(Map<String, dynamic> payload, String key, bool fallback) {
|
||||
final dynamic value = payload[key];
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
List<String> _stringListField(
|
||||
Map<String, dynamic> payload,
|
||||
String key,
|
||||
List<String> fallback,
|
||||
) {
|
||||
final dynamic value = payload[key];
|
||||
if (value is! List<dynamic>) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return value.map((dynamic item) => '$item').toList(growable: false);
|
||||
}
|
||||
|
||||
DateTime _dateField(
|
||||
Map<String, dynamic> payload,
|
||||
String key,
|
||||
DateTime fallback,
|
||||
) {
|
||||
final dynamic value = payload[key];
|
||||
if (value is String) {
|
||||
final DateTime? parsed = DateTime.tryParse(value);
|
||||
if (parsed != null) {
|
||||
return parsed.toLocal();
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
Future<void> _setState(LocalDataState next) async {
|
||||
state = AsyncData<LocalDataState>(next);
|
||||
await _persist(next);
|
||||
|
||||
Reference in New Issue
Block a user