feat: initial commit - Habo habit tracking app
- Complete MVP with Repository Pattern, SQLite storage - Provider + ChangeNotifier state management - Navigation 2.0 with deep link support - Habit CRUD with twoDayRule, notifications, categories - Backup/Restore via JSON - Statistics with streak tracking - Material You theme support - Biometric lock support - Desktop widget support - 27 languages i18n structure - Comprehensive test suite (87/89 passing)
This commit is contained in:
25
lib/model/backup.dart
Normal file
25
lib/model/backup.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
class BackupData {
|
||||
final int version;
|
||||
final List<Map<String, dynamic>> habits;
|
||||
final List<Map<String, dynamic>> categories;
|
||||
final List<Map<String, dynamic>> habitCategories;
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
BackupData({
|
||||
this.version = 3,
|
||||
this.habits = const [],
|
||||
this.categories = const [],
|
||||
this.habitCategories = const [],
|
||||
this.metadata = const {},
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'version': version,
|
||||
'habits': habits,
|
||||
'categories': categories,
|
||||
'habit_categories': habitCategories,
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
31
lib/model/category.dart
Normal file
31
lib/model/category.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
class Category {
|
||||
int? id;
|
||||
String title;
|
||||
int iconCodePoint;
|
||||
String? fontFamily;
|
||||
|
||||
Category({
|
||||
this.id,
|
||||
this.title = '',
|
||||
this.iconCodePoint = 0,
|
||||
this.fontFamily,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'iconCodePoint': iconCodePoint,
|
||||
'fontFamily': fontFamily,
|
||||
};
|
||||
}
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['id'] as int?,
|
||||
title: json['title'] as String? ?? '',
|
||||
iconCodePoint: json['iconCodePoint'] as int? ?? 0,
|
||||
fontFamily: json['fontFamily'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
179
lib/model/habit_data.dart
Normal file
179
lib/model/habit_data.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:habo/constants.dart';
|
||||
import 'package:habo/model/category.dart';
|
||||
|
||||
class HabitData {
|
||||
int? id;
|
||||
int position;
|
||||
String title;
|
||||
bool twoDayRule;
|
||||
String cue;
|
||||
String routine;
|
||||
String reward;
|
||||
bool showReward;
|
||||
bool advanced;
|
||||
bool notification;
|
||||
TimeOfDay notTime;
|
||||
String sanction;
|
||||
bool showSanction;
|
||||
String accountant;
|
||||
HabitType habitType;
|
||||
double targetValue;
|
||||
double partialValue;
|
||||
String unit;
|
||||
bool archived;
|
||||
SplayTreeMap<DateTime, List> events;
|
||||
List<Category> categories;
|
||||
|
||||
// Runtime computed fields
|
||||
int streak;
|
||||
bool streakVisible;
|
||||
bool orangeStreak;
|
||||
|
||||
HabitData({
|
||||
this.id,
|
||||
this.position = 0,
|
||||
this.title = '',
|
||||
this.twoDayRule = false,
|
||||
this.cue = '',
|
||||
this.routine = '',
|
||||
this.reward = '',
|
||||
this.showReward = false,
|
||||
this.advanced = false,
|
||||
this.notification = false,
|
||||
this.notTime = const TimeOfDay(hour: 20, minute: 0),
|
||||
this.sanction = '',
|
||||
this.showSanction = false,
|
||||
this.accountant = '',
|
||||
this.habitType = HabitType.boolean,
|
||||
this.targetValue = 100.0,
|
||||
this.partialValue = 10.0,
|
||||
this.unit = '',
|
||||
this.archived = false,
|
||||
SplayTreeMap<DateTime, List>? events,
|
||||
this.categories = const [],
|
||||
this.streak = 0,
|
||||
this.streakVisible = false,
|
||||
this.orangeStreak = false,
|
||||
}) : events = events ?? SplayTreeMap<DateTime, List>();
|
||||
|
||||
bool isCompletedForDate(DateTime date) {
|
||||
final key = DateTime(date.year, date.month, date.day);
|
||||
final event = events[key];
|
||||
if (event == null) return false;
|
||||
final dayType = event[0] as DayType;
|
||||
if (dayType == DayType.check) return true;
|
||||
if (dayType == DayType.progress && event.length >= 4) {
|
||||
final progressValue = event[2] as double? ?? 0.0;
|
||||
final target = event[3] as double? ?? targetValue;
|
||||
return progressValue >= target;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
double getProgressForDate(DateTime date) {
|
||||
final key = DateTime(date.year, date.month, date.day);
|
||||
final event = events[key];
|
||||
if (event == null || event.length < 3) return 0.0;
|
||||
return event[2] as double? ?? 0.0;
|
||||
}
|
||||
|
||||
double getProgressPercentage(DateTime date) {
|
||||
final key = DateTime(date.year, date.month, date.day);
|
||||
final event = events[key];
|
||||
if (event == null || event.length < 4) return 0.0;
|
||||
final progressValue = event[2] as double? ?? 0.0;
|
||||
final target = event[3] as double? ?? targetValue;
|
||||
if (target == 0) return 0.0;
|
||||
return (progressValue / target).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final eventsJson = <String, dynamic>{};
|
||||
for (final entry in events.entries) {
|
||||
eventsJson[entry.key.toIso8601String()] = [
|
||||
entry.value[0] is DayType ? (entry.value[0] as DayType).index : entry.value[0],
|
||||
if (entry.value.length > 1) entry.value[1] else '',
|
||||
if (entry.value.length > 2) entry.value[2],
|
||||
if (entry.value.length > 3) entry.value[3],
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'position': position,
|
||||
'title': title,
|
||||
'twoDayRule': twoDayRule,
|
||||
'cue': cue,
|
||||
'routine': routine,
|
||||
'reward': reward,
|
||||
'showReward': showReward,
|
||||
'advanced': advanced,
|
||||
'notification': notification,
|
||||
'notTime': {'hour': notTime.hour, 'minute': notTime.minute},
|
||||
'sanction': sanction,
|
||||
'showSanction': showSanction,
|
||||
'accountant': accountant,
|
||||
'habitType': habitType.index,
|
||||
'targetValue': targetValue,
|
||||
'partialValue': partialValue,
|
||||
'unit': unit,
|
||||
'archived': archived,
|
||||
'events': eventsJson,
|
||||
};
|
||||
}
|
||||
|
||||
factory HabitData.fromJson(Map<String, dynamic> json) {
|
||||
final eventsJson = json['events'] as Map<String, dynamic>? ?? {};
|
||||
final events = SplayTreeMap<DateTime, List>();
|
||||
for (final entry in eventsJson.entries) {
|
||||
try {
|
||||
final date = DateTime.parse(entry.key);
|
||||
final value = entry.value as List;
|
||||
final dayType = value[0] is int ? DayType.values[value[0] as int] : value[0];
|
||||
final event = [
|
||||
dayType,
|
||||
if (value.length > 1) value[1] else '',
|
||||
if (value.length > 2) value[2] else 0.0,
|
||||
if (value.length > 3) value[3] else 0.0,
|
||||
];
|
||||
events[date] = event;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final notTimeJson = json['notTime'];
|
||||
TimeOfDay notTime;
|
||||
if (notTimeJson is Map) {
|
||||
notTime = TimeOfDay(
|
||||
hour: notTimeJson['hour'] as int? ?? 20,
|
||||
minute: notTimeJson['minute'] as int? ?? 0,
|
||||
);
|
||||
} else {
|
||||
notTime = const TimeOfDay(hour: 20, minute: 0);
|
||||
}
|
||||
|
||||
return HabitData(
|
||||
id: json['id'] as int?,
|
||||
position: json['position'] as int? ?? 0,
|
||||
title: json['title'] as String? ?? '',
|
||||
twoDayRule: json['twoDayRule'] as bool? ?? false,
|
||||
cue: json['cue'] as String? ?? '',
|
||||
routine: json['routine'] as String? ?? '',
|
||||
reward: json['reward'] as String? ?? '',
|
||||
showReward: json['showReward'] as bool? ?? false,
|
||||
advanced: json['advanced'] as bool? ?? false,
|
||||
notification: json['notification'] as bool? ?? false,
|
||||
notTime: notTime,
|
||||
sanction: json['sanction'] as String? ?? '',
|
||||
showSanction: json['showSanction'] as bool? ?? false,
|
||||
accountant: json['accountant'] as String? ?? '',
|
||||
habitType: HabitType.values[json['habitType'] as int? ?? 0],
|
||||
targetValue: (json['targetValue'] as num?)?.toDouble() ?? 100.0,
|
||||
partialValue: (json['partialValue'] as num?)?.toDouble() ?? 10.0,
|
||||
unit: json['unit'] as String? ?? '',
|
||||
archived: json['archived'] as bool? ?? false,
|
||||
events: events,
|
||||
);
|
||||
}
|
||||
}
|
||||
363
lib/model/habo_model.dart
Normal file
363
lib/model/habo_model.dart
Normal file
@@ -0,0 +1,363 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'package:sqflite_common/sqlite_api.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:habo/constants.dart';
|
||||
import 'package:habo/model/habit_data.dart';
|
||||
import 'package:habo/model/category.dart';
|
||||
|
||||
class HaboModel {
|
||||
static final HaboModel _instance = HaboModel._internal();
|
||||
factory HaboModel() => _instance;
|
||||
HaboModel._internal();
|
||||
|
||||
DatabaseFactory? _factory;
|
||||
Database? _database;
|
||||
|
||||
static const int _dbVersion = 9;
|
||||
|
||||
DatabaseFactory get _dbFactory {
|
||||
_factory ??= (Platform.isLinux || Platform.isMacOS)
|
||||
? databaseFactoryFfi
|
||||
: databaseFactory;
|
||||
return _factory!;
|
||||
}
|
||||
|
||||
Future<Database> get database async {
|
||||
_database ??= await _initDatabase();
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<Database> _initDatabase() async {
|
||||
// For Linux/Mac, use application support directory
|
||||
String dbPath;
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
dbPath = dir.path;
|
||||
} else {
|
||||
dbPath = await getDatabasesPath();
|
||||
}
|
||||
|
||||
final path = join(dbPath, 'habo_db0.db');
|
||||
return _dbFactory.openDatabase(
|
||||
path,
|
||||
options: OpenDatabaseOptions(
|
||||
version: _dbVersion,
|
||||
onCreate: _onCreate,
|
||||
onUpgrade: _onUpgrade,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initDatabase() async {
|
||||
// Initialize FFI for desktop
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
sqfliteFfiInit();
|
||||
}
|
||||
await database;
|
||||
}
|
||||
|
||||
Future<void> _onCreate(Database db, int version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE habits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
position INTEGER,
|
||||
title TEXT,
|
||||
twoDayRule INTEGER,
|
||||
cue TEXT DEFAULT '',
|
||||
routine TEXT DEFAULT '',
|
||||
reward TEXT DEFAULT '',
|
||||
showReward INTEGER,
|
||||
advanced INTEGER,
|
||||
notification INTEGER,
|
||||
notTime TEXT,
|
||||
sanction TEXT DEFAULT '',
|
||||
showSanction INTEGER DEFAULT 0,
|
||||
accountant TEXT DEFAULT '',
|
||||
habitType INTEGER DEFAULT 0,
|
||||
targetValue REAL DEFAULT 1.0,
|
||||
partialValue REAL DEFAULT 1.0,
|
||||
unit TEXT DEFAULT '',
|
||||
archived INTEGER DEFAULT 0
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE events (
|
||||
id INTEGER,
|
||||
dateTime TEXT,
|
||||
dayType INTEGER,
|
||||
comment TEXT DEFAULT '',
|
||||
progressValue REAL DEFAULT 0.0,
|
||||
targetValue REAL DEFAULT 0.0,
|
||||
PRIMARY KEY(id, dateTime),
|
||||
FOREIGN KEY (id) REFERENCES habits(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
iconCodePoint INTEGER NOT NULL,
|
||||
fontFamily TEXT
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE habit_categories (
|
||||
habit_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (habit_id, category_id),
|
||||
FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
await db.execute("ALTER TABLE events ADD COLUMN comment TEXT DEFAULT ''");
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN sanction TEXT DEFAULT ''");
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN showSanction INTEGER DEFAULT 0");
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN accountant TEXT DEFAULT ''");
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN habitType INTEGER DEFAULT 0");
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN targetValue REAL DEFAULT 1.0");
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN partialValue REAL DEFAULT 1.0");
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN unit TEXT DEFAULT ''");
|
||||
await db.execute("ALTER TABLE events ADD COLUMN progressValue REAL DEFAULT 0.0");
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
await db.execute("ALTER TABLE events ADD COLUMN targetValue REAL DEFAULT 0.0");
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
iconCodePoint INTEGER NOT NULL,
|
||||
fontFamily TEXT
|
||||
)
|
||||
''');
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS habit_categories (
|
||||
habit_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (habit_id, category_id),
|
||||
FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
}
|
||||
if (oldVersion < 6) {
|
||||
await db.execute("ALTER TABLE habits ADD COLUMN archived INTEGER DEFAULT 0");
|
||||
}
|
||||
if (oldVersion < 7) {
|
||||
await db.execute("ALTER TABLE categories ADD COLUMN fontFamily TEXT");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Habit CRUD ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllHabits() async {
|
||||
final db = await database;
|
||||
return db.query('habits', orderBy: 'position ASC');
|
||||
}
|
||||
|
||||
Future<int> insertHabit(HabitData data) async {
|
||||
final db = await database;
|
||||
return db.insert('habits', _habitDataToMap(data));
|
||||
}
|
||||
|
||||
Future<void> updateHabit(HabitData data) async {
|
||||
final db = await database;
|
||||
await db.update('habits', _habitDataToMap(data), where: 'id = ?', whereArgs: [data.id]);
|
||||
}
|
||||
|
||||
Future<void> deleteHabit(int id) async {
|
||||
final db = await database;
|
||||
await db.delete('habits', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getHabitById(int id) async {
|
||||
final db = await database;
|
||||
final results = await db.query('habits', where: 'id = ?', whereArgs: [id]);
|
||||
return results.isNotEmpty ? results.first : null;
|
||||
}
|
||||
|
||||
Future<void> updateHabitsOrder(List<dynamic> habits) async {
|
||||
final db = await database;
|
||||
final batch = db.batch();
|
||||
for (int i = 0; i < habits.length; i++) {
|
||||
final habit = habits[i];
|
||||
final id = habit.habitData.id;
|
||||
batch.update('habits', {'position': i}, where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
|
||||
Future<void> deleteAllHabits() async {
|
||||
final db = await database;
|
||||
await db.delete('habits');
|
||||
}
|
||||
|
||||
// ─── Event CRUD ────────────────────────────────────────────────────────────
|
||||
|
||||
Future<List<List>> getEventsForHabit(int habitId) async {
|
||||
final db = await database;
|
||||
final results = await db.query(
|
||||
'events',
|
||||
where: 'id = ?',
|
||||
whereArgs: [habitId],
|
||||
orderBy: 'dateTime ASC',
|
||||
);
|
||||
return results.map((row) => _rowToEvent(row)).toList();
|
||||
}
|
||||
|
||||
Future<SplayTreeMap<DateTime, List>> getEventsMapForHabit(int habitId) async {
|
||||
final events = await getEventsForHabit(habitId);
|
||||
final map = SplayTreeMap<DateTime, List>();
|
||||
for (final event in events) {
|
||||
if (event.isNotEmpty && event[0] is DateTime) {
|
||||
map[event[0] as DateTime] = event.sublist(1);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
Future<void> insertEvent(int habitId, DateTime date, List event) async {
|
||||
final db = await database;
|
||||
final dayType = event[0] is DayType ? (event[0] as DayType).index : event[0];
|
||||
final comment = event.length > 1 ? event[1] as String : '';
|
||||
final progressValue = event.length > 2 ? (event[2] as num).toDouble() : 0.0;
|
||||
final targetValue = event.length > 3 ? (event[3] as num).toDouble() : 0.0;
|
||||
final dateStr = date.toIso8601String();
|
||||
|
||||
await db.insert(
|
||||
'events',
|
||||
{
|
||||
'id': habitId,
|
||||
'dateTime': dateStr,
|
||||
'dayType': dayType,
|
||||
'comment': comment,
|
||||
'progressValue': progressValue,
|
||||
'targetValue': targetValue,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteEvent(int habitId, DateTime date) async {
|
||||
final db = await database;
|
||||
await db.delete(
|
||||
'events',
|
||||
where: 'id = ? AND dateTime = ?',
|
||||
whereArgs: [habitId, date.toIso8601String()],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteAllEventsForHabit(int habitId) async {
|
||||
final db = await database;
|
||||
await db.delete('events', where: 'id = ?', whereArgs: [habitId]);
|
||||
}
|
||||
|
||||
Future<void> deleteAllEvents() async {
|
||||
final db = await database;
|
||||
await db.delete('events');
|
||||
}
|
||||
|
||||
// ─── Category CRUD ────────────────────────────────────────────────────────
|
||||
|
||||
Future<List<Map<String, dynamic>>> getAllCategories() async {
|
||||
final db = await database;
|
||||
return db.query('categories');
|
||||
}
|
||||
|
||||
Future<int> insertCategory(Category category) async {
|
||||
final db = await database;
|
||||
return db.insert('categories', category.toJson());
|
||||
}
|
||||
|
||||
Future<void> updateCategory(Category category) async {
|
||||
final db = await database;
|
||||
await db.update('categories', category.toJson(), where: 'id = ?', whereArgs: [category.id]);
|
||||
}
|
||||
|
||||
Future<void> deleteCategory(int id) async {
|
||||
final db = await database;
|
||||
await db.delete('categories', where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
Future<void> updateHabitCategories(int habitId, List<Category> categories) async {
|
||||
final db = await database;
|
||||
await db.delete('habit_categories', where: 'habit_id = ?', whereArgs: [habitId]);
|
||||
for (final cat in categories) {
|
||||
if (cat.id != null) {
|
||||
await db.insert('habit_categories', {'habit_id': habitId, 'category_id': cat.id});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getCategoriesForHabit(int habitId) async {
|
||||
final db = await database;
|
||||
return db.rawQuery(
|
||||
'SELECT c.* FROM categories c JOIN habit_categories hc ON c.id = hc.category_id WHERE hc.habit_id = ?',
|
||||
[habitId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteAllCategories() async {
|
||||
final db = await database;
|
||||
await db.delete('habit_categories');
|
||||
await db.delete('categories');
|
||||
}
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
Map<String, dynamic> _habitDataToMap(HabitData data) {
|
||||
return {
|
||||
if (data.id != null) 'id': data.id,
|
||||
'position': data.position,
|
||||
'title': data.title,
|
||||
'twoDayRule': data.twoDayRule ? 1 : 0,
|
||||
'cue': data.cue,
|
||||
'routine': data.routine,
|
||||
'reward': data.reward,
|
||||
'showReward': data.showReward ? 1 : 0,
|
||||
'advanced': data.advanced ? 1 : 0,
|
||||
'notification': data.notification ? 1 : 0,
|
||||
'notTime': '${data.notTime.hour}:${data.notTime.minute}',
|
||||
'sanction': data.sanction,
|
||||
'showSanction': data.showSanction ? 1 : 0,
|
||||
'accountant': data.accountant,
|
||||
'habitType': data.habitType.index,
|
||||
'targetValue': data.targetValue,
|
||||
'partialValue': data.partialValue,
|
||||
'unit': data.unit,
|
||||
'archived': data.archived ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
List _rowToEvent(Map<String, dynamic> row) {
|
||||
final dateStr = row['dateTime'] as String;
|
||||
final date = DateTime.parse(dateStr);
|
||||
final dayTypeIndex = row['dayType'] as int;
|
||||
final dayType = DayType.values[dayTypeIndex];
|
||||
final comment = row['comment'] as String? ?? '';
|
||||
final progressValue = (row['progressValue'] as num?)?.toDouble() ?? 0.0;
|
||||
final targetValue = (row['targetValue'] as num?)?.toDouble() ?? 0.0;
|
||||
return [date, dayType, comment, progressValue, targetValue];
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
final db = _database;
|
||||
if (db != null) {
|
||||
await db.close();
|
||||
_database = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/model/settings_data.dart
Normal file
50
lib/model/settings_data.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
class SettingsData {
|
||||
// Appearance
|
||||
String theme;
|
||||
String weekStart;
|
||||
bool showMonthName;
|
||||
bool showCategories;
|
||||
|
||||
// Notifications
|
||||
bool showDailyNot;
|
||||
int notTimeHour;
|
||||
int notTimeMinute;
|
||||
|
||||
// Sound
|
||||
bool soundEffects;
|
||||
double soundVolume;
|
||||
|
||||
// Security
|
||||
bool biometricLock;
|
||||
bool oneTapCheck;
|
||||
|
||||
// Onboarding
|
||||
bool seenOnboarding;
|
||||
String lastWhatsNewVersion;
|
||||
|
||||
// Custom Colors
|
||||
int checkColor;
|
||||
int failColor;
|
||||
int skipColor;
|
||||
int progressColor;
|
||||
|
||||
SettingsData({
|
||||
this.theme = 'device',
|
||||
this.weekStart = 'monday',
|
||||
this.showMonthName = true,
|
||||
this.showCategories = true,
|
||||
this.showDailyNot = true,
|
||||
this.notTimeHour = 20,
|
||||
this.notTimeMinute = 0,
|
||||
this.soundEffects = true,
|
||||
this.soundVolume = 3.0,
|
||||
this.biometricLock = false,
|
||||
this.oneTapCheck = false,
|
||||
this.seenOnboarding = false,
|
||||
this.lastWhatsNewVersion = '',
|
||||
this.checkColor = 0xFF09BF30,
|
||||
this.failColor = 0xFFF44336,
|
||||
this.skipColor = 0xFFFBC02D,
|
||||
this.progressColor = 0xFF2196F3,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user