343 lines
8.2 KiB
Dart
343 lines
8.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:big_fraction/big_fraction.dart';
|
|
|
|
import 'package:flutter_heatmap_calendar/flutter_heatmap_calendar.dart';
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
class ReadWrite {
|
|
static Future<File> get_history_file() async {
|
|
Directory? directory = await getApplicationDocumentsDirectory();
|
|
|
|
return File("${directory.path}/history.json");
|
|
}
|
|
|
|
static Future<File> get_first_time_file() async {
|
|
Directory? directory = await getApplicationDocumentsDirectory();
|
|
|
|
return File("${directory.path}/first-time.json");
|
|
}
|
|
|
|
static Future<List<DateTime>> read_history() async {
|
|
final file = await get_history_file();
|
|
|
|
List<List<int>> tuple_history = [];
|
|
|
|
try {
|
|
var text = await file.readAsString();
|
|
|
|
List<dynamic> temp = jsonDecode(text);
|
|
|
|
for (dynamic e in temp) {
|
|
List<dynamic> l = e;
|
|
|
|
int a = l[0];
|
|
int b = l[1];
|
|
int c = l[2];
|
|
|
|
tuple_history.add([a, b, c]);
|
|
}
|
|
|
|
print('history found, and it has ${tuple_history.length} elements');
|
|
} on PathNotFoundException {
|
|
print('no history found');
|
|
}
|
|
|
|
var history = <DateTime>[];
|
|
|
|
for (var e in tuple_history) {
|
|
history.add(DateTime(e[0], e[1], e[2]));
|
|
}
|
|
|
|
return history;
|
|
}
|
|
|
|
static Future<Null> writeTextFile(List<DateTime> history) async {
|
|
final file = await get_history_file();
|
|
|
|
var tuple_history = <List<int>>[];
|
|
|
|
for (DateTime dt in history) {
|
|
tuple_history.add([dt.year, dt.month, dt.day]);
|
|
}
|
|
|
|
file.writeAsString(jsonEncode(tuple_history));
|
|
|
|
return null;
|
|
}
|
|
|
|
static Future<DateTime> get_first_time() async {
|
|
final file = await get_first_time_file();
|
|
|
|
DateTime first_time;
|
|
|
|
try {
|
|
var text = await file.readAsString();
|
|
|
|
List<dynamic> readable = jsonDecode(text);
|
|
|
|
int year = readable[0];
|
|
int month = readable[1];
|
|
int day = readable[2];
|
|
|
|
first_time = DateTime(year, month, day);
|
|
|
|
print('first time day found');
|
|
} on PathNotFoundException {
|
|
print('no first time day found, assuming today');
|
|
|
|
DateTime now = DateTime.now();
|
|
|
|
List<int> writeable = <int>[now.year, now.month, now.day];
|
|
|
|
file.writeAsString(jsonEncode(writeable));
|
|
|
|
first_time = DateTime(now.year, now.month, now.day);
|
|
}
|
|
|
|
return first_time;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
class MyAppState extends ChangeNotifier {
|
|
var history = <DateTime>[];
|
|
|
|
DateTime first_time = DateTime(1999, 1, 1);
|
|
|
|
var percentage = 0.0;
|
|
|
|
MyAppState() {
|
|
ReadWrite.read_history().then((x) {
|
|
history = x;
|
|
|
|
recalculate_percentage();
|
|
|
|
notifyListeners();
|
|
});
|
|
|
|
ReadWrite.get_first_time().then((x) {
|
|
first_time = x;
|
|
|
|
recalculate_percentage();
|
|
|
|
notifyListeners();
|
|
});
|
|
}
|
|
|
|
void recalculate_percentage() {
|
|
var now = DateTime.now();
|
|
|
|
now = DateTime(now.year, now.month, now.day);
|
|
|
|
var cutoff = DateTime(now.year, now.month, now.day - 120);
|
|
|
|
while (history.length > 0 && history[0].isBefore(cutoff)) {
|
|
history.removeAt(0);
|
|
}
|
|
|
|
if (history.length == 0) {
|
|
percentage = 0.0;
|
|
} else if (first_time.isAfter(cutoff)) {
|
|
// print("line 177: ${history.length} / ${now.difference(first_time).inDays}");
|
|
|
|
percentage = history.length / (now.difference(first_time).inDays + 1);
|
|
} else {
|
|
percentage = history.length / 120;
|
|
}
|
|
|
|
// print("percentage = ${percentage}");
|
|
}
|
|
|
|
void incrementCounter() async {
|
|
var now = DateTime.now();
|
|
|
|
now = DateTime(now.year, now.month, now.day);
|
|
|
|
if (history.length == 0 || history[history.length - 1] != now) {
|
|
history.add(now);
|
|
|
|
await ReadWrite.writeTextFile(history);
|
|
}
|
|
|
|
recalculate_percentage();
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
void decrementCounter() async {
|
|
var now = DateTime.now();
|
|
|
|
now = DateTime(now.year, now.month, now.day);
|
|
|
|
if (history.length > 0 && history[history.length - 1] == now) {
|
|
history.remove(now);
|
|
|
|
await ReadWrite.writeTextFile(history);
|
|
}
|
|
|
|
recalculate_percentage();
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
void clear_counter() {
|
|
history.clear();
|
|
|
|
recalculate_percentage();
|
|
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
// This widget is the root of your application.
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ChangeNotifierProvider(
|
|
create: (context) => new MyAppState(),
|
|
child: MaterialApp(
|
|
title: 'Flutter Demo',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
|
|
useMaterial3: true,
|
|
),
|
|
home: const MyHomePage(title: 'Habit Tracker (v0.01)'),
|
|
));
|
|
}
|
|
}
|
|
|
|
class MyHomePage extends StatelessWidget {
|
|
const MyHomePage({super.key, required this.title});
|
|
|
|
final String title;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var appState = context.watch<MyAppState>();
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
var percentage = appState.percentage;
|
|
|
|
var size = 200.0;
|
|
|
|
var now = DateTime.now();
|
|
|
|
var cutoff = DateTime(now.year, now.month, now.day - 120);
|
|
|
|
DateTime startDate;
|
|
|
|
if (appState.first_time.isAfter(cutoff)) {
|
|
startDate = appState.first_time;
|
|
} else {
|
|
startDate = cutoff;
|
|
}
|
|
|
|
var inverse_primary = theme.colorScheme.inversePrimary;
|
|
|
|
var secondary = theme.colorScheme.secondary;
|
|
|
|
var primary = theme.colorScheme.primary;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: inverse_primary,
|
|
title: Text(title),
|
|
),
|
|
body: Center(
|
|
child: ListView(
|
|
children: [
|
|
Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: <Widget>[
|
|
HeatMap(
|
|
defaultColor: inverse_primary,
|
|
colorMode: ColorMode.opacity,
|
|
startDate: startDate,
|
|
endDate: now,
|
|
showText: false,
|
|
scrollable: false,
|
|
showColorTip: false,
|
|
datasets: {
|
|
for (int i = 0; i < 120; i++)
|
|
DateTime(now.year, now.month, now.day - i): 1,
|
|
for (DateTime dt in appState.history) dt: 2,
|
|
},
|
|
colorsets: {
|
|
1: primary,
|
|
},
|
|
),
|
|
|
|
SizedBox(height: 10),
|
|
|
|
Stack(
|
|
alignment: AlignmentDirectional.center,
|
|
children: <Widget>[
|
|
Center(
|
|
child: SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: CircularProgressIndicator(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
strokeWidth: 8,
|
|
value: percentage,
|
|
),
|
|
),
|
|
),
|
|
Center(
|
|
child: Text(
|
|
'${(percentage * 100).toStringAsFixed(2)}%',
|
|
style: theme.textTheme.displayMedium!,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
SizedBox(height: 10),
|
|
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
appState.decrementCounter();
|
|
},
|
|
icon: Icon(Icons.remove),
|
|
label: Text('Failure'),
|
|
),
|
|
SizedBox(width: 10),
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
appState.incrementCounter();
|
|
},
|
|
icon: Icon(Icons.add),
|
|
label: Text('Success'),
|
|
),
|
|
],
|
|
),
|
|
|
|
// SizedBox(height: 10),
|
|
|
|
// Text("(The denominator is ${now.difference(appState.first_time).inDays})"),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|