f655adfbea
Migrate app code into canonical feature slices, add phone-only AI digest scheduling and review, wire local notification/background task support, and cover the flow with tests.
381 lines
14 KiB
Dart
381 lines
14 KiB
Dart
// ignore_for_file: sort_constructors_first
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
|
import 'package:relationship_saver/features/dashboard/domain/dashboard_models.dart';
|
|
import 'package:relationship_saver/features/ideas/domain/idea_models.dart';
|
|
import 'package:relationship_saver/features/moments/domain/moment_models.dart';
|
|
import 'package:relationship_saver/features/people/domain/person_models.dart';
|
|
import 'package:relationship_saver/features/reminders/domain/reminder_models.dart';
|
|
import 'package:relationship_saver/features/share_intake/domain/share_models.dart';
|
|
|
|
/// Aggregate local snapshot used by the prototype app store.
|
|
///
|
|
/// During this prototyping phase the app still uses one persisted snapshot for
|
|
/// most product state. The types inside it are now feature-owned, so each slice
|
|
/// can evolve without hiding its models in a horizontal "local" module.
|
|
@immutable
|
|
class LocalDataState {
|
|
const LocalDataState({
|
|
required this.people,
|
|
required this.moments,
|
|
required this.ideas,
|
|
required this.reminders,
|
|
required this.tasks,
|
|
this.dismissedSignalIds = const <String>[],
|
|
this.sourceLinks = const <SourceProfileLink>[],
|
|
this.sharedMessages = const <SharedMessageEntry>[],
|
|
this.sharedInbox = const <SharedInboxEntry>[],
|
|
this.personFacts = const <PersonFact>[],
|
|
this.importantDates = const <PersonImportantDate>[],
|
|
this.preferenceSignals = const <PersonPreferenceSignal>[],
|
|
this.aiSuggestionDrafts = const <AiSuggestionDraft>[],
|
|
});
|
|
|
|
final List<PersonProfile> people;
|
|
final List<RelationshipMoment> moments;
|
|
final List<RelationshipIdea> ideas;
|
|
final List<ReminderRule> reminders;
|
|
final List<DashboardTask> tasks;
|
|
final List<String> dismissedSignalIds;
|
|
final List<SourceProfileLink> sourceLinks;
|
|
final List<SharedMessageEntry> sharedMessages;
|
|
final List<SharedInboxEntry> sharedInbox;
|
|
final List<PersonFact> personFacts;
|
|
final List<PersonImportantDate> importantDates;
|
|
final List<PersonPreferenceSignal> preferenceSignals;
|
|
final List<AiSuggestionDraft> aiSuggestionDrafts;
|
|
|
|
LocalDataState copyWith({
|
|
List<PersonProfile>? people,
|
|
List<RelationshipMoment>? moments,
|
|
List<RelationshipIdea>? ideas,
|
|
List<ReminderRule>? reminders,
|
|
List<DashboardTask>? tasks,
|
|
List<String>? dismissedSignalIds,
|
|
List<SourceProfileLink>? sourceLinks,
|
|
List<SharedMessageEntry>? sharedMessages,
|
|
List<SharedInboxEntry>? sharedInbox,
|
|
List<PersonFact>? personFacts,
|
|
List<PersonImportantDate>? importantDates,
|
|
List<PersonPreferenceSignal>? preferenceSignals,
|
|
List<AiSuggestionDraft>? aiSuggestionDrafts,
|
|
}) {
|
|
return LocalDataState(
|
|
people: people ?? this.people,
|
|
moments: moments ?? this.moments,
|
|
ideas: ideas ?? this.ideas,
|
|
reminders: reminders ?? this.reminders,
|
|
tasks: tasks ?? this.tasks,
|
|
dismissedSignalIds: dismissedSignalIds ?? this.dismissedSignalIds,
|
|
sourceLinks: sourceLinks ?? this.sourceLinks,
|
|
sharedMessages: sharedMessages ?? this.sharedMessages,
|
|
sharedInbox: sharedInbox ?? this.sharedInbox,
|
|
personFacts: personFacts ?? this.personFacts,
|
|
importantDates: importantDates ?? this.importantDates,
|
|
preferenceSignals: preferenceSignals ?? this.preferenceSignals,
|
|
aiSuggestionDrafts: aiSuggestionDrafts ?? this.aiSuggestionDrafts,
|
|
);
|
|
}
|
|
|
|
DashboardSummary summary({DateTime? now}) {
|
|
final DateTime baseline = now ?? DateTime.now();
|
|
return DashboardSummary(
|
|
activePeople: people.length,
|
|
upcomingPlans: people
|
|
.where((PersonProfile p) => p.nextMoment.isAfter(baseline))
|
|
.length,
|
|
pendingIdeas: ideas
|
|
.where((RelationshipIdea idea) => !idea.isArchived)
|
|
.length,
|
|
weeklyConsistency: moments.isEmpty
|
|
? 0
|
|
: (70 + moments.length * 4).clamp(0, 100),
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return <String, dynamic>{
|
|
'people': people
|
|
.map((PersonProfile person) => person.toJson())
|
|
.toList(growable: false),
|
|
'moments': moments
|
|
.map((RelationshipMoment moment) => moment.toJson())
|
|
.toList(growable: false),
|
|
'ideas': ideas
|
|
.map((RelationshipIdea idea) => idea.toJson())
|
|
.toList(growable: false),
|
|
'reminders': reminders
|
|
.map((ReminderRule reminder) => reminder.toJson())
|
|
.toList(growable: false),
|
|
'tasks': tasks
|
|
.map((DashboardTask task) => task.toJson())
|
|
.toList(growable: false),
|
|
'dismissedSignalIds': dismissedSignalIds,
|
|
'sourceLinks': sourceLinks
|
|
.map((SourceProfileLink link) => link.toJson())
|
|
.toList(growable: false),
|
|
'sharedMessages': sharedMessages
|
|
.map((SharedMessageEntry entry) => entry.toJson())
|
|
.toList(growable: false),
|
|
'sharedInbox': sharedInbox
|
|
.map((SharedInboxEntry entry) => entry.toJson())
|
|
.toList(growable: false),
|
|
'personFacts': personFacts
|
|
.map((PersonFact fact) => fact.toJson())
|
|
.toList(growable: false),
|
|
'importantDates': importantDates
|
|
.map((PersonImportantDate value) => value.toJson())
|
|
.toList(growable: false),
|
|
'preferenceSignals': preferenceSignals
|
|
.map((PersonPreferenceSignal signal) => signal.toJson())
|
|
.toList(growable: false),
|
|
'aiSuggestionDrafts': aiSuggestionDrafts
|
|
.map((AiSuggestionDraft draft) => draft.toJson())
|
|
.toList(growable: false),
|
|
};
|
|
}
|
|
|
|
factory LocalDataState.fromJson(Map<String, dynamic> json) {
|
|
return LocalDataState(
|
|
people: (json['people'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic person) =>
|
|
PersonProfile.fromJson(person as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
moments: (json['moments'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic moment) =>
|
|
RelationshipMoment.fromJson(moment as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
ideas: (json['ideas'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic idea) =>
|
|
RelationshipIdea.fromJson(idea as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
reminders: (json['reminders'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic reminder) =>
|
|
ReminderRule.fromJson(reminder as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
tasks: (json['tasks'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic task) =>
|
|
DashboardTask.fromJson(task as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
dismissedSignalIds:
|
|
(json['dismissedSignalIds'] as List<dynamic>? ?? <dynamic>[])
|
|
.map((dynamic value) => '$value')
|
|
.toList(growable: false),
|
|
sourceLinks: (json['sourceLinks'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic link) =>
|
|
SourceProfileLink.fromJson(link as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
sharedMessages: (json['sharedMessages'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic entry) =>
|
|
SharedMessageEntry.fromJson(entry as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
sharedInbox: (json['sharedInbox'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic entry) =>
|
|
SharedInboxEntry.fromJson(entry as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
personFacts: (json['personFacts'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic fact) => PersonFact.fromJson(fact as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
importantDates: (json['importantDates'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic value) =>
|
|
PersonImportantDate.fromJson(value as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
preferenceSignals:
|
|
(json['preferenceSignals'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic signal) => PersonPreferenceSignal.fromJson(
|
|
signal as Map<String, dynamic>,
|
|
),
|
|
)
|
|
.toList(growable: false),
|
|
aiSuggestionDrafts:
|
|
(json['aiSuggestionDrafts'] as List<dynamic>? ?? <dynamic>[])
|
|
.map(
|
|
(dynamic draft) =>
|
|
AiSuggestionDraft.fromJson(draft as Map<String, dynamic>),
|
|
)
|
|
.toList(growable: false),
|
|
);
|
|
}
|
|
|
|
static LocalDataState seed() {
|
|
return LocalDataState(
|
|
people: <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.',
|
|
aliases: <String>['Aves'],
|
|
location: 'Austin, TX',
|
|
lastUpdatedAt: DateTime(2026, 2, 14, 9, 0),
|
|
lastInteractedAt: DateTime(2026, 2, 12, 18, 30),
|
|
),
|
|
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.',
|
|
location: 'Chicago, IL',
|
|
lastUpdatedAt: DateTime(2026, 2, 13, 8, 30),
|
|
lastInteractedAt: DateTime(2026, 2, 11, 19, 0),
|
|
),
|
|
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.',
|
|
aliases: <String>['Millie'],
|
|
location: 'Seattle, WA',
|
|
lastUpdatedAt: DateTime(2026, 2, 13, 14, 20),
|
|
lastInteractedAt: DateTime(2026, 2, 12, 14, 20),
|
|
),
|
|
],
|
|
moments: <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',
|
|
),
|
|
],
|
|
ideas: <RelationshipIdea>[
|
|
RelationshipIdea(
|
|
id: 'i-1',
|
|
personId: 'p-ava',
|
|
type: IdeaType.gift,
|
|
title: 'Handwritten recipe journal',
|
|
details: 'Collect 10 memories and recipes from this year.',
|
|
createdAt: DateTime(2026, 2, 12, 9),
|
|
),
|
|
RelationshipIdea(
|
|
id: 'i-2',
|
|
personId: 'p-mila',
|
|
type: IdeaType.event,
|
|
title: 'Plant market + brunch date',
|
|
details: 'Saturday morning slot; book nearby cafe in advance.',
|
|
createdAt: DateTime(2026, 2, 13, 11),
|
|
),
|
|
],
|
|
reminders: <ReminderRule>[
|
|
ReminderRule(
|
|
id: 'r-1',
|
|
personId: 'p-jordan',
|
|
title: 'Weekly check-in message',
|
|
cadence: ReminderCadence.weekly,
|
|
nextAt: DateTime(2026, 2, 18, 18),
|
|
),
|
|
ReminderRule(
|
|
id: 'r-2',
|
|
personId: 'p-ava',
|
|
title: 'Sunday ritual planning',
|
|
cadence: ReminderCadence.weekly,
|
|
nextAt: DateTime(2026, 2, 16, 10),
|
|
),
|
|
],
|
|
tasks: <DashboardTask>[
|
|
DashboardTask(
|
|
id: 't-1',
|
|
title: 'Plan Friday dinner with Ava',
|
|
description: 'Keep it low-key: ramen + bookstore stop.',
|
|
when: DateTime(2026, 2, 16, 17, 0),
|
|
),
|
|
DashboardTask(
|
|
id: 't-2',
|
|
title: 'Send race-day encouragement to Jordan',
|
|
description: 'Voice note before 8:00 AM.',
|
|
when: DateTime(2026, 2, 17, 7, 30),
|
|
),
|
|
DashboardTask(
|
|
id: 't-3',
|
|
title: 'Finalize Mila birthday shortlist',
|
|
description: 'Pick 1 meaningful gift and 1 backup.',
|
|
when: DateTime(2026, 2, 18, 20, 0),
|
|
),
|
|
],
|
|
personFacts: <PersonFact>[
|
|
PersonFact(
|
|
id: 'pf-1',
|
|
personId: 'p-ava',
|
|
type: CapturedFactType.like,
|
|
text: 'Quiet brunch spots and recipe-book stores',
|
|
label: 'Weekend preference',
|
|
sourceKind: CaptureSourceKind.manual,
|
|
createdAt: DateTime(2026, 2, 10, 8, 0),
|
|
updatedAt: DateTime(2026, 2, 10, 8, 0),
|
|
),
|
|
PersonFact(
|
|
id: 'pf-2',
|
|
personId: 'p-mila',
|
|
type: CapturedFactType.giftIdea,
|
|
text: 'Vintage ceramic planter in muted green',
|
|
label: 'Gift lead',
|
|
sourceKind: CaptureSourceKind.manual,
|
|
createdAt: DateTime(2026, 2, 12, 14, 25),
|
|
updatedAt: DateTime(2026, 2, 12, 14, 25),
|
|
),
|
|
],
|
|
importantDates: <PersonImportantDate>[
|
|
PersonImportantDate(
|
|
id: 'pd-1',
|
|
personId: 'p-mila',
|
|
label: 'Birthday',
|
|
date: DateTime(2026, 3, 3),
|
|
classification: 'birthday',
|
|
sourceKind: CaptureSourceKind.manual,
|
|
createdAt: DateTime(2026, 2, 12, 14, 30),
|
|
updatedAt: DateTime(2026, 2, 12, 14, 30),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|