feat: add local database persistence
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
import '../core/ambience.dart';
|
||||
import '../data/app_preferences_store.dart';
|
||||
import '../data/focus_seed_store.dart';
|
||||
import '../features/shell/focus_seed_home.dart';
|
||||
import '../l10n/generated/app_localizations.dart';
|
||||
import '../models/appearance_preference.dart';
|
||||
@@ -18,9 +19,10 @@ Locale focusSeedDefaultLocale({bool isRelease = kReleaseMode}) {
|
||||
}
|
||||
|
||||
class FocusSeedApp extends StatefulWidget {
|
||||
const FocusSeedApp({this.locale, super.key});
|
||||
const FocusSeedApp({this.locale, this.focusSeedStore, super.key});
|
||||
|
||||
final Locale? locale;
|
||||
final FocusSeedStore? focusSeedStore;
|
||||
|
||||
@override
|
||||
State<FocusSeedApp> createState() => _FocusSeedAppState();
|
||||
@@ -114,6 +116,7 @@ class _FocusSeedAppState extends State<FocusSeedApp>
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: FocusSeedHome(
|
||||
focusSeedStore: widget.focusSeedStore,
|
||||
appearance: _appearance,
|
||||
onAppearanceChanged: _setAppearance,
|
||||
language: _language,
|
||||
|
||||
@@ -1,33 +1,245 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../core/json_utils.dart';
|
||||
import '../core/date_keys.dart' as date_keys;
|
||||
import '../models/app_state.dart';
|
||||
import '../models/focus_session.dart';
|
||||
import '../models/habit.dart';
|
||||
import '../models/mood.dart';
|
||||
import '../models/reflection_entry.dart';
|
||||
import 'local/focus_seed_database.dart';
|
||||
|
||||
class FocusSeedStore {
|
||||
static const _storageKey = 'focus_seed_state_v1';
|
||||
FocusSeedStore({FocusSeedDatabase? database})
|
||||
: _database = database ?? FocusSeedDatabase();
|
||||
|
||||
static const _legacyStorageKey = 'focus_seed_state_v1';
|
||||
static const _legacyMigrationKey = 'focus_seed_state_v1_migrated_to_sqlite';
|
||||
|
||||
final FocusSeedDatabase _database;
|
||||
|
||||
Future<AppState> load({List<String>? defaultHabitTitles}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return AppState.starter(defaultHabitTitles: defaultHabitTitles);
|
||||
}
|
||||
try {
|
||||
return AppState.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||
} on Object {
|
||||
return AppState.starter(defaultHabitTitles: defaultHabitTitles);
|
||||
await _migrateLegacyJsonIfNeeded();
|
||||
final snapshot = await _readSnapshot();
|
||||
if (!snapshot.isFreshStart) {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
final starter = AppState.starter(defaultHabitTitles: defaultHabitTitles);
|
||||
await save(starter);
|
||||
return starter;
|
||||
}
|
||||
|
||||
Future<void> save(AppState state) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_storageKey, prettyJson(state.toJson()));
|
||||
await _database.transaction(() async {
|
||||
await _database.delete(_database.habitLogRows).go();
|
||||
await _database.delete(_database.reflectionRows).go();
|
||||
await _database.delete(_database.focusSessionRows).go();
|
||||
await _database.delete(_database.habitRows).go();
|
||||
await _database.delete(_database.dailyIntentionRows).go();
|
||||
|
||||
if (state.anchor.trim().isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
await _database
|
||||
.into(_database.dailyIntentionRows)
|
||||
.insert(
|
||||
DailyIntentionRowsCompanion.insert(
|
||||
dateKey: state.anchorDateKey,
|
||||
intentionText: state.anchor,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (final (index, habit) in state.habits.indexed) {
|
||||
await _database
|
||||
.into(_database.habitRows)
|
||||
.insert(
|
||||
HabitRowsCompanion.insert(
|
||||
id: habit.id,
|
||||
title: habit.title,
|
||||
sortOrder: index,
|
||||
createdAt: habit.createdAt,
|
||||
),
|
||||
);
|
||||
for (final completedDate in habit.completedDates) {
|
||||
await _database
|
||||
.into(_database.habitLogRows)
|
||||
.insert(
|
||||
HabitLogRowsCompanion.insert(
|
||||
id: '${habit.id}-$completedDate',
|
||||
habitId: habit.id,
|
||||
dateKey: completedDate,
|
||||
completedAt: DateTime.parse('$completedDate 12:00:00'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final session in state.sessions) {
|
||||
await _database
|
||||
.into(_database.focusSessionRows)
|
||||
.insert(_focusSessionCompanion(session));
|
||||
}
|
||||
|
||||
for (final reflection in state.reflections) {
|
||||
await _database
|
||||
.into(_database.reflectionRows)
|
||||
.insert(
|
||||
ReflectionRowsCompanion.insert(
|
||||
id: reflection.id,
|
||||
dateKey: reflection.dateKey,
|
||||
mood: reflection.mood.name,
|
||||
note: reflection.note,
|
||||
createdAt: reflection.createdAt,
|
||||
updatedAt: reflection.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
await preferences.setBool(_legacyMigrationKey, true);
|
||||
await preferences.remove(_legacyStorageKey);
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_storageKey);
|
||||
await _database.clearProductData();
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
await preferences.remove(_legacyStorageKey);
|
||||
await preferences.setBool(_legacyMigrationKey, true);
|
||||
}
|
||||
|
||||
Future<AppState> _readSnapshot() async {
|
||||
final intentionRows =
|
||||
await (_database.select(_database.dailyIntentionRows)
|
||||
..orderBy([(row) => OrderingTerm.desc(row.dateKey)])
|
||||
..limit(1))
|
||||
.get();
|
||||
final latestIntention = intentionRows.firstOrNull;
|
||||
final habitRows =
|
||||
await (_database.select(_database.habitRows)
|
||||
..where((row) => row.archivedAt.isNull())
|
||||
..orderBy([
|
||||
(row) => OrderingTerm.asc(row.sortOrder),
|
||||
(row) => OrderingTerm.asc(row.createdAt),
|
||||
]))
|
||||
.get();
|
||||
final habitLogRows =
|
||||
await (_database.select(_database.habitLogRows)
|
||||
..orderBy([(row) => OrderingTerm.asc(row.dateKey)])).get();
|
||||
final focusSessionRows =
|
||||
await (_database.select(_database.focusSessionRows)
|
||||
..orderBy([(row) => OrderingTerm.asc(row.endedAt)])).get();
|
||||
final reflectionRows =
|
||||
await (_database.select(_database.reflectionRows)
|
||||
..orderBy([(row) => OrderingTerm.asc(row.createdAt)])).get();
|
||||
|
||||
final completedDatesByHabit = <String, Set<String>>{};
|
||||
for (final log in habitLogRows) {
|
||||
completedDatesByHabit.putIfAbsent(log.habitId, () => {}).add(log.dateKey);
|
||||
}
|
||||
|
||||
return AppState(
|
||||
anchor: latestIntention?.intentionText ?? '',
|
||||
anchorDateKey: latestIntention?.dateKey ?? date_keys.todayKey(),
|
||||
habits: [
|
||||
for (final habit in habitRows)
|
||||
Habit(
|
||||
id: habit.id,
|
||||
title: habit.title,
|
||||
createdAt: habit.createdAt,
|
||||
completedDates: completedDatesByHabit[habit.id] ?? const {},
|
||||
),
|
||||
],
|
||||
sessions: [
|
||||
for (final session in focusSessionRows)
|
||||
FocusSession(
|
||||
id: session.id,
|
||||
minutes: session.plannedMinutes,
|
||||
completedAt: session.endedAt,
|
||||
),
|
||||
],
|
||||
reflections: [
|
||||
for (final reflection in reflectionRows)
|
||||
ReflectionEntry(
|
||||
id: reflection.id,
|
||||
dateKey: reflection.dateKey,
|
||||
note: reflection.note,
|
||||
mood: Mood.values.firstWhere(
|
||||
(mood) => mood.name == reflection.mood,
|
||||
orElse: () => Mood.clear,
|
||||
),
|
||||
createdAt: reflection.createdAt,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _migrateLegacyJsonIfNeeded() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
if (preferences.getBool(_legacyMigrationKey) ?? false) {
|
||||
return;
|
||||
}
|
||||
if (await _hasDatabaseContent()) {
|
||||
await preferences.setBool(_legacyMigrationKey, true);
|
||||
return;
|
||||
}
|
||||
|
||||
final raw = preferences.getString(_legacyStorageKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
await preferences.setBool(_legacyMigrationKey, true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final legacyState = AppState.fromJson(
|
||||
jsonDecode(raw) as Map<String, dynamic>,
|
||||
);
|
||||
await save(legacyState);
|
||||
} on Object {
|
||||
await preferences.setBool(_legacyMigrationKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _hasDatabaseContent() async {
|
||||
final intention =
|
||||
await (_database.select(_database.dailyIntentionRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
if (intention != null) {
|
||||
return true;
|
||||
}
|
||||
final habit =
|
||||
await (_database.select(_database.habitRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
if (habit != null) {
|
||||
return true;
|
||||
}
|
||||
final session =
|
||||
await (_database.select(_database.focusSessionRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
if (session != null) {
|
||||
return true;
|
||||
}
|
||||
final reflection =
|
||||
await (_database.select(_database.reflectionRows)
|
||||
..limit(1)).getSingleOrNull();
|
||||
return reflection != null;
|
||||
}
|
||||
|
||||
FocusSessionRowsCompanion _focusSessionCompanion(FocusSession session) {
|
||||
final completedAt = session.completedAt;
|
||||
return FocusSessionRowsCompanion.insert(
|
||||
id: session.id,
|
||||
dateKey: date_keys.dateKey(completedAt),
|
||||
plannedMinutes: session.minutes,
|
||||
actualSeconds: session.minutes * 60,
|
||||
startedAt: completedAt.subtract(Duration(minutes: session.minutes)),
|
||||
endedAt: completedAt,
|
||||
status: 'completed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'focus_seed_database.g.dart';
|
||||
|
||||
class DailyIntentionRows extends Table {
|
||||
@override
|
||||
String get tableName => 'daily_intentions';
|
||||
|
||||
TextColumn get dateKey => text()();
|
||||
TextColumn get intentionText => text().named('text')();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {dateKey};
|
||||
}
|
||||
|
||||
class FocusSessionRows extends Table {
|
||||
@override
|
||||
String get tableName => 'focus_sessions';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get dateKey => text()();
|
||||
IntColumn get plannedMinutes => integer()();
|
||||
IntColumn get actualSeconds => integer()();
|
||||
DateTimeColumn get startedAt => dateTime()();
|
||||
DateTimeColumn get endedAt => dateTime()();
|
||||
TextColumn get status => text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class HabitRows extends Table {
|
||||
@override
|
||||
String get tableName => 'habits';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get title => text()();
|
||||
IntColumn get sortOrder => integer()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get archivedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class HabitLogRows extends Table {
|
||||
@override
|
||||
String get tableName => 'habit_logs';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get habitId => text()();
|
||||
TextColumn get dateKey => text()();
|
||||
DateTimeColumn get completedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class ReflectionRows extends Table {
|
||||
@override
|
||||
String get tableName => 'reflections';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get dateKey => text()();
|
||||
TextColumn get mood => text()();
|
||||
TextColumn get note => text()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class AppSettingRows extends Table {
|
||||
@override
|
||||
String get tableName => 'app_settings';
|
||||
|
||||
TextColumn get key => text()();
|
||||
TextColumn get value => text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {key};
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [
|
||||
DailyIntentionRows,
|
||||
FocusSessionRows,
|
||||
HabitRows,
|
||||
HabitLogRows,
|
||||
ReflectionRows,
|
||||
AppSettingRows,
|
||||
],
|
||||
)
|
||||
class FocusSeedDatabase extends _$FocusSeedDatabase {
|
||||
FocusSeedDatabase([QueryExecutor? executor])
|
||||
: super(executor ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
Future<void> clearProductData() async {
|
||||
await transaction(() async {
|
||||
await delete(habitLogRows).go();
|
||||
await delete(reflectionRows).go();
|
||||
await delete(focusSessionRows).go();
|
||||
await delete(habitRows).go();
|
||||
await delete(dailyIntentionRows).go();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final file = File(path.join(directory.path, 'focus_seed.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import '../../application/focus_seed_controller.dart';
|
||||
import '../../application/focus_timer_controller.dart';
|
||||
import '../../data/focus_seed_store.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../models/appearance_preference.dart';
|
||||
import '../../models/language_preference.dart';
|
||||
@@ -18,6 +19,7 @@ class FocusSeedHome extends StatefulWidget {
|
||||
required this.language,
|
||||
required this.showLanguageTools,
|
||||
required this.onLanguageChanged,
|
||||
this.focusSeedStore,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -26,6 +28,7 @@ class FocusSeedHome extends StatefulWidget {
|
||||
final LanguagePreference language;
|
||||
final bool showLanguageTools;
|
||||
final ValueChanged<LanguagePreference> onLanguageChanged;
|
||||
final FocusSeedStore? focusSeedStore;
|
||||
|
||||
@override
|
||||
State<FocusSeedHome> createState() => _FocusSeedHomeState();
|
||||
@@ -43,6 +46,7 @@ class _FocusSeedHomeState extends State<FocusSeedHome> {
|
||||
return;
|
||||
}
|
||||
_controller = FocusSeedController(
|
||||
store: widget.focusSeedStore,
|
||||
defaultHabitTitles: localizedStarterHabits(context.l10n),
|
||||
);
|
||||
_timerController = FocusTimerController(
|
||||
|
||||
@@ -17,7 +17,11 @@ class AppState {
|
||||
final List<FocusSession> sessions;
|
||||
final List<ReflectionEntry> reflections;
|
||||
|
||||
bool get isFreshStart => habits.isEmpty && sessions.isEmpty && anchor.isEmpty;
|
||||
bool get isFreshStart =>
|
||||
habits.isEmpty &&
|
||||
sessions.isEmpty &&
|
||||
reflections.isEmpty &&
|
||||
anchor.isEmpty;
|
||||
|
||||
String activeAnchor(String dateKey) {
|
||||
return anchorDateKey == dateKey ? anchor : '';
|
||||
|
||||
+369
-1
@@ -1,6 +1,30 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "88.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.1.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -17,6 +41,54 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.15.0"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_collection
|
||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
built_value:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.12.6"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -25,6 +97,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +137,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -49,6 +161,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.31.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.31.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -73,6 +209,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -101,6 +245,38 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: graphs
|
||||
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -109,6 +285,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -141,6 +333,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -165,14 +365,54 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
path:
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.19"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -213,6 +453,38 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
recase:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: recase
|
||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -269,11 +541,35 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -282,6 +578,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.9.4"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.42"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.43.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -298,6 +618,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -322,6 +650,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -338,6 +674,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -346,6 +690,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -354,6 +714,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
@@ -38,10 +38,16 @@ dependencies:
|
||||
cupertino_icons: ^1.0.8
|
||||
intl: any
|
||||
shared_preferences: ^2.5.3
|
||||
drift: ^2.31.0
|
||||
sqlite3_flutter_libs: ^0.5.42
|
||||
path_provider: ^2.1.5
|
||||
path: ^1.9.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.10.4
|
||||
drift_dev: ^2.31.0
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:focus_seed/src/data/focus_seed_store.dart';
|
||||
import 'package:focus_seed/src/data/local/focus_seed_database.dart';
|
||||
import 'package:focus_seed/src/models/app_state.dart';
|
||||
import 'package:focus_seed/src/models/focus_session.dart';
|
||||
import 'package:focus_seed/src/models/habit.dart';
|
||||
import 'package:focus_seed/src/models/mood.dart';
|
||||
import 'package:focus_seed/src/models/reflection_entry.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('new installs seed localized starter habits into sqlite', () async {
|
||||
final database = FocusSeedDatabase(NativeDatabase.memory());
|
||||
addTearDown(database.close);
|
||||
final store = FocusSeedStore(database: database);
|
||||
|
||||
final state = await store.load(
|
||||
defaultHabitTitles: const ['Plan', 'Focus', 'Reflect'],
|
||||
);
|
||||
|
||||
expect(state.anchor, isEmpty);
|
||||
expect(state.habits.map((habit) => habit.title), [
|
||||
'Plan',
|
||||
'Focus',
|
||||
'Reflect',
|
||||
]);
|
||||
});
|
||||
|
||||
test(
|
||||
'sqlite snapshot round-trips focus, habit, and reflection data',
|
||||
() async {
|
||||
final database = FocusSeedDatabase(NativeDatabase.memory());
|
||||
addTearDown(database.close);
|
||||
final store = FocusSeedStore(database: database);
|
||||
final now = DateTime(2026, 6, 12, 10, 30);
|
||||
final state = AppState(
|
||||
anchor: 'Ship the local database',
|
||||
anchorDateKey: '2026-06-12',
|
||||
habits: [
|
||||
Habit(
|
||||
id: 'habit-1',
|
||||
title: 'Review the plan',
|
||||
createdAt: now.subtract(const Duration(days: 2)),
|
||||
completedDates: const {'2026-06-11', '2026-06-12'},
|
||||
),
|
||||
],
|
||||
sessions: [
|
||||
FocusSession(id: 'session-1', minutes: 25, completedAt: now),
|
||||
],
|
||||
reflections: [
|
||||
ReflectionEntry(
|
||||
id: 'reflection-1',
|
||||
dateKey: '2026-06-12',
|
||||
note: 'Small scope kept it clean.',
|
||||
mood: Mood.clear,
|
||||
createdAt: now.add(const Duration(minutes: 1)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await store.save(state);
|
||||
final loaded = await store.load();
|
||||
|
||||
expect(loaded.anchor, 'Ship the local database');
|
||||
expect(loaded.anchorDateKey, '2026-06-12');
|
||||
expect(loaded.habits.single.completedDates, {'2026-06-11', '2026-06-12'});
|
||||
expect(loaded.sessions.single.minutes, 25);
|
||||
expect(loaded.reflections.single.note, 'Small scope kept it clean.');
|
||||
},
|
||||
);
|
||||
|
||||
test('legacy shared preferences json migrates once and is removed', () async {
|
||||
final legacyState = AppState(
|
||||
anchor: 'Migrate safely',
|
||||
anchorDateKey: '2026-06-12',
|
||||
habits: [
|
||||
Habit(
|
||||
id: 'habit-legacy',
|
||||
title: 'Keep existing data',
|
||||
createdAt: DateTime(2026, 6, 10),
|
||||
completedDates: const {'2026-06-12'},
|
||||
),
|
||||
],
|
||||
sessions: [
|
||||
FocusSession(
|
||||
id: 'session-legacy',
|
||||
minutes: 15,
|
||||
completedAt: DateTime(2026, 6, 12, 8),
|
||||
),
|
||||
],
|
||||
reflections: const [],
|
||||
);
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'focus_seed_state_v1': jsonEncode(legacyState.toJson()),
|
||||
});
|
||||
final database = FocusSeedDatabase(NativeDatabase.memory());
|
||||
addTearDown(database.close);
|
||||
final store = FocusSeedStore(database: database);
|
||||
|
||||
final migrated = await store.load();
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
|
||||
expect(migrated.anchor, 'Migrate safely');
|
||||
expect(migrated.habits.single.title, 'Keep existing data');
|
||||
expect(migrated.sessions.single.minutes, 15);
|
||||
expect(preferences.getString('focus_seed_state_v1'), isNull);
|
||||
});
|
||||
}
|
||||
+20
-10
@@ -1,6 +1,9 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:focus_seed/src/app/focus_seed_app.dart';
|
||||
import 'package:focus_seed/src/data/focus_seed_store.dart';
|
||||
import 'package:focus_seed/src/data/local/focus_seed_database.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
@@ -14,8 +17,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Today guides the user from intention to focus', (tester) async {
|
||||
await tester.pumpWidget(const FocusSeedApp(locale: Locale('en')));
|
||||
await tester.pumpAndSettle();
|
||||
await pumpFocusSeedApp(tester, locale: const Locale('en'));
|
||||
|
||||
expect(find.text('Focus Seed'), findsOneWidget);
|
||||
expect(find.text("TODAY'S INTENTION"), findsOneWidget);
|
||||
@@ -35,8 +37,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('bottom navigation exposes Journey and Me', (tester) async {
|
||||
await tester.pumpWidget(const FocusSeedApp(locale: Locale('en')));
|
||||
await tester.pumpAndSettle();
|
||||
await pumpFocusSeedApp(tester, locale: const Locale('en'));
|
||||
|
||||
await tester.tap(find.text('Journey'));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -60,8 +61,7 @@ void main() {
|
||||
tester.platformDispatcher.clearTextScaleFactorTestValue();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(const FocusSeedApp(locale: Locale('en')));
|
||||
await tester.pumpAndSettle();
|
||||
await pumpFocusSeedApp(tester, locale: const Locale('en'));
|
||||
|
||||
expect(find.text('Focus Seed'), findsOneWidget);
|
||||
expect(find.byType(Scrollable), findsWidgets);
|
||||
@@ -69,8 +69,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('development default uses concise Chinese copy', (tester) async {
|
||||
await tester.pumpWidget(const FocusSeedApp());
|
||||
await tester.pumpAndSettle();
|
||||
await pumpFocusSeedApp(tester);
|
||||
|
||||
expect(find.text('Focus Seed'), findsOneWidget);
|
||||
expect(find.text('\u4eca\u65e5\u76ee\u6807'), findsOneWidget);
|
||||
@@ -88,8 +87,7 @@ void main() {
|
||||
testWidgets('development language switch changes the active locale', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(const FocusSeedApp());
|
||||
await tester.pumpAndSettle();
|
||||
await pumpFocusSeedApp(tester);
|
||||
|
||||
await tester.tap(find.text('\u6211\u7684'));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -102,3 +100,15 @@ void main() {
|
||||
expect(find.text('Language'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> pumpFocusSeedApp(WidgetTester tester, {Locale? locale}) async {
|
||||
final database = FocusSeedDatabase(NativeDatabase.memory());
|
||||
addTearDown(database.close);
|
||||
await tester.pumpWidget(
|
||||
FocusSeedApp(
|
||||
locale: locale,
|
||||
focusSeedStore: FocusSeedStore(database: database),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user