Add removable mock mode for a clickable preview
Runs the whole app on seeded in-memory data with no backend or device storage, behind useMockData in lib/Grounded/mock/MockSwitch.dart. To remove: delete lib/Grounded/mock/ and the lines tagged "MOCK:" in ParentViewModel (or revert this commit, keeping the notification guard). Also skips local notifications on web, where the plugin and dart:io Platform both throw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
import '../about/external/data/Commitment.dart';
|
||||
@@ -32,7 +33,7 @@ class LocalNotificationEngine {
|
||||
static bool _ready = false;
|
||||
|
||||
static Future<void> init() async {
|
||||
if (_ready) {
|
||||
if (_ready || kIsWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,6 +106,10 @@ class LocalNotificationEngine {
|
||||
/// The ongoing notification for a running task. Shows the live remaining
|
||||
/// time so it is useful from the lock screen without unlocking.
|
||||
static Future<void> showSessionNotification(LiveSession session) async {
|
||||
// The browser has no notification channels; the runner works without.
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
await init();
|
||||
|
||||
final String remaining = session.requiredSeconds > 0
|
||||
@@ -144,6 +149,10 @@ class LocalNotificationEngine {
|
||||
}
|
||||
|
||||
static Future<void> cancelSessionNotification() async {
|
||||
// The browser has no notification channels; the runner works without.
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
await init();
|
||||
await plugin.cancel(id: sessionNotificationId);
|
||||
}
|
||||
@@ -156,6 +165,9 @@ class LocalNotificationEngine {
|
||||
EscalationTier tier,
|
||||
ToneLevel tone,
|
||||
) async {
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
await init();
|
||||
|
||||
final bool nonNegotiable =
|
||||
@@ -198,6 +210,10 @@ class LocalNotificationEngine {
|
||||
}
|
||||
|
||||
static Future<void> cancelAll() async {
|
||||
// The browser has no notification channels; the runner works without.
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
await init();
|
||||
await plugin.cancelAll();
|
||||
}
|
||||
|
||||
443
frontend/lib/Grounded/mock/MockComms.dart
Normal file
443
frontend/lib/Grounded/mock/MockComms.dart
Normal file
@@ -0,0 +1,443 @@
|
||||
// MOCK DATA — TEMPORARY. See MockSwitch.dart.
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../about/external/data/RoutineChain.dart';
|
||||
import '../about/external/data/pages/request/CommitmentsRequest.dart';
|
||||
import '../about/external/data/pages/request/HistoryRequest.dart';
|
||||
import '../about/external/initial/AbandonRequest.dart';
|
||||
import '../about/external/initial/AmnestyRequest.dart';
|
||||
import '../about/external/initial/CommitmentRequest.dart';
|
||||
import '../about/external/initial/CompletionRequest.dart';
|
||||
import '../about/external/initial/DeferralRequest.dart';
|
||||
import '../about/external/initial/DeviceRequest.dart';
|
||||
import '../about/external/initial/GoalRequest.dart';
|
||||
import '../about/external/initial/IdRequest.dart';
|
||||
import '../about/external/initial/LoginData.dart';
|
||||
import '../about/external/initial/ReportCardRequest.dart';
|
||||
import '../about/external/initial/SessionLogRequest.dart';
|
||||
import '../about/external/initial/SickModeRequest.dart';
|
||||
import '../about/external/initial/ToneRequest.dart';
|
||||
import '../comms/ConnectComms.dart';
|
||||
import 'MockStore.dart';
|
||||
|
||||
/// A fake backend. Answers every call from [MockStore] after a short delay,
|
||||
/// so loading states still flash the way they will against the real thing.
|
||||
class MockComms implements ConnectComms {
|
||||
MockComms() {
|
||||
MockStore.ensureSeeded();
|
||||
}
|
||||
|
||||
Future<Response> _ok(dynamic data) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 250));
|
||||
return Response(
|
||||
requestOptions: RequestOptions(path: 'mock'),
|
||||
statusCode: 200,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> _done() => _ok(<String, dynamic>{'success': true});
|
||||
|
||||
Future<Response> _emptyPage() => _ok(MockStore.page(<Map<String, dynamic>>[]));
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> registerDevice(DeviceRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> registerDeviceToken(String token) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> login(LoginData request) => _ok(<String, dynamic>{
|
||||
'id': 'mock-user',
|
||||
'name': 'Alex',
|
||||
'token': 'mock-token',
|
||||
'pic': '',
|
||||
'tone': 'Firm',
|
||||
'standing': 'Good',
|
||||
'amnestyTokens': 3,
|
||||
'sickMode': false,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Response> logout() => _done();
|
||||
|
||||
@override
|
||||
Future<Response> me() => _ok(<String, dynamic>{'id': 'mock-user', 'name': 'Alex'});
|
||||
|
||||
// ── Goals ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getMyGoals(HistoryRequest request) =>
|
||||
_ok(MockStore.goalsWithCounts());
|
||||
|
||||
@override
|
||||
Future<Response> saveGoalEntry(GoalRequest request) {
|
||||
final Map<String, dynamic> json = request.toJson();
|
||||
json['targetDate'] = request.targetDate.isEmpty ? null : request.targetDate;
|
||||
json['archived'] = false;
|
||||
|
||||
final int index =
|
||||
MockStore.goals.indexWhere((goal) => goal['id'] == request.id);
|
||||
if (request.id == null || index < 0) {
|
||||
json['id'] = MockStore.newId('goal');
|
||||
MockStore.goals.add(json);
|
||||
} else {
|
||||
MockStore.goals[index] = json;
|
||||
}
|
||||
return _ok(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getGoalTasks(IdRequest request) =>
|
||||
_ok(MockStore.goalTasks(request.id));
|
||||
|
||||
@override
|
||||
Future<Response> archiveGoalEntry(IdRequest request) {
|
||||
for (Map<String, dynamic> goal in MockStore.goals) {
|
||||
if (goal['id'] == request.id) {
|
||||
goal['archived'] = true;
|
||||
}
|
||||
}
|
||||
return _done();
|
||||
}
|
||||
|
||||
// ── Commitments ───────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getTodayPlan(CommitmentsRequest request) =>
|
||||
_ok(MockStore.page(MockStore.todayPlan()));
|
||||
|
||||
@override
|
||||
Future<Response> getMyCommitments(CommitmentsRequest request) =>
|
||||
_ok(MockStore.page(MockStore.commitments));
|
||||
|
||||
@override
|
||||
Future<Response> getOverdueQueue(HistoryRequest request) =>
|
||||
_ok(MockStore.page(MockStore.overdue()));
|
||||
|
||||
@override
|
||||
Future<Response> saveCommitmentEntry(CommitmentRequest request) {
|
||||
final Map<String, dynamic> json = request.toJson();
|
||||
json['id'] = MockStore.newId('task');
|
||||
json['status'] = 'Open';
|
||||
json['deferralCount'] = 0;
|
||||
MockStore.commitments.add(json);
|
||||
return _ok(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> updateCommitmentEntry(CommitmentRequest request) {
|
||||
final Map<String, dynamic>? item = MockStore.commitment(request.id ?? '');
|
||||
if (item != null) {
|
||||
item.addAll(request.toJson());
|
||||
}
|
||||
return _done();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> deleteCommitmentEntry(IdRequest request) {
|
||||
MockStore.commitments.removeWhere((item) => item['id'] == request.id);
|
||||
return _done();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> completeCommitmentEntry(CompletionRequest request) {
|
||||
final Map<String, dynamic>? item = MockStore.commitment(request.commitmentId);
|
||||
if (item != null) {
|
||||
final DateTime? end = DateTime.tryParse(item['dueEnd'] ?? '');
|
||||
item['status'] = end != null && end.isBefore(DateTime.now())
|
||||
? 'LateCompleted'
|
||||
: 'Completed';
|
||||
}
|
||||
return _done();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> deferCommitmentEntry(DeferralRequest request) {
|
||||
final Map<String, dynamic>? item = MockStore.commitment(request.commitmentId);
|
||||
if (item != null) {
|
||||
final DateTime now = DateTime.now();
|
||||
item['dueStart'] = request.newDueStart.isNotEmpty
|
||||
? request.newDueStart
|
||||
: now.add(const Duration(hours: 1)).toIso8601String();
|
||||
item['dueEnd'] = request.newDueEnd.isNotEmpty
|
||||
? request.newDueEnd
|
||||
: now.add(const Duration(days: 1)).toIso8601String();
|
||||
item['deferralCount'] = (item['deferralCount'] ?? 0) + 1;
|
||||
item['status'] = 'Deferred';
|
||||
}
|
||||
return _done();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> abandonCommitmentEntry(AbandonRequest request) {
|
||||
final Map<String, dynamic>? item = MockStore.commitment(request.commitmentId);
|
||||
if (item != null) {
|
||||
item['status'] = 'Abandoned';
|
||||
}
|
||||
return _done();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getCommitmentHistory(HistoryRequest request) => _emptyPage();
|
||||
|
||||
@override
|
||||
Future<Response> getCommitmentEvents(IdRequest request) => _emptyPage();
|
||||
|
||||
// ── Capacity ──────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> checkCapacity(CommitmentsRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> getCapacityProfileEntry() => _ok(<String, dynamic>{});
|
||||
|
||||
// ── Debt & standing ───────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getDebtSummary() => _ok(<String, dynamic>{});
|
||||
|
||||
@override
|
||||
Future<Response> getDebtLedger(HistoryRequest request) => _emptyPage();
|
||||
|
||||
@override
|
||||
Future<Response> getDebtTrend(ReportCardRequest request) => _ok(<dynamic>[]);
|
||||
|
||||
@override
|
||||
Future<Response> getStanding() => _ok(<String, dynamic>{'standing': 'Good'});
|
||||
|
||||
@override
|
||||
Future<Response> getStandingHistory(HistoryRequest request) => _emptyPage();
|
||||
|
||||
// ── Excuses ───────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getExcuseClusters(ReportCardRequest request) =>
|
||||
_ok(<Map<String, dynamic>>[
|
||||
{
|
||||
'id': 'cluster-tired',
|
||||
'label': 'Too tired',
|
||||
'occurrences': 4,
|
||||
'dominantCategory': 'Deep work',
|
||||
'byWeekday': {'4': 3, '2': 1},
|
||||
'byHour': {'19': 3, '21': 1},
|
||||
'insight':
|
||||
"You've said \"too tired\" 4 times this month — 3 of them on Thursday evenings. Put deep work before lunch on Thursdays.",
|
||||
},
|
||||
{
|
||||
'id': 'cluster-time',
|
||||
'label': 'Ran out of time',
|
||||
'occurrences': 3,
|
||||
'dominantCategory': 'Admin',
|
||||
'byWeekday': {'1': 2, '5': 1},
|
||||
'byHour': {'17': 3},
|
||||
'insight': '',
|
||||
},
|
||||
]);
|
||||
|
||||
// ── Proof ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> uploadPhotoProof(FormData request) =>
|
||||
_ok(<String, dynamic>{'proofRef': 'mock-photo'});
|
||||
|
||||
@override
|
||||
Future<Response> submitTimerProofEntry(CompletionRequest request) =>
|
||||
completeCommitmentEntry(request);
|
||||
|
||||
@override
|
||||
Future<Response> submitLocationProofEntry(CompletionRequest request) =>
|
||||
completeCommitmentEntry(request);
|
||||
|
||||
// ── Guardrails ────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> spendAmnestyToken(AmnestyRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> getAmnestyBalance() => _ok(<String, dynamic>{'balance': 3});
|
||||
|
||||
@override
|
||||
Future<Response> updateSickMode(SickModeRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> checkDistress() => _ok(<String, dynamic>{'distressed': false});
|
||||
|
||||
@override
|
||||
Future<Response> updateTone(ToneRequest request) => _done();
|
||||
|
||||
// ── Habits & routines ─────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getMyHabits(HistoryRequest request) => _ok(<dynamic>[]);
|
||||
|
||||
@override
|
||||
Future<Response> saveHabitEntry(Map<String, dynamic> request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> logHabitEntry(IdRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> getKeystoneHabits() => _ok(<dynamic>[]);
|
||||
|
||||
@override
|
||||
Future<Response> getMyRoutines(HistoryRequest request) => _ok(
|
||||
MockStore.routines.map((routine) => routine.toJson()).toList());
|
||||
|
||||
@override
|
||||
Future<Response> saveRoutineEntry(Map<String, dynamic> request) {
|
||||
final RoutineChain routine = RoutineChain.fromJson(request);
|
||||
final int index = MockStore.routines
|
||||
.indexWhere((item) => item.id != null && item.id == routine.id);
|
||||
if (routine.id == null || index < 0) {
|
||||
routine.id = MockStore.newId('routine');
|
||||
MockStore.routines.add(routine);
|
||||
} else {
|
||||
// Keep today's progress across an edit.
|
||||
final RoutineChain before = MockStore.routines[index];
|
||||
for (int step = 0;
|
||||
step < routine.steps.length && step < before.steps.length;
|
||||
step++) {
|
||||
routine.steps[step].completed = before.steps[step].completed;
|
||||
}
|
||||
MockStore.routines[index] = routine;
|
||||
}
|
||||
return _ok(routine.toJson());
|
||||
}
|
||||
|
||||
/// A logged run marks today's progress on the routine, so the library ring
|
||||
/// and the "done" tick reflect what just happened.
|
||||
@override
|
||||
Future<Response> logRoutineChainEntry(Map<String, dynamic> request) {
|
||||
for (RoutineChain routine in MockStore.routines) {
|
||||
if (routine.id == request['routineId']) {
|
||||
final int done = request['completedSteps'] ?? 0;
|
||||
for (int step = 0; step < routine.steps.length; step++) {
|
||||
routine.steps[step].completed = step < done;
|
||||
}
|
||||
routine.lastBreakIndex = request['breakIndex'] ?? -1;
|
||||
}
|
||||
}
|
||||
return _done();
|
||||
}
|
||||
|
||||
// ── Training ──────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getMyPrograms(HistoryRequest request) =>
|
||||
_ok(<Map<String, dynamic>>[
|
||||
{
|
||||
'id': 'program-base',
|
||||
'name': 'Base strength · 8 weeks',
|
||||
'weeks': 8,
|
||||
'sessionsPerWeek': 3,
|
||||
'deloadWeeks': <int>[4, 8],
|
||||
'weeklyContactCeiling': 120,
|
||||
'lowerBodyRecoveryHours': 48,
|
||||
'active': true,
|
||||
},
|
||||
]);
|
||||
|
||||
@override
|
||||
Future<Response> saveProgramEntry(Map<String, dynamic> request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> activateProgramEntry(IdRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> getProgramSessions(IdRequest request) =>
|
||||
_ok(<Map<String, dynamic>>[
|
||||
{
|
||||
'id': 'session-a',
|
||||
'programId': 'program-base',
|
||||
'dayIndex': 0,
|
||||
'name': 'Day A · Squat & press',
|
||||
'highIntensityLowerBody': true,
|
||||
'prescriptions': <dynamic>[{}, {}, {}, {}],
|
||||
},
|
||||
{
|
||||
'id': 'session-b',
|
||||
'programId': 'program-base',
|
||||
'dayIndex': 2,
|
||||
'name': 'Day B · Pull & hinge',
|
||||
'highIntensityLowerBody': false,
|
||||
'prescriptions': <dynamic>[{}, {}, {}],
|
||||
},
|
||||
]);
|
||||
|
||||
@override
|
||||
Future<Response> startSessionEntry(IdRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> saveSessionLogEntry(SessionLogRequest request) => _done();
|
||||
|
||||
@override
|
||||
Future<Response> getSessionHistory(HistoryRequest request) => _emptyPage();
|
||||
|
||||
@override
|
||||
Future<Response> getLastSessionFor(IdRequest request) => _ok(<String, dynamic>{});
|
||||
|
||||
@override
|
||||
Future<Response> getWeeklyVolume(ReportCardRequest request) => _ok(<String, dynamic>{});
|
||||
|
||||
@override
|
||||
Future<Response> getContactVolume(ReportCardRequest request) => _ok(<String, dynamic>{});
|
||||
|
||||
@override
|
||||
Future<Response> getPersonalRecords(HistoryRequest request) => _emptyPage();
|
||||
|
||||
// ── Report card ───────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getWeeklyReportCard(ReportCardRequest request) => _ok(_report());
|
||||
|
||||
@override
|
||||
Future<Response> getMonthlyReportCard(ReportCardRequest request) => _ok(_report());
|
||||
|
||||
Map<String, dynamic> _report() {
|
||||
final DateTime now = DateTime.now();
|
||||
return {
|
||||
'id': 'report-week',
|
||||
'periodStart': now.subtract(const Duration(days: 7)).toIso8601String(),
|
||||
'periodEnd': now.toIso8601String(),
|
||||
'completionByClass': {
|
||||
'NonNegotiable': 0.92,
|
||||
'Standard': 0.71,
|
||||
'Elective': 0.40,
|
||||
},
|
||||
'completionByCategory': {
|
||||
'Deep work': 0.64,
|
||||
'Admin': 0.88,
|
||||
'Health': 0.79,
|
||||
},
|
||||
'completionByWeekday': {
|
||||
'1': 0.9, '2': 0.8, '3': 0.75, '4': 0.45, '5': 0.7, '6': 0.6, '7': 0.85,
|
||||
},
|
||||
'worstHour': 19,
|
||||
'debtTrend': <double>[8, 9.5, 11, 10.2, 7.4, 6.8, 5.9],
|
||||
'deferralLeaderboard': <Map<String, dynamic>>[
|
||||
{'commitmentId': 'c-email', 'title': 'Email supervisor the outline', 'count': 2},
|
||||
{'commitmentId': 'c-kitchen', 'title': 'Deep clean the kitchen', 'count': 1},
|
||||
],
|
||||
'excuseTaxonomy': <dynamic>[],
|
||||
'estimationAccuracy': {'Deep work': 1.6, 'Admin': 1.1},
|
||||
'trainingAdherence': 0.83,
|
||||
'programIntegrity': 0.9,
|
||||
'assignedAction':
|
||||
'Move deep work before noon on Thursdays. That is where the week leaks.',
|
||||
'grade': 'B',
|
||||
'praise': 'Every non-negotiable but one landed inside its window.',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Notifications ─────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Future<Response> getMyNotifications(HistoryRequest request) => _emptyPage();
|
||||
|
||||
@override
|
||||
Future<Response> acknowledgeNudgeEntry(IdRequest request) => _done();
|
||||
}
|
||||
26
frontend/lib/Grounded/mock/MockMemory.dart
Normal file
26
frontend/lib/Grounded/mock/MockMemory.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
// MOCK DATA — TEMPORARY. See MockSwitch.dart.
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../about/internal/application/MeDescription.dart';
|
||||
import '../about/internal/application/UserDetails.dart';
|
||||
import '../memory/InternalMemory.dart';
|
||||
import 'MockStorage.dart';
|
||||
|
||||
/// The real [InternalMemory], pointed at in-memory storage and pre-seeded
|
||||
/// with a signed-in user so the app opens straight onto Home.
|
||||
class MockMemory extends InternalMemory {
|
||||
static bool _seeded = false;
|
||||
|
||||
MockMemory() {
|
||||
if (!_seeded) {
|
||||
_seeded = true;
|
||||
setMyDescription(
|
||||
MeDescription(id: "mock-user", name: "Alex", token: "mock-token"));
|
||||
setUserDetails(UserDetails(pic: "", name: "Alex"));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FlutterSecureStorage get groundedStorage => const MockStorage();
|
||||
}
|
||||
55
frontend/lib/Grounded/mock/MockStorage.dart
Normal file
55
frontend/lib/Grounded/mock/MockStorage.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
// MOCK DATA — TEMPORARY. See MockSwitch.dart.
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// Secure storage held in memory, so mock mode never touches the keychain or
|
||||
/// browser storage. Seeded as signed in.
|
||||
class MockStorage extends FlutterSecureStorage {
|
||||
static final Map<String, String> _values = <String, String>{};
|
||||
|
||||
const MockStorage();
|
||||
|
||||
@override
|
||||
Future<String?> read({
|
||||
required String key,
|
||||
AppleOptions? iOptions,
|
||||
AndroidOptions? aOptions,
|
||||
LinuxOptions? lOptions,
|
||||
WebOptions? webOptions,
|
||||
AppleOptions? mOptions,
|
||||
WindowsOptions? wOptions,
|
||||
}) async {
|
||||
return _values[key];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write({
|
||||
required String key,
|
||||
required String? value,
|
||||
AppleOptions? iOptions,
|
||||
AndroidOptions? aOptions,
|
||||
LinuxOptions? lOptions,
|
||||
WebOptions? webOptions,
|
||||
AppleOptions? mOptions,
|
||||
WindowsOptions? wOptions,
|
||||
}) async {
|
||||
if (value == null) {
|
||||
_values.remove(key);
|
||||
return;
|
||||
}
|
||||
_values[key] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete({
|
||||
required String key,
|
||||
AppleOptions? iOptions,
|
||||
AndroidOptions? aOptions,
|
||||
LinuxOptions? lOptions,
|
||||
WebOptions? webOptions,
|
||||
AppleOptions? mOptions,
|
||||
WindowsOptions? wOptions,
|
||||
}) async {
|
||||
_values.remove(key);
|
||||
}
|
||||
}
|
||||
298
frontend/lib/Grounded/mock/MockStore.dart
Normal file
298
frontend/lib/Grounded/mock/MockStore.dart
Normal file
@@ -0,0 +1,298 @@
|
||||
// MOCK DATA — TEMPORARY. See MockSwitch.dart.
|
||||
|
||||
import '../about/external/data/RoutineChain.dart';
|
||||
import '../about/internal/application/CommitmentClass.dart';
|
||||
import '../utils/RoutineTemplates.dart';
|
||||
|
||||
/// The fake backend's database: plain JSON maps, seeded relative to today,
|
||||
/// mutated by [MockComms] so taps actually change what you see. Static, so
|
||||
/// every screen shares one copy for the life of the app.
|
||||
class MockStore {
|
||||
static bool _seeded = false;
|
||||
|
||||
static int _nextId = 100;
|
||||
|
||||
static final List<Map<String, dynamic>> goals = <Map<String, dynamic>>[];
|
||||
|
||||
static final List<Map<String, dynamic>> commitments =
|
||||
<Map<String, dynamic>>[];
|
||||
|
||||
static final List<RoutineChain> routines = <RoutineChain>[];
|
||||
|
||||
static String newId(String prefix) {
|
||||
_nextId = _nextId + 1;
|
||||
return "$prefix-$_nextId";
|
||||
}
|
||||
|
||||
static void ensureSeeded() {
|
||||
if (_seeded) {
|
||||
return;
|
||||
}
|
||||
_seeded = true;
|
||||
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime today = DateTime(now.year, now.month, now.day);
|
||||
final int weekday = now.weekday;
|
||||
|
||||
DateTime at(int dayOffset, int hour, [int minute = 0]) {
|
||||
return today.add(Duration(days: dayOffset, hours: hour, minutes: minute));
|
||||
}
|
||||
|
||||
// ── Goals ──────────────────────────────────────────────────────────────
|
||||
goals.addAll(<Map<String, dynamic>>[
|
||||
{
|
||||
'id': 'goal-strong',
|
||||
'title': 'Get strong',
|
||||
'description': 'Deadlift my bodyweight by spring, without getting hurt.',
|
||||
'category': 'Health',
|
||||
'defaultClass': 'Standard',
|
||||
'startDate': at(-30, 0).toIso8601String(),
|
||||
'targetDate': at(60, 0).toIso8601String(),
|
||||
'colourHex': '#FF2F6F4E',
|
||||
'archived': false,
|
||||
},
|
||||
{
|
||||
'id': 'goal-thesis',
|
||||
'title': 'Finish the thesis',
|
||||
'description': 'Submit before the department deadline. No extensions.',
|
||||
'category': 'Work',
|
||||
'defaultClass': 'NonNegotiable',
|
||||
'startDate': at(-60, 0).toIso8601String(),
|
||||
'targetDate': at(45, 0).toIso8601String(),
|
||||
'colourHex': '#FF2C4A63',
|
||||
'archived': false,
|
||||
},
|
||||
{
|
||||
'id': 'goal-calm',
|
||||
'title': 'Calm mornings',
|
||||
'description': 'Start the day on purpose instead of on the phone.',
|
||||
'category': 'Mind',
|
||||
'defaultClass': 'Elective',
|
||||
'startDate': at(-10, 0).toIso8601String(),
|
||||
'targetDate': null,
|
||||
'colourHex': '#FF4F5D86',
|
||||
'archived': false,
|
||||
},
|
||||
]);
|
||||
|
||||
// ── Commitments ────────────────────────────────────────────────────────
|
||||
Map<String, dynamic> task(
|
||||
String id,
|
||||
String title, {
|
||||
String? goal,
|
||||
String cls = 'Standard',
|
||||
required DateTime start,
|
||||
required DateTime end,
|
||||
int minutes = 30,
|
||||
String proof = 'Honour',
|
||||
int timer = 0,
|
||||
String status = 'Open',
|
||||
int deferrals = 0,
|
||||
String category = '',
|
||||
}) {
|
||||
return {
|
||||
'id': id,
|
||||
'type': 'TASK',
|
||||
'commitmentClass': cls,
|
||||
'title': title,
|
||||
'category': category,
|
||||
'dueStart': start.toIso8601String(),
|
||||
'dueEnd': end.toIso8601String(),
|
||||
'estMinutes': minutes,
|
||||
'energy': 'Medium',
|
||||
'proofType': proof,
|
||||
'proofTimerMinutes': timer,
|
||||
'rrule': '',
|
||||
'parentId': goal,
|
||||
'status': status,
|
||||
'deferralCount': deferrals,
|
||||
};
|
||||
}
|
||||
|
||||
commitments.addAll(<Map<String, dynamic>>[
|
||||
task('c-walk', 'Morning walk, no phone',
|
||||
goal: 'goal-calm',
|
||||
cls: 'Elective',
|
||||
start: at(0, 6, 30),
|
||||
end: at(0, 8),
|
||||
minutes: 20,
|
||||
status: 'Completed'),
|
||||
task('c-chapter', 'Write 500 words of chapter 3',
|
||||
goal: 'goal-thesis',
|
||||
cls: 'NonNegotiable',
|
||||
start: at(0, 9),
|
||||
end: at(0, 23, 30),
|
||||
minutes: 25,
|
||||
proof: 'Timer',
|
||||
timer: 1,
|
||||
category: 'Deep work'),
|
||||
task('c-rent', 'Pay rent',
|
||||
cls: 'NonNegotiable',
|
||||
start: at(0, 8),
|
||||
end: at(0, 23, 59),
|
||||
minutes: 5,
|
||||
category: 'Admin'),
|
||||
task('c-physio', 'Book physio appointment',
|
||||
goal: 'goal-strong',
|
||||
start: at(0, 12),
|
||||
end: at(0, 23),
|
||||
minutes: 10,
|
||||
category: 'Admin'),
|
||||
task('c-read', 'Read two papers for the literature review',
|
||||
goal: 'goal-thesis',
|
||||
start: at(0, 18),
|
||||
end: at(0, 23, 45),
|
||||
minutes: 60,
|
||||
category: 'Deep work'),
|
||||
task('c-meal', 'Meal prep for the week',
|
||||
goal: 'goal-strong',
|
||||
start: at(1, 17),
|
||||
end: at(1, 20),
|
||||
minutes: 90),
|
||||
// Two things already past their window, so the overdue queue and debt
|
||||
// have something to show.
|
||||
task('c-email', 'Email supervisor the outline',
|
||||
goal: 'goal-thesis',
|
||||
start: at(-2, 9),
|
||||
end: at(-2, 12),
|
||||
minutes: 15,
|
||||
status: 'Overdue',
|
||||
deferrals: 1),
|
||||
task('c-kitchen', 'Deep clean the kitchen',
|
||||
start: at(-1, 18),
|
||||
end: at(-1, 21),
|
||||
minutes: 45,
|
||||
status: 'Overdue'),
|
||||
]);
|
||||
|
||||
// ── Routines ───────────────────────────────────────────────────────────
|
||||
final RoutineChain strength = RoutineTemplates.fullBody()
|
||||
..id = 'routine-strength'
|
||||
..goalId = 'goal-strong'
|
||||
..days = _withToday(<int>[1, 3, 5], weekday)
|
||||
..startMinute = 7 * 60;
|
||||
|
||||
final RoutineChain push = RoutineTemplates.upperPush()
|
||||
..id = 'routine-push'
|
||||
..goalId = 'goal-strong'
|
||||
..days = <int>[1, 2, 3, 4, 5, 6, 7].where((day) => day != weekday).take(2).toList();
|
||||
|
||||
final RoutineChain breathing = RoutineTemplates.boxBreathing()
|
||||
..id = 'routine-breathing'
|
||||
..goalId = 'goal-calm';
|
||||
|
||||
final RoutineChain morning = RoutineTemplates.morning()
|
||||
..id = 'routine-morning'
|
||||
..goalId = 'goal-calm'
|
||||
..days = _withToday(<int>[1, 2, 3, 4, 5], weekday);
|
||||
for (int index = 0; index < morning.steps.length; index++) {
|
||||
morning.steps[index].completed = true;
|
||||
}
|
||||
|
||||
final RoutineChain shutdown = RoutineTemplates.shutdown()
|
||||
..id = 'routine-shutdown'
|
||||
..days = _withToday(<int>[1, 2, 3, 4, 5], weekday)
|
||||
..commitmentClass = CommitmentClass.Standard;
|
||||
shutdown.steps.first.completed = true;
|
||||
|
||||
final RoutineChain mobility = RoutineTemplates.mobility()
|
||||
..id = 'routine-mobility';
|
||||
|
||||
routines.addAll(<RoutineChain>[
|
||||
breathing,
|
||||
morning,
|
||||
strength,
|
||||
shutdown,
|
||||
push,
|
||||
mobility,
|
||||
]);
|
||||
}
|
||||
|
||||
static List<int> _withToday(List<int> days, int weekday) {
|
||||
final List<int> result = List<int>.from(days);
|
||||
if (!result.contains(weekday)) {
|
||||
result.add(weekday);
|
||||
}
|
||||
result.sort();
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Derived views ──────────────────────────────────────────────────────
|
||||
|
||||
static bool _settled(Map<String, dynamic> item) {
|
||||
final String status = item['status'];
|
||||
return status == 'Completed' ||
|
||||
status == 'LateCompleted' ||
|
||||
status == 'Abandoned' ||
|
||||
status == 'Archived';
|
||||
}
|
||||
|
||||
static bool _windowClosed(Map<String, dynamic> item) {
|
||||
final DateTime? end = DateTime.tryParse(item['dueEnd'] ?? '');
|
||||
return end != null && end.isBefore(DateTime.now());
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> todayPlan() {
|
||||
final DateTime now = DateTime.now();
|
||||
return commitments.where((item) {
|
||||
final DateTime? start = DateTime.tryParse(item['dueStart'] ?? '');
|
||||
return start != null &&
|
||||
start.year == now.year &&
|
||||
start.month == now.month &&
|
||||
start.day == now.day &&
|
||||
item['status'] != 'Abandoned';
|
||||
}).toList()
|
||||
..sort((a, b) => (a['dueStart'] as String).compareTo(b['dueStart']));
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> overdue() {
|
||||
return commitments
|
||||
.where((item) => !_settled(item) && _windowClosed(item))
|
||||
.map((item) => Map<String, dynamic>.from(item)..['status'] = 'Overdue')
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> goalTasks(String goalId) {
|
||||
return commitments.where((item) => item['parentId'] == goalId).toList();
|
||||
}
|
||||
|
||||
/// Goals with the server-derived counts filled in from the tasks.
|
||||
static List<Map<String, dynamic>> goalsWithCounts() {
|
||||
return goals.where((goal) => goal['archived'] != true).map((goal) {
|
||||
final List<Map<String, dynamic>> tasks = goalTasks(goal['id']);
|
||||
final int completed = tasks
|
||||
.where((item) =>
|
||||
item['status'] == 'Completed' || item['status'] == 'LateCompleted')
|
||||
.length;
|
||||
final int late =
|
||||
tasks.where((item) => !_settled(item) && _windowClosed(item)).length;
|
||||
return Map<String, dynamic>.from(goal)
|
||||
..['totalTasks'] = tasks.where((item) => item['status'] != 'Abandoned').length
|
||||
..['completedTasks'] = completed
|
||||
..['overdueTasks'] = late
|
||||
..['debtContribution'] = late * 2.0;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? commitment(String id) {
|
||||
for (Map<String, dynamic> item in commitments) {
|
||||
if (item['id'] == id) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Map<String, dynamic> page(List<Map<String, dynamic>> content) {
|
||||
return {
|
||||
'number': 0,
|
||||
'size': content.length,
|
||||
'totalElements': content.length,
|
||||
'totalPages': 1,
|
||||
'numberOfElements': content.length,
|
||||
'first': true,
|
||||
'last': true,
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
}
|
||||
8
frontend/lib/Grounded/mock/MockSwitch.dart
Normal file
8
frontend/lib/Grounded/mock/MockSwitch.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MOCK DATA — TEMPORARY. Delete the whole `lib/Grounded/mock/` folder and the
|
||||
// lines tagged `MOCK:` (search the project for "MOCK:") once the backend is up.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// True runs the app entirely on seeded, in-memory data: no backend, no
|
||||
/// device storage, no sign-in. Everything resets on restart.
|
||||
const bool useMockData = true;
|
||||
@@ -18,6 +18,9 @@ import '../../designs/text/Text.dart';
|
||||
import '../../informatics/AppDataManager.dart';
|
||||
import '../../informatics/DataManager.dart';
|
||||
import '../../memory/InternalMemory.dart';
|
||||
import '../../mock/MockComms.dart'; // MOCK: remove with lib/Grounded/mock/
|
||||
import '../../mock/MockMemory.dart'; // MOCK: remove with lib/Grounded/mock/
|
||||
import '../../mock/MockSwitch.dart'; // MOCK: remove with lib/Grounded/mock/
|
||||
import '../../utils/Colors.dart';
|
||||
import '../system/sessionexpired/SessionExpired.dart';
|
||||
import '../system/updateme/UpdateMe.dart';
|
||||
@@ -41,7 +44,11 @@ class ParentViewModel extends ChangeNotifier {
|
||||
ParentViewModel(this.context) {
|
||||
overlayState = Overlay.of(context);
|
||||
dataManager =
|
||||
AppDataManager(InternalMemory(), Comms(InternalMemory()), FileStorage());
|
||||
// MOCK: restore to the single real line below when removing mocks.
|
||||
useMockData
|
||||
? AppDataManager(MockMemory(), MockComms(), FileStorage())
|
||||
: AppDataManager(
|
||||
InternalMemory(), Comms(InternalMemory()), FileStorage());
|
||||
}
|
||||
|
||||
DataManager getDataManager() {
|
||||
@@ -199,6 +206,11 @@ class ParentViewModel extends ChangeNotifier {
|
||||
Future<bool> hasNetwork(Function() actions) async {
|
||||
closeLoading();
|
||||
|
||||
// MOCK: mock mode never needs a connection.
|
||||
if (useMockData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<ConnectivityResult> resultList =
|
||||
await (Connectivity().checkConnectivity());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user