Add persisted local state and baseline CRUD flows

This commit is contained in:
Rijad Zuzo
2026-02-15 15:12:23 +01:00
parent d708aa58d9
commit 4472668474
12 changed files with 1324 additions and 350 deletions
+165 -95
View File
@@ -1,105 +1,175 @@
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';
class LocalRepository {
const LocalRepository();
/// 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;
List<PersonProfile> people() {
return <PersonProfile>[
PersonProfile(
id: 'p-ava',
name: 'Ava Hart',
relationship: 'Partner',
affinityScore: 92,
nextMoment: DateTime(2026, 2, 16, 19, 30),
tags: <String>['coffee', 'books', 'slow mornings'],
notes: 'Prefers thoughtful plans over expensive plans.',
),
PersonProfile(
id: 'p-jordan',
name: 'Jordan Lee',
relationship: 'Close Friend',
affinityScore: 78,
nextMoment: DateTime(2026, 2, 18, 18, 0),
tags: <String>['running', 'street food'],
notes: 'Has a race on Sunday; ask how training is going.',
),
PersonProfile(
id: 'p-mila',
name: 'Mila Stone',
relationship: 'Sister',
affinityScore: 84,
nextMoment: DateTime(2026, 2, 20, 12, 0),
tags: <String>['plants', 'retro music'],
notes: 'Birthday prep should start this week.',
),
];
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;
}
}
List<RelationshipMoment> moments() {
return <RelationshipMoment>[
RelationshipMoment(
id: 'm-1',
personId: 'p-ava',
title: 'Sunday Walk Tradition',
summary: 'Short walk + no-phone hour worked really well.',
at: DateTime(2026, 2, 9, 9, 30),
type: 'ritual',
),
RelationshipMoment(
id: 'm-2',
personId: 'p-jordan',
title: 'Post-workout Check-in',
summary: 'Shared training playlist and grabbed smoothies.',
at: DateTime(2026, 2, 11, 19, 0),
type: 'support',
),
RelationshipMoment(
id: 'm-3',
personId: 'p-mila',
title: 'Gift Idea Captured',
summary: 'Found a vintage lamp from her saved style board.',
at: DateTime(2026, 2, 12, 14, 20),
type: 'gift',
),
];
}
List<DashboardTask> tasks() {
return <DashboardTask>[
DashboardTask(
title: 'Plan Friday dinner with Ava',
description: 'Keep it low-key: ramen + bookstore stop.',
when: DateTime(2026, 2, 16, 17, 0),
),
DashboardTask(
title: 'Send race-day encouragement to Jordan',
description: 'Voice note before 8:00 AM.',
when: DateTime(2026, 2, 17, 7, 30),
),
DashboardTask(
title: 'Finalize Mila birthday shortlist',
description: 'Pick 1 meaningful gift and 1 backup.',
when: DateTime(2026, 2, 18, 20, 0),
),
];
}
DashboardSummary summary() {
return DashboardSummary(
activePeople: people().length,
upcomingPlans: people()
.where(
(PersonProfile p) => p.nextMoment.isAfter(DateTime(2026, 2, 14)),
)
.length,
pendingIdeas: 6,
weeklyConsistency: 82,
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 Provider<LocalRepository> localRepositoryProvider =
Provider<LocalRepository>((Ref ref) {
return const LocalRepository();
});
final AsyncNotifierProvider<LocalRepository, LocalDataState>
localRepositoryProvider =
AsyncNotifierProvider<LocalRepository, LocalDataState>(LocalRepository.new);