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:
11
lib/repositories/backup_repository.dart
Normal file
11
lib/repositories/backup_repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
abstract class BackupRepository {
|
||||
Future<Map<String, dynamic>> exportAllData();
|
||||
Future<void> importData(Map<String, dynamic> data);
|
||||
Future<int> getDatabaseVersion();
|
||||
Future<String> getDatabasePath();
|
||||
Future<void> closeDatabase();
|
||||
Future<void> reopenDatabase();
|
||||
Future<int> getHabitCount();
|
||||
Future<int> getEventCount();
|
||||
Future<bool> validateDatabaseIntegrity();
|
||||
}
|
||||
11
lib/repositories/category_repository.dart
Normal file
11
lib/repositories/category_repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:habo/model/category.dart';
|
||||
|
||||
abstract class CategoryRepository {
|
||||
Future<List<Category>> getAllCategories();
|
||||
Future<int> createCategory(Category category);
|
||||
Future<void> updateCategory(Category category);
|
||||
Future<void> deleteCategory(int id);
|
||||
Future<void> updateHabitCategories(int habitId, List<Category> categories);
|
||||
Future<List<Category>> getCategoriesForHabit(int habitId);
|
||||
Future<void> deleteAllCategories();
|
||||
}
|
||||
11
lib/repositories/event_repository.dart
Normal file
11
lib/repositories/event_repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'dart:collection';
|
||||
|
||||
abstract class EventRepository {
|
||||
Future<List<List>> getEventsForHabit(int habitId);
|
||||
Future<SplayTreeMap<DateTime, List>> getEventsMapForHabit(int habitId);
|
||||
Future<void> insertEvent(int habitId, DateTime date, List event);
|
||||
Future<void> deleteEvent(int habitId, DateTime date);
|
||||
Future<void> deleteAllEventsForHabit(int habitId);
|
||||
Future<void> insertEventsForHabit(int habitId, Map<DateTime, List> events);
|
||||
Future<void> deleteAllEvents();
|
||||
}
|
||||
12
lib/repositories/habit_repository.dart
Normal file
12
lib/repositories/habit_repository.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'package:habo/habits/habit.dart';
|
||||
|
||||
abstract class HabitRepository {
|
||||
Future<List<Habit>> getAllHabits();
|
||||
Future<int> createHabit(Habit habit);
|
||||
Future<void> updateHabit(Habit habit);
|
||||
Future<void> deleteHabit(int id);
|
||||
Future<Habit?> findHabitById(int id);
|
||||
Future<void> updateHabitsOrder(List<Habit> habits);
|
||||
Future<void> deleteAllHabits();
|
||||
Future<void> insertHabits(List<Habit> habits);
|
||||
}
|
||||
32
lib/repositories/repository_factory.dart
Normal file
32
lib/repositories/repository_factory.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:habo/model/habo_model.dart';
|
||||
import 'package:habo/repositories/habit_repository.dart';
|
||||
import 'package:habo/repositories/event_repository.dart';
|
||||
import 'package:habo/repositories/category_repository.dart';
|
||||
import 'package:habo/repositories/sqlite_habit_repository.dart';
|
||||
import 'package:habo/repositories/sqlite_event_repository.dart';
|
||||
import 'package:habo/repositories/sqlite_category_repository.dart';
|
||||
|
||||
class RepositoryFactory {
|
||||
final HaboModel _model;
|
||||
|
||||
HabitRepository? _habitRepository;
|
||||
EventRepository? _eventRepository;
|
||||
CategoryRepository? _categoryRepository;
|
||||
|
||||
RepositoryFactory(this._model);
|
||||
|
||||
HabitRepository get habitRepository {
|
||||
_habitRepository ??= SqliteHabitRepository(_model);
|
||||
return _habitRepository!;
|
||||
}
|
||||
|
||||
EventRepository get eventRepository {
|
||||
_eventRepository ??= SqliteEventRepository(_model);
|
||||
return _eventRepository!;
|
||||
}
|
||||
|
||||
CategoryRepository get categoryRepository {
|
||||
_categoryRepository ??= SqliteCategoryRepository(_model);
|
||||
return _categoryRepository!;
|
||||
}
|
||||
}
|
||||
56
lib/repositories/sqlite_category_repository.dart
Normal file
56
lib/repositories/sqlite_category_repository.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:habo/model/category.dart';
|
||||
import 'package:habo/model/habo_model.dart';
|
||||
import 'package:habo/repositories/category_repository.dart';
|
||||
|
||||
class SqliteCategoryRepository implements CategoryRepository {
|
||||
final HaboModel _model;
|
||||
|
||||
SqliteCategoryRepository(this._model);
|
||||
|
||||
@override
|
||||
Future<List<Category>> getAllCategories() async {
|
||||
final maps = await _model.getAllCategories();
|
||||
return maps.map((map) => Category(
|
||||
id: map['id'] as int?,
|
||||
title: map['title'] as String? ?? '',
|
||||
iconCodePoint: map['iconCodePoint'] as int? ?? 0,
|
||||
fontFamily: map['fontFamily'] as String?,
|
||||
)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> createCategory(Category category) async {
|
||||
return _model.insertCategory(category);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateCategory(Category category) async {
|
||||
await _model.updateCategory(category);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteCategory(int id) async {
|
||||
await _model.deleteCategory(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateHabitCategories(int habitId, List<Category> categories) async {
|
||||
await _model.updateHabitCategories(habitId, categories);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Category>> getCategoriesForHabit(int habitId) async {
|
||||
final maps = await _model.getCategoriesForHabit(habitId);
|
||||
return maps.map((map) => Category(
|
||||
id: map['id'] as int?,
|
||||
title: map['title'] as String? ?? '',
|
||||
iconCodePoint: map['iconCodePoint'] as int? ?? 0,
|
||||
fontFamily: map['fontFamily'] as String?,
|
||||
)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAllCategories() async {
|
||||
await _model.deleteAllCategories();
|
||||
}
|
||||
}
|
||||
47
lib/repositories/sqlite_event_repository.dart
Normal file
47
lib/repositories/sqlite_event_repository.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'dart:collection';
|
||||
import 'package:habo/constants.dart';
|
||||
import 'package:habo/model/habo_model.dart';
|
||||
import 'package:habo/repositories/event_repository.dart';
|
||||
|
||||
class SqliteEventRepository implements EventRepository {
|
||||
final HaboModel _model;
|
||||
|
||||
SqliteEventRepository(this._model);
|
||||
|
||||
@override
|
||||
Future<List<List>> getEventsForHabit(int habitId) async {
|
||||
return _model.getEventsForHabit(habitId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SplayTreeMap<DateTime, List>> getEventsMapForHabit(int habitId) async {
|
||||
return _model.getEventsMapForHabit(habitId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertEvent(int habitId, DateTime date, List event) async {
|
||||
await _model.insertEvent(habitId, date, event);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteEvent(int habitId, DateTime date) async {
|
||||
await _model.deleteEvent(habitId, date);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAllEventsForHabit(int habitId) async {
|
||||
await _model.deleteAllEventsForHabit(habitId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertEventsForHabit(int habitId, Map<DateTime, List> events) async {
|
||||
for (final entry in events.entries) {
|
||||
await _model.insertEvent(habitId, entry.key, entry.value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAllEvents() async {
|
||||
await _model.deleteAllEvents();
|
||||
}
|
||||
}
|
||||
105
lib/repositories/sqlite_habit_repository.dart
Normal file
105
lib/repositories/sqlite_habit_repository.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:habo/constants.dart';
|
||||
import 'package:habo/habits/habit.dart';
|
||||
import 'package:habo/model/habit_data.dart';
|
||||
import 'package:habo/model/habo_model.dart';
|
||||
import 'package:habo/repositories/habit_repository.dart';
|
||||
|
||||
class SqliteHabitRepository implements HabitRepository {
|
||||
final HaboModel _model;
|
||||
|
||||
SqliteHabitRepository(this._model);
|
||||
|
||||
@override
|
||||
Future<List<Habit>> getAllHabits() async {
|
||||
final maps = await _model.getAllHabits();
|
||||
return maps.map((map) {
|
||||
final data = _mapToHabitData(map);
|
||||
return Habit(habitData: data);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> createHabit(Habit habit) async {
|
||||
return _model.insertHabit(habit.habitData);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateHabit(Habit habit) async {
|
||||
await _model.updateHabit(habit.habitData);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteHabit(int id) async {
|
||||
await _model.deleteHabit(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Habit?> findHabitById(int id) async {
|
||||
final map = await _model.getHabitById(id);
|
||||
if (map == null) return null;
|
||||
final data = _mapToHabitData(map);
|
||||
return Habit(habitData: data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateHabitsOrder(List<Habit> habits) async {
|
||||
await _model.updateHabitsOrder(habits);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAllHabits() async {
|
||||
await _model.deleteAllHabits();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertHabits(List<Habit> habits) async {
|
||||
for (final habit in habits) {
|
||||
await _model.insertHabit(habit.habitData);
|
||||
}
|
||||
}
|
||||
|
||||
HabitData _mapToHabitData(Map<String, dynamic> map) {
|
||||
return HabitData(
|
||||
id: map['id'] as int?,
|
||||
position: map['position'] as int? ?? 0,
|
||||
title: map['title'] as String? ?? '',
|
||||
twoDayRule: (map['twoDayRule'] as int? ?? 0) == 1,
|
||||
cue: map['cue'] as String? ?? '',
|
||||
routine: map['routine'] as String? ?? '',
|
||||
reward: map['reward'] as String? ?? '',
|
||||
showReward: (map['showReward'] as int? ?? 0) == 1,
|
||||
advanced: (map['advanced'] as int? ?? 0) == 1,
|
||||
notification: (map['notification'] as int? ?? 0) == 1,
|
||||
notTime: _parseTimeOfDay(map['notTime'] as String? ?? ''),
|
||||
sanction: map['sanction'] as String? ?? '',
|
||||
showSanction: (map['showSanction'] as int? ?? 0) == 1,
|
||||
accountant: map['accountant'] as String? ?? '',
|
||||
habitType: _indexToHabitType(map['habitType'] as int? ?? 0),
|
||||
targetValue: (map['targetValue'] as num?)?.toDouble() ?? 100.0,
|
||||
partialValue: (map['partialValue'] as num?)?.toDouble() ?? 10.0,
|
||||
unit: map['unit'] as String? ?? '',
|
||||
archived: (map['archived'] as int? ?? 0) == 1,
|
||||
);
|
||||
}
|
||||
|
||||
HabitType _indexToHabitType(int index) {
|
||||
if (index >= 0 && index < HabitType.values.length) {
|
||||
return HabitType.values[index];
|
||||
}
|
||||
return HabitType.boolean;
|
||||
}
|
||||
|
||||
TimeOfDay _parseTimeOfDay(String timeStr) {
|
||||
if (timeStr.isEmpty) return const TimeOfDay(hour: 20, minute: 0);
|
||||
try {
|
||||
final parts = timeStr.split(':');
|
||||
return TimeOfDay(
|
||||
hour: int.parse(parts[0]),
|
||||
minute: int.parse(parts[1]),
|
||||
);
|
||||
} catch (_) {
|
||||
return const TimeOfDay(hour: 20, minute: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user