176 lines
5.5 KiB
Dart
176 lines
5.5 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 = 1;
|
|
|
|
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 LocalDataState current = _requireState();
|
|
final PersonProfile person = PersonProfile(
|
|
id: 'p-${_uuid.v4()}',
|
|
name: name.trim(),
|
|
relationship: relationship.trim(),
|
|
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 {
|
|
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);
|
|
|
|
await _setState(current.copyWith(people: people, moments: moments));
|
|
}
|
|
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
final String title = _titleFromSummary(normalized);
|
|
final RelationshipMoment moment = RelationshipMoment(
|
|
id: 'm-${_uuid.v4()}',
|
|
personId: personId,
|
|
title: title,
|
|
summary: normalized,
|
|
at: DateTime.now(),
|
|
type: type,
|
|
);
|
|
|
|
final List<RelationshipMoment> moments = <RelationshipMoment>[
|
|
moment,
|
|
...current.moments,
|
|
];
|
|
await _setState(current.copyWith(moments: moments));
|
|
}
|
|
|
|
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> 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);
|