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:
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user