334 lines
11 KiB
Dart
334 lines
11 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:relationship_saver/features/local/local_models.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
/// Persisted local data source for offline-first product state.
|
|
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 Uuid _uuid = Uuid();
|
|
|
|
@override
|
|
Future<LocalDataState> build() async {
|
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
await _migrateIfNeeded(prefs);
|
|
|
|
final String? raw = prefs.getString(_storageKey);
|
|
if (raw == null || raw.isEmpty) {
|
|
final LocalDataState seeded = LocalDataState.seed();
|
|
await _persist(seeded);
|
|
return seeded;
|
|
}
|
|
|
|
try {
|
|
final Map<String, dynamic> json = jsonDecode(raw) as Map<String, dynamic>;
|
|
return LocalDataState.fromJson(json);
|
|
} on FormatException {
|
|
final LocalDataState fallback = LocalDataState.seed();
|
|
await _persist(fallback);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
Future<void> addPerson({
|
|
required String name,
|
|
required String relationship,
|
|
required String notes,
|
|
required List<String> tags,
|
|
DateTime? nextMoment,
|
|
}) async {
|
|
final String normalizedName = name.trim();
|
|
final String normalizedRelationship = relationship.trim();
|
|
if (normalizedName.isEmpty || normalizedRelationship.isEmpty) {
|
|
throw ArgumentError('Name and relationship are required');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final PersonProfile person = PersonProfile(
|
|
id: 'p-${_uuid.v4()}',
|
|
name: normalizedName,
|
|
relationship: normalizedRelationship,
|
|
affinityScore: 75,
|
|
nextMoment: nextMoment ?? DateTime.now().add(const Duration(days: 3)),
|
|
tags: tags,
|
|
notes: notes.trim(),
|
|
);
|
|
|
|
final List<PersonProfile> people = <PersonProfile>[
|
|
person,
|
|
...current.people,
|
|
];
|
|
await _setState(current.copyWith(people: people));
|
|
}
|
|
|
|
Future<void> updatePerson(PersonProfile person) async {
|
|
if (person.name.trim().isEmpty || person.relationship.trim().isEmpty) {
|
|
throw ArgumentError('Name and relationship are required');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final List<PersonProfile> updated = current.people
|
|
.map((PersonProfile item) => item.id == person.id ? person : item)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(people: updated));
|
|
}
|
|
|
|
Future<void> deletePerson(String personId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<PersonProfile> people = current.people
|
|
.where((PersonProfile person) => person.id != personId)
|
|
.toList(growable: false);
|
|
final List<RelationshipMoment> moments = current.moments
|
|
.where((RelationshipMoment moment) => moment.personId != personId)
|
|
.toList(growable: false);
|
|
final List<RelationshipIdea> ideas = current.ideas
|
|
.where((RelationshipIdea idea) => idea.personId != personId)
|
|
.toList(growable: false);
|
|
final List<ReminderRule> reminders = current.reminders
|
|
.where((ReminderRule reminder) => reminder.personId != personId)
|
|
.toList(growable: false);
|
|
|
|
await _setState(
|
|
current.copyWith(
|
|
people: people,
|
|
moments: moments,
|
|
ideas: ideas,
|
|
reminders: reminders,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> addMoment({
|
|
required String personId,
|
|
required String summary,
|
|
String type = 'capture',
|
|
}) async {
|
|
final LocalDataState current = _requireState();
|
|
final String normalized = summary.trim();
|
|
if (normalized.isEmpty) {
|
|
throw ArgumentError('Capture summary cannot be empty');
|
|
}
|
|
|
|
final String payload = normalized.length > 280
|
|
? normalized.substring(0, 280)
|
|
: normalized;
|
|
final String title = _titleFromSummary(payload);
|
|
final RelationshipMoment moment = RelationshipMoment(
|
|
id: 'm-${_uuid.v4()}',
|
|
personId: personId,
|
|
title: title,
|
|
summary: payload,
|
|
at: DateTime.now(),
|
|
type: type,
|
|
);
|
|
|
|
final List<RelationshipMoment> moments = <RelationshipMoment>[
|
|
moment,
|
|
...current.moments,
|
|
];
|
|
await _setState(current.copyWith(moments: moments));
|
|
}
|
|
|
|
Future<void> updateMoment(RelationshipMoment moment) async {
|
|
if (moment.summary.trim().isEmpty) {
|
|
throw ArgumentError('Capture summary cannot be empty');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final List<RelationshipMoment> updated = current.moments
|
|
.map((RelationshipMoment item) => item.id == moment.id ? moment : item)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(moments: updated));
|
|
}
|
|
|
|
Future<void> deleteMoment(String momentId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<RelationshipMoment> moments = current.moments
|
|
.where((RelationshipMoment moment) => moment.id != momentId)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(moments: moments));
|
|
}
|
|
|
|
Future<void> addIdea({
|
|
required IdeaType type,
|
|
required String title,
|
|
required String details,
|
|
String? personId,
|
|
}) async {
|
|
final String normalizedTitle = title.trim();
|
|
if (normalizedTitle.isEmpty) {
|
|
throw ArgumentError('Idea title is required');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final RelationshipIdea idea = RelationshipIdea(
|
|
id: 'i-${_uuid.v4()}',
|
|
personId: personId,
|
|
type: type,
|
|
title: normalizedTitle,
|
|
details: details.trim(),
|
|
createdAt: DateTime.now(),
|
|
);
|
|
|
|
final List<RelationshipIdea> ideas = <RelationshipIdea>[
|
|
idea,
|
|
...current.ideas,
|
|
];
|
|
await _setState(current.copyWith(ideas: ideas));
|
|
}
|
|
|
|
Future<void> updateIdea(RelationshipIdea idea) async {
|
|
if (idea.title.trim().isEmpty) {
|
|
throw ArgumentError('Idea title is required');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final List<RelationshipIdea> ideas = current.ideas
|
|
.map((RelationshipIdea item) => item.id == idea.id ? idea : item)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(ideas: ideas));
|
|
}
|
|
|
|
Future<void> toggleIdeaArchived(String ideaId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<RelationshipIdea> ideas = current.ideas
|
|
.map(
|
|
(RelationshipIdea item) => item.id == ideaId
|
|
? item.copyWith(isArchived: !item.isArchived)
|
|
: item,
|
|
)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(ideas: ideas));
|
|
}
|
|
|
|
Future<void> deleteIdea(String ideaId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<RelationshipIdea> ideas = current.ideas
|
|
.where((RelationshipIdea idea) => idea.id != ideaId)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(ideas: ideas));
|
|
}
|
|
|
|
Future<void> addReminder({
|
|
required String title,
|
|
required ReminderCadence cadence,
|
|
required DateTime nextAt,
|
|
String? personId,
|
|
}) async {
|
|
final String normalizedTitle = title.trim();
|
|
if (normalizedTitle.isEmpty) {
|
|
throw ArgumentError('Reminder title is required');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final ReminderRule reminder = ReminderRule(
|
|
id: 'r-${_uuid.v4()}',
|
|
personId: personId,
|
|
title: normalizedTitle,
|
|
cadence: cadence,
|
|
nextAt: nextAt,
|
|
enabled: true,
|
|
);
|
|
|
|
final List<ReminderRule> reminders = <ReminderRule>[
|
|
reminder,
|
|
...current.reminders,
|
|
];
|
|
await _setState(current.copyWith(reminders: reminders));
|
|
}
|
|
|
|
Future<void> updateReminder(ReminderRule reminder) async {
|
|
if (reminder.title.trim().isEmpty) {
|
|
throw ArgumentError('Reminder title is required');
|
|
}
|
|
|
|
final LocalDataState current = _requireState();
|
|
final List<ReminderRule> reminders = current.reminders
|
|
.map((ReminderRule item) => item.id == reminder.id ? reminder : item)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(reminders: reminders));
|
|
}
|
|
|
|
Future<void> toggleReminderEnabled(String reminderId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<ReminderRule> reminders = current.reminders
|
|
.map(
|
|
(ReminderRule item) => item.id == reminderId
|
|
? item.copyWith(enabled: !item.enabled)
|
|
: item,
|
|
)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(reminders: reminders));
|
|
}
|
|
|
|
Future<void> deleteReminder(String reminderId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<ReminderRule> reminders = current.reminders
|
|
.where((ReminderRule reminder) => reminder.id != reminderId)
|
|
.toList(growable: false);
|
|
await _setState(current.copyWith(reminders: reminders));
|
|
}
|
|
|
|
Future<void> toggleTaskDone(String taskId) async {
|
|
final LocalDataState current = _requireState();
|
|
final List<DashboardTask> tasks = current.tasks
|
|
.map(
|
|
(DashboardTask task) =>
|
|
task.id == taskId ? task.copyWith(done: !task.done) : task,
|
|
)
|
|
.toList(growable: false);
|
|
|
|
await _setState(current.copyWith(tasks: tasks));
|
|
}
|
|
|
|
Future<void> _setState(LocalDataState next) async {
|
|
state = AsyncData<LocalDataState>(next);
|
|
await _persist(next);
|
|
}
|
|
|
|
LocalDataState _requireState() {
|
|
final LocalDataState? value = state.asData?.value;
|
|
if (value == null) {
|
|
throw StateError('Local data state is not ready yet');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
Future<void> _persist(LocalDataState state) async {
|
|
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_storageKey, jsonEncode(state.toJson()));
|
|
await prefs.setInt(_schemaVersionKey, _schemaVersion);
|
|
}
|
|
|
|
Future<void> _migrateIfNeeded(SharedPreferences prefs) async {
|
|
final int currentVersion = prefs.getInt(_schemaVersionKey) ?? 0;
|
|
if (currentVersion == _schemaVersion) {
|
|
return;
|
|
}
|
|
|
|
if (currentVersion <= 0) {
|
|
await prefs.remove(_storageKey);
|
|
await prefs.setInt(_schemaVersionKey, _schemaVersion);
|
|
return;
|
|
}
|
|
|
|
// Migration placeholder for future schema versions.
|
|
await prefs.setInt(_schemaVersionKey, _schemaVersion);
|
|
}
|
|
|
|
String _titleFromSummary(String summary) {
|
|
final List<String> words = summary.split(RegExp(r'\s+'));
|
|
final int take = words.length < 5 ? words.length : 5;
|
|
return words.take(take).join(' ');
|
|
}
|
|
}
|
|
|
|
final AsyncNotifierProvider<LocalRepository, LocalDataState>
|
|
localRepositoryProvider =
|
|
AsyncNotifierProvider<LocalRepository, LocalDataState>(LocalRepository.new);
|