Files
habo/lib/model/habit_data.dart
dazhuang aa69f2a91e 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)
2026-04-13 15:02:30 +00:00

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,
);
}
}