- 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)
180 lines
5.5 KiB
Dart
180 lines
5.5 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|