When tapping Start with existing saved runs, shows dialog asking whether to continue from last run or start fresh. Continuing pre-fills all profile and behavior inputs from the saved run. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
310 lines
8.7 KiB
Dart
310 lines
8.7 KiB
Dart
import 'dart:convert';
|
|
import 'package:sqflite/sqflite.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import '../models/models.dart';
|
|
|
|
class LocalStorage {
|
|
static const _dbName = 'add_months.db';
|
|
static const _tableName = 'user_data';
|
|
static const _savedRunsTable = 'saved_runs';
|
|
static const _version = 2;
|
|
|
|
static Database? _database;
|
|
static const _uuid = Uuid();
|
|
|
|
static Future<Database> get database async {
|
|
if (_database != null) return _database!;
|
|
_database = await _initDatabase();
|
|
return _database!;
|
|
}
|
|
|
|
static Future<Database> _initDatabase() async {
|
|
final dbPath = await getDatabasesPath();
|
|
final path = join(dbPath, _dbName);
|
|
|
|
return await openDatabase(
|
|
path,
|
|
version: _version,
|
|
onCreate: (db, version) async {
|
|
await db.execute('''
|
|
CREATE TABLE $_tableName (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
)
|
|
''');
|
|
await db.execute('''
|
|
CREATE TABLE $_savedRunsTable (
|
|
id TEXT PRIMARY KEY,
|
|
label TEXT NOT NULL,
|
|
result TEXT NOT NULL,
|
|
profile TEXT NOT NULL,
|
|
behaviors TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL
|
|
)
|
|
''');
|
|
},
|
|
onUpgrade: (db, oldVersion, newVersion) async {
|
|
if (oldVersion < 2) {
|
|
await db.execute('''
|
|
CREATE TABLE $_savedRunsTable (
|
|
id TEXT PRIMARY KEY,
|
|
label TEXT NOT NULL,
|
|
result TEXT NOT NULL,
|
|
profile TEXT NOT NULL,
|
|
behaviors TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL
|
|
)
|
|
''');
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
// Generic key-value operations
|
|
static Future<void> _put(String key, Map<String, dynamic> value) async {
|
|
final db = await database;
|
|
await db.insert(
|
|
_tableName,
|
|
{
|
|
'key': key,
|
|
'value': jsonEncode(value),
|
|
'updated_at': DateTime.now().millisecondsSinceEpoch,
|
|
},
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> _get(String key) async {
|
|
final db = await database;
|
|
final results = await db.query(
|
|
_tableName,
|
|
where: 'key = ?',
|
|
whereArgs: [key],
|
|
);
|
|
|
|
if (results.isEmpty) return null;
|
|
return jsonDecode(results.first['value'] as String) as Map<String, dynamic>;
|
|
}
|
|
|
|
// Profile operations
|
|
static Future<void> saveProfile(UserProfile profile) async {
|
|
await _put('profile', profile.toJson());
|
|
}
|
|
|
|
static Future<UserProfile?> getProfile() async {
|
|
final json = await _get('profile');
|
|
if (json == null) return null;
|
|
return UserProfile.fromJson(json);
|
|
}
|
|
|
|
// Behavioral inputs operations
|
|
static Future<void> saveBehaviors(BehavioralInputs behaviors) async {
|
|
await _put('behaviors', behaviors.toJson());
|
|
}
|
|
|
|
static Future<BehavioralInputs?> getBehaviors() async {
|
|
final json = await _get('behaviors');
|
|
if (json == null) return null;
|
|
return BehavioralInputs.fromJson(json);
|
|
}
|
|
|
|
// Result operations
|
|
static Future<void> saveResult(CalculationResult result) async {
|
|
await _put('lastResult', result.toJson());
|
|
}
|
|
|
|
static Future<CalculationResult?> getLastResult() async {
|
|
final json = await _get('lastResult');
|
|
if (json == null) return null;
|
|
return CalculationResult.fromJson(json);
|
|
}
|
|
|
|
// Check if user has completed setup
|
|
static Future<bool> hasCompletedSetup() async {
|
|
final profile = await getProfile();
|
|
final behaviors = await getBehaviors();
|
|
return profile != null && behaviors != null;
|
|
}
|
|
|
|
// Delete all data (including saved runs)
|
|
static Future<void> deleteAllData() async {
|
|
final db = await database;
|
|
await db.delete(_tableName);
|
|
await db.delete(_savedRunsTable);
|
|
}
|
|
|
|
// Get last updated timestamp
|
|
static Future<DateTime?> getLastUpdated() async {
|
|
final db = await database;
|
|
final results = await db.query(
|
|
_tableName,
|
|
columns: ['updated_at'],
|
|
orderBy: 'updated_at DESC',
|
|
limit: 1,
|
|
);
|
|
|
|
if (results.isEmpty) return null;
|
|
return DateTime.fromMillisecondsSinceEpoch(
|
|
results.first['updated_at'] as int,
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// Saved Runs operations
|
|
// ============================================
|
|
|
|
static Future<String> saveSavedRun({
|
|
required String label,
|
|
required CalculationResult result,
|
|
required UserProfile profile,
|
|
required BehavioralInputs behaviors,
|
|
}) async {
|
|
final db = await database;
|
|
final id = _uuid.v4();
|
|
final now = DateTime.now();
|
|
|
|
await db.insert(_savedRunsTable, {
|
|
'id': id,
|
|
'label': label,
|
|
'result': jsonEncode(result.toJson()),
|
|
'profile': jsonEncode(profile.toJson()),
|
|
'behaviors': jsonEncode(behaviors.toJson()),
|
|
'created_at': now.millisecondsSinceEpoch,
|
|
});
|
|
|
|
return id;
|
|
}
|
|
|
|
static Future<List<SavedRun>> getSavedRuns() async {
|
|
final db = await database;
|
|
final results = await db.query(
|
|
_savedRunsTable,
|
|
orderBy: 'created_at DESC',
|
|
);
|
|
|
|
return results.map((row) => SavedRun(
|
|
id: row['id'] as String,
|
|
label: row['label'] as String,
|
|
result: CalculationResult.fromJson(
|
|
jsonDecode(row['result'] as String) as Map<String, dynamic>,
|
|
),
|
|
profile: UserProfile.fromJson(
|
|
jsonDecode(row['profile'] as String) as Map<String, dynamic>,
|
|
),
|
|
behaviors: BehavioralInputs.fromJson(
|
|
jsonDecode(row['behaviors'] as String) as Map<String, dynamic>,
|
|
),
|
|
createdAt: DateTime.fromMillisecondsSinceEpoch(row['created_at'] as int),
|
|
)).toList();
|
|
}
|
|
|
|
static Future<SavedRun?> getSavedRun(String id) async {
|
|
final db = await database;
|
|
final results = await db.query(
|
|
_savedRunsTable,
|
|
where: 'id = ?',
|
|
whereArgs: [id],
|
|
);
|
|
|
|
if (results.isEmpty) return null;
|
|
|
|
final row = results.first;
|
|
return SavedRun(
|
|
id: row['id'] as String,
|
|
label: row['label'] as String,
|
|
result: CalculationResult.fromJson(
|
|
jsonDecode(row['result'] as String) as Map<String, dynamic>,
|
|
),
|
|
profile: UserProfile.fromJson(
|
|
jsonDecode(row['profile'] as String) as Map<String, dynamic>,
|
|
),
|
|
behaviors: BehavioralInputs.fromJson(
|
|
jsonDecode(row['behaviors'] as String) as Map<String, dynamic>,
|
|
),
|
|
createdAt: DateTime.fromMillisecondsSinceEpoch(row['created_at'] as int),
|
|
);
|
|
}
|
|
|
|
static Future<void> updateSavedRunLabel(String id, String newLabel) async {
|
|
final db = await database;
|
|
await db.update(
|
|
_savedRunsTable,
|
|
{'label': newLabel},
|
|
where: 'id = ?',
|
|
whereArgs: [id],
|
|
);
|
|
}
|
|
|
|
static Future<void> deleteSavedRun(String id) async {
|
|
final db = await database;
|
|
await db.delete(
|
|
_savedRunsTable,
|
|
where: 'id = ?',
|
|
whereArgs: [id],
|
|
);
|
|
}
|
|
|
|
static Future<void> deleteAllSavedRuns() async {
|
|
final db = await database;
|
|
await db.delete(_savedRunsTable);
|
|
}
|
|
|
|
static Future<int> getSavedRunsCount() async {
|
|
final db = await database;
|
|
final result = await db.rawQuery('SELECT COUNT(*) as count FROM $_savedRunsTable');
|
|
return result.first['count'] as int;
|
|
}
|
|
|
|
static Future<SavedRun?> getMostRecentSavedRun() async {
|
|
final db = await database;
|
|
final results = await db.query(
|
|
_savedRunsTable,
|
|
orderBy: 'created_at DESC',
|
|
limit: 1,
|
|
);
|
|
|
|
if (results.isEmpty) return null;
|
|
|
|
final row = results.first;
|
|
return SavedRun(
|
|
id: row['id'] as String,
|
|
label: row['label'] as String,
|
|
result: CalculationResult.fromJson(
|
|
jsonDecode(row['result'] as String) as Map<String, dynamic>,
|
|
),
|
|
profile: UserProfile.fromJson(
|
|
jsonDecode(row['profile'] as String) as Map<String, dynamic>,
|
|
),
|
|
behaviors: BehavioralInputs.fromJson(
|
|
jsonDecode(row['behaviors'] as String) as Map<String, dynamic>,
|
|
),
|
|
createdAt: DateTime.fromMillisecondsSinceEpoch(row['created_at'] as int),
|
|
);
|
|
}
|
|
|
|
// ============================================
|
|
// Unit preference operations
|
|
// ============================================
|
|
|
|
static Future<void> setUseMetricUnits(bool useMetric) async {
|
|
await _put('useMetricUnits', {'value': useMetric});
|
|
}
|
|
|
|
static Future<bool> getUseMetricUnits() async {
|
|
final json = await _get('useMetricUnits');
|
|
if (json == null) return false; // Default to imperial (US)
|
|
return json['value'] as bool;
|
|
}
|
|
|
|
static Future<void> setSkipForever(bool skip) async {
|
|
await _put('skipForever', {'value': skip});
|
|
}
|
|
|
|
static Future<bool> getSkipForever() async {
|
|
final json = await _get('skipForever');
|
|
if (json == null) return false;
|
|
return json['value'] as bool;
|
|
}
|
|
}
|