import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tasktracker/Categories.dart'; import 'package:tasktracker/Projects.dart'; import 'package:tasktracker/Todo.dart'; import 'package:tasktracker/Welcome.dart'; import 'package:tasktracker/splash.dart'; import 'package:tasktracker/theme_provider.dart'; import 'Settings/Settings.dart'; import 'package:wakelock/wakelock.dart'; import 'Data.dart'; import 'NewTask.dart'; import 'newActivity.dart'; import 'Tasks.dart'; import 'Activities.dart'; import 'User.dart' as User; import 'package:syncfusion_flutter_charts/charts.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'Dialogs.dart'; final GlobalKey navigatorKey = new GlobalKey(); showAlertDialog(BuildContext context, String title, String message) { // set up the button Widget okButton = TextButton( child: Text("OK"), onPressed: () { Navigator.of(context).pop(); }, ); // set up the AlertDialog AlertDialog alert = AlertDialog( title: Text(title), content: Text(message), actions: [ okButton, ], ); // show the dialog showDialog( context: context, builder: (BuildContext context) { return alert; }, ); } extension HexColor on Color { /// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#". static Color fromHex(String hexString) { final buffer = StringBuffer(); if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); buffer.write(hexString.replaceFirst('#', '')); return Color(int.parse(buffer.toString(), radix: 16)); } /// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`)./home/warlock/Desktop/Task Tracker/tasktracker String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}' '${alpha.toRadixString(16).padLeft(2, '0')}' '${red.toRadixString(16).padLeft(2, '0')}' '${green.toRadixString(16).padLeft(2, '0')}' '${blue.toRadixString(16).padLeft(2, '0')}'; } void main() { //Wakelock.enable(); // or Wakelock.toggle(on: true); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) => ChangeNotifierProvider( create: (context) => ThemeProvider(), builder: (context, _) { final themeProvider = Provider.of(context); return MaterialApp( title: 'Task Tracker', themeMode: themeProvider.themeMode, theme: ThemeData(accentColor: Colors.redAccent, brightness: Brightness.light, primaryColor: Colors.amber, fontFamily: 'Noto-Sans'), darkTheme: ThemeData(backgroundColor: Colors.black,accentColor: Colors.redAccent, brightness: Brightness.dark, primaryColor: Colors.amber, fontFamily: 'Noto-Sans'), navigatorKey: navigatorKey, //home: const SplashScreen(), initialRoute: '/', routes: { '/': (context) => const SplashScreen(), '/welcome': (context) => const WelcomePage(), '/home': (context) => const MyHomePage(), '/Tasks': (context) => const Tasks(), '/Categories': (context) => const Categories(), '/Activities': (context) => const Activities(), '/Settings': (context) => const SettingsPage(), '/Projects':(context)=> const Projects() }); }); } List days = []; String curDay = ""; DateTime? firstDay = null; DateTime? lastDay = null; DateTimeRange? taskTypeRange = null; DateTimeRange? catsRange = null; List productivityData = [ ProductivityMapData('02/24', 35), ProductivityMapData('02/25', 28), ProductivityMapData('02/26', 34), ProductivityMapData('02/27', 32), ProductivityMapData('02/28', 40) ]; List taskTypesData = [TaskTypeMapData('Eat', 3600, Colors.green), TaskTypeMapData('Play', 300, Colors.blue)]; List catsData = [ CatMapData('Jan', 35, Colors.green), CatMapData('Feb', 28, Colors.blueAccent), CatMapData('Mar', 34, Colors.yellow), CatMapData('Apr', 32, Colors.grey), ]; List dailyData = [ CatMapData('Jan', 35, Colors.green), CatMapData('Feb', 28, Colors.blueAccent), CatMapData('Mar', 34, Colors.yellow), CatMapData('Apr', 32, Colors.grey), ]; class MyHomePage extends StatefulWidget { const MyHomePage({Key? key}) : super(key: key); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { var connectivitySub; @override void initState() { // TODO: implement initState print("Im home!"); init(context); super.initState(); print("Initializing refresh stream on main dart"); connectivitySub=Connectivity().onConnectivityChanged.listen((result) { if (this.mounted) { setState(() {}); } }); LoadStats(); // User.progressDialog=progressDialog; } var refreshSub; void init(BuildContext context) async{ await Future.delayed(Duration(seconds: 1)); refreshSub = User.refreshStream.stream.listen((value) { print("Streaming refresh : $value"); if(value){ Dialogs.waiting("Syncing..."); print("Opening progress dialog"); }else{ Dialogs.hide(); print("Closing progress dialog"); } }); await User.refreshUserData(); } DateTime? lastProductive = null; @override void dispose() { // TODO: implement dispose super.dispose(); connectivitySub?.cancel(); } void LoadStats() async { // return; // await User.refreshUserData(); DateFormat dFormat = DateFormat("MM/dd"); Map catTimeMap = {}; Map catBriefMap = {}; Map productivtyActs = {}; Map unproductivtyActs = {}; Map taskTypesDuration = {}; firstDay = null; lastDay = null; String lastDate = ""; lastProductive=null; days = []; for (var element in User.activities) { if (lastDay == null) { lastDay = element.endTime; } if (taskTypeRange == null) { print("$lastDay - $firstDay"); taskTypeRange = DateTimeRange(start: lastDay!.subtract(const Duration(days: 0)), end: lastDay!); } if (catsRange == null) { print("$lastDay - $firstDay"); catsRange = DateTimeRange(start: lastDay!.subtract(const Duration(days: 0)), end: lastDay!); } firstDay = element.startTime; String thisDate = dFormat.format(element.startTime); int thisMinutes = element.endTime.difference(element.startTime).inMinutes; if (!days.contains(thisDate)) { days.add(dFormat.format(element.startTime)); } if (curDay == "") { curDay = dFormat.format(DateTime.now()); } if ((element.startTime.isAfter(taskTypeRange!.start) && element.startTime.isBefore(taskTypeRange!.end)) || (dFormat.format(element.startTime) == dFormat.format(taskTypeRange!.start) || dFormat.format(element.startTime) == dFormat.format(taskTypeRange!.end))) { if (taskTypesDuration.containsKey(element.taskType)) { taskTypesDuration[element.taskType] = taskTypesDuration[element.taskType]! + thisMinutes; } else { taskTypesDuration.putIfAbsent(element.taskType, () => thisMinutes); } } if (element.taskType.cat?.productive ?? false) { if(lastProductive==null){lastProductive = element.trueEndTime;} if (productivtyActs.containsKey(thisDate)) { productivtyActs[thisDate] = (productivtyActs[thisDate]! + thisMinutes); } else { productivtyActs.putIfAbsent(thisDate, () => thisMinutes); } } else { if (unproductivtyActs.containsKey(thisDate)) { unproductivtyActs[thisDate] = (unproductivtyActs[thisDate]! + thisMinutes); } else { unproductivtyActs.putIfAbsent(thisDate, () => thisMinutes); } } if (thisDate == curDay) { if (element.taskType.cat == null) { continue; } print("Null : ${thisMinutes}"); if (catTimeMap.containsKey(element.taskType.cat)) { catTimeMap[element.taskType.cat!] = (catTimeMap[element.taskType.cat]! + thisMinutes); } else { catTimeMap.putIfAbsent(element.taskType.cat!, () => thisMinutes); } } if ((element.startTime.isAfter(catsRange!.start) && element.startTime.isBefore(catsRange!.end)) || (dFormat.format(element.startTime) == dFormat.format(catsRange!.start) || dFormat.format(element.startTime) == dFormat.format(catsRange!.end))) { if (element.taskType.cat == null) { continue; } print("Null : ${thisMinutes}"); if (catBriefMap.containsKey(element.taskType.cat)) { catBriefMap[element.taskType.cat!] = (catBriefMap[element.taskType.cat]! + thisMinutes); } else { catBriefMap.putIfAbsent(element.taskType.cat!, () => thisMinutes); } } } //curDay = days[0]; if (this.mounted) { setState(() { dailyData = []; productivityData = []; taskTypesData = []; catsData = []; int trackedTime = 0; catTimeMap.forEach((key, value) { //print(key.name + " : $value"); Color barCol = HexColor.fromHex(key.color); dailyData.add(CatMapData(key.name, value, barCol)); trackedTime += value; }); int untrackedTime = 1440-trackedTime; if(untrackedTime<0){ User.refreshUserData().then((val)=>LoadStats()); print("Shit went wrong!"); } print("Tracked time : $trackedTime"); dailyData.sort((a, b) { return a.name.toLowerCase().compareTo(b.name.toLowerCase()); }); if(untrackedTime> 0){dailyData.add(CatMapData("Untracked",1440-trackedTime, Colors.black));}else{} for (var element in days) { // if(productivtyActs.containsKey(element) && unproductivtyActs.containsKey(element)){ int prodActs = (productivtyActs[element] ?? 0); int unprodActs = (unproductivtyActs[element] ?? 0); double prod = (prodActs / 1440) * 100; productivityData.add(ProductivityMapData(element, prod)); // } } taskTypesDuration.forEach((key, value) { print("$key : $value"); taskTypesData.add(TaskTypeMapData(key.name, value, HexColor.fromHex(key.cat!.color))); }); taskTypesData.sort((a, b) { return a.time.compareTo(b.time); }); catBriefMap.forEach((key, value) { print(key.name + " : $value"); Color barCol = HexColor.fromHex(key.color); catsData.add(CatMapData(key.name, value, barCol)); }); catsData.sort((a, b) => a.time.compareTo(b.time)); }); } // loadingStats=false; } void showOfflineSnack() async { await Future.delayed(const Duration(seconds: 1)); if (User.offline) { const SnackBar offlineSnack = SnackBar( content: Text('Offline'), duration: Duration(seconds: 100), ); // ScaffoldMessenger.of(context).showSnackBar(offlineSnack); } } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton.extended( onPressed: () { Navigator.of(context).push(MaterialPageRoute(builder: (context) => NewActivity())).then((value) => {User.refreshUserData().then((va) => LoadStats())}); }, label: Text("New Activity"), icon: Icon(Icons.add)), appBar: AppBar( title: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: [ Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [Icon(Icons.article_outlined, color: Theme.of(context).primaryColor), SizedBox(width: 10), Text('Summary')]), Row( children: [ (User.offline) ? Icon(Icons.signal_cellular_connected_no_internet_4_bar_outlined) : InkWell( onTap: () { setState(() async { await User.refreshUserData(); LoadStats(); }); }, child: Icon(Icons.refresh, size: 30), ) ], ) ], ), //Container(color: Colors.red,child: Text("Offline",style:TextStyle(fontSize: 5))), ], )), drawer: navDrawer(context, 0), body: SafeArea( child: (User.activities.isEmpty) ? Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center ,children:[ Expanded(flex: 1,child: Container(),), Expanded(flex: 2,child: Image(image: AssetImage('images/empty.png'))), Expanded(flex:2,child: Text("Add your first activity to access Summary",style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic),)) ]) :Column( mainAxisSize: MainAxisSize.max, children: [ if(User.offline)Expanded(flex:1,child: Container(width:1000,color: Colors.red,child: Align(alignment: Alignment.center,child: Text("Offline")))), Expanded( flex:50, child: SingleChildScrollView( scrollDirection: Axis.vertical, child: Column( children: [ (false) ? Container( padding: EdgeInsets.all(20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ Text("Good\nMorning!", style: TextStyle(fontSize: 23, fontStyle: FontStyle.italic)), Text( "12%", style: TextStyle(fontSize: 30), ), Column( children: [ Text(DateFormat("yy - MM-dd").format(DateTime.now())), Text(DateFormat("HH:mm").format(DateTime.now()), style: TextStyle(fontSize: 40)), ], ) ], ), ) : Container(), Container( height: 350, padding: EdgeInsets.all(10), child: Card( elevation: 8, shadowColor: Colors.blueGrey, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), child: Column( children: [ Row( children: [ SizedBox( width: 10, ), Text("Productivity", style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), ], ), Divider(), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( children: [ Text("Today : "), Text("${(productivityData.length > 0) ? productivityData[0].productivity.toStringAsFixed(1) : 'n/a'}%", style: TextStyle( fontSize: 20, color: (productivityData.length > 1) ? ((productivityData[0].productivity > productivityData[1].productivity) ? Colors.lightGreenAccent : Colors.red) : Colors.white)) ], ), Row( children: [ Text("Yesterday : "), Text("${(productivityData.length > 1) ? productivityData[1].productivity.toStringAsFixed(1) : 'n/a'}%", style: TextStyle(fontSize: 18)) ], ), ], ), Expanded( child: SfCartesianChart( // Initialize category axis primaryXAxis: CategoryAxis(), series: >[ LineSeries( // Bind data source dataSource: productivityData.reversed.toList(), xValueMapper: (ProductivityMapData sales, _) => sales.day, yValueMapper: (ProductivityMapData sales, _) => sales.productivity, dataLabelMapper: (ProductivityMapData sales, _) => sales.productivity.toStringAsFixed(1) + "%", dataLabelSettings: DataLabelSettings(overflowMode: OverflowMode.hide, showZeroValue: false, isVisible: true), color: Colors.green) ]), ), SizedBox(height: 20,), if(lastProductive!=null && DateTime.now().difference(lastProductive!).inMinutes > 60)RichText(text: TextSpan( children: [ TextSpan(text: "You haven't been productive in last",style: TextStyle(color:Colors.orange)), TextSpan(text:" ${MinutesToTimeString(DateTime.now().difference(lastProductive!).inMinutes)}",style: TextStyle(color:Colors.redAccent,fontWeight: FontWeight.bold)) ] )) ], ), )), ), Container( height: 400, padding: EdgeInsets.all(10), child: Card( elevation: 8, shadowColor: Colors.blueGrey, child: Padding( padding: EdgeInsets.all(8), child: (!days.isEmpty) ? Column( children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding(padding: EdgeInsets.all(20), child: Text('Daily Briefing', style: TextStyle(fontWeight: FontWeight.bold))), dayPickerWidget(days, value: curDay, onChange: (_value) { print('new val : $_value'); curDay = _value; setState(() { LoadStats(); }); }), ]), Expanded( child: SfCircularChart(legend: Legend(isVisible: true,position: LegendPosition.bottom,overflowMode: LegendItemOverflowMode.wrap), series: [ // Render pie chart PieSeries( dataSource: dailyData, pointColorMapper: (CatMapData data, _) => data.color, xValueMapper: (CatMapData data, _) => data.name, yValueMapper: (CatMapData data, _) => data.time, dataLabelMapper: (CatMapData sales, _) => MinutesToTimeString(sales.time), dataLabelSettings: DataLabelSettings(isVisible: true, useSeriesColor: true, overflowMode: OverflowMode.shift, showZeroValue: false)) ])) ], ) : Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [CircularProgressIndicator()])))), Container( height: (taskTypesData.length * 45).clamp(350, 1000).toDouble(), padding: EdgeInsets.all(10), child: Card( elevation: 8, shadowColor: Colors.blueGrey, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), child: Column(children: [ Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Task Types", style: TextStyle(fontWeight: FontWeight.bold)), InkWell( onTap: () async { DateTimeRange? value = await showDateRangePicker(context: context, firstDate: firstDay ?? DateTime.now(), lastDate: lastDay ?? DateTime.now()); if (value != null) { taskTypeRange = value; } LoadStats(); }, child: Text((taskTypeRange != null) ? (DateFormat("MM/dd").format(taskTypeRange!.start) + " - " + DateFormat("MM/dd").format(taskTypeRange!.end)) : 'n/a'), ) ]), Expanded( // maxHeight: 300, // maxWidth: 100, child: SfCartesianChart(primaryXAxis: CategoryAxis(), //primaryYAxis: NumericAxis(minimum: 0, maximum: 40, interval: 10), series: >[ BarSeries( dataSource: taskTypesData, xValueMapper: (TaskTypeMapData data, _) => data.task, yValueMapper: (TaskTypeMapData data, _) => data.time / 60, pointColorMapper: (TaskTypeMapData data, _) => data.color, dataLabelMapper: (TaskTypeMapData data, _) => MinutesToTimeString(data.time), dataLabelSettings: DataLabelSettings(isVisible: true), color: Color.fromRGBO(8, 142, 255, 1)) ]), ) ])))), Container( height: (catsData.length * 45).clamp(350, 1000).toDouble(), padding: EdgeInsets.all(10), child: Card( elevation: 8, shadowColor: Colors.blueGrey, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 25), child: Column(children: [ Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Categories", style: TextStyle(fontWeight: FontWeight.bold)), InkWell( onTap: () async { DateTimeRange? value = await showDateRangePicker(context: context, firstDate: firstDay ?? DateTime.now(), lastDate: lastDay ?? DateTime.now()); if (value != null) { catsRange = value; } LoadStats(); }, child: Text((catsRange != null) ? (DateFormat("MM/dd").format(catsRange!.start) + " - " + DateFormat("MM/dd").format(catsRange!.end)) : 'n/a'), ) ]), Expanded( // maxHeight: 300, // maxWidth: 100, child: SfCartesianChart(primaryXAxis: CategoryAxis(), //primaryYAxis: NumericAxis(minimum: 0, maximum: 40, interval: 10), series: >[ BarSeries( dataSource: catsData, xValueMapper: (CatMapData data, _) => data.name, yValueMapper: (CatMapData data, _) => data.time / 60, pointColorMapper: (CatMapData data, _) => data.color, dataLabelMapper: (CatMapData data, _) => MinutesToTimeString(data.time), dataLabelSettings: DataLabelSettings(isVisible: true), color: Color.fromRGBO(8, 142, 255, 1)) ]), ) ])))), ], ), ), ), ], ), ), ); } Widget dayPickerWidget(List list, {required String value, required Function(String value) onChange}) { if (!list.contains(value)) { print("resetting"); onChange(list[0]); } bool nextAvailable = (list.indexOf(value) < (list.length - 1)); bool prevAvailable = (list.indexOf(value) > 0); return Row( children: [ InkWell( onTap: () { if (nextAvailable) { onChange(list[list.indexOf(value) + 1]); } }, child: Container( height: 40, width: 40, child: Icon(Icons.arrow_back_ios, size: 18, color: (nextAvailable) ? Colors.white : Colors.grey), ), ), Text( value, ), InkWell( onTap: () { if (prevAvailable) { onChange(list[list.indexOf(value) - 1]); } }, child: Container( height: 40, width: 40, child: Icon(Icons.arrow_forward_ios, size: 18, color: (prevAvailable) ? Colors.white : Colors.grey), )) ], ); } } Widget moreButton() { return MaterialButton( onPressed: () {}, color: Colors.green, child: Row( children: [Text('More'), Icon(Icons.keyboard_arrow_right)], )); } Drawer navDrawer(BuildContext context, int pageIndex) { return Drawer( child: ListView( children: [ Padding( padding: EdgeInsets.all(16), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Time Tracker", style: TextStyle(fontSize: 25, color: Theme.of(context).accentColor, fontWeight: FontWeight.bold)), Icon( Icons.more_time, size: 30, ), ])), Divider(), ListTile( selected: (pageIndex == 0), title: Text('Summary'), leading: Icon(Icons.article_outlined, color: Theme.of(context).primaryColor), onTap: () { if (pageIndex == 0) { return; } Navigator.of(context).pushReplacementNamed('/home'); }, ), // ListTile( // selected: (pageIndex == 1), // title: Text('Analytics'), // leading: Icon(Icons.analytics_outlined, // color: Theme.of(context).primaryColor), // onTap: () { // if (pageIndex == 1) { // return; // } // // Navigator.of(context).pushReplacementNamed('/'); // }, // ), Divider(), ListTile( selected: (pageIndex == 2), title: Text('Activities'), leading: Icon(Icons.task, color: Theme.of(context).primaryColor), onTap: () { if (pageIndex == 2) { return; } Navigator.of(context).pushReplacementNamed('/Activities'); }, ), ListTile( selected: (pageIndex == 3), title: Text('Task Types'), leading: Icon(Icons.task, color: Theme.of(context).primaryColor), onTap: () { if (pageIndex == 3) { return; } Navigator.of(context).pushReplacementNamed('/Tasks'); }, ), ListTile( selected: (pageIndex == 4), title: Text('Categories'), leading: Icon(Icons.account_tree_outlined, color: Theme.of(context).primaryColor), onTap: () { if (pageIndex == 4) { return; } Navigator.of(context).pushReplacementNamed('/Categories'); }, ), Divider(), ListTile( selected: (pageIndex == 7), title: Text('Projects'), leading: Icon(Icons.work_outline_sharp, color: Theme.of(context).primaryColor), onTap: () { if (pageIndex == 7) { return; } Navigator.of(context).pushReplacementNamed('/Projects'); }, ), // ListTile( // selected: (pageIndex == 7), // title: Text('TODO'), // leading: Icon(Icons.check, color: Theme.of(context).primaryColor), // onTap: () { // if (pageIndex == 7) { // return; // } // // Navigator.of(context).pushReplacementNamed('/Todo'); // }, // ), Divider(), ListTile( selected: (pageIndex == 5), title: Text('Settings'), leading: Icon(Icons.settings, color: Colors.blueGrey), onTap: () { if (pageIndex == 5) { return; } Navigator.of(context).pushNamed('/Settings'); }, ), ListTile( selected: (pageIndex == 6), title: Text('About'), leading: Icon(Icons.help_outline_outlined), onTap: () { showAboutDialog(context: context); }, ), ], )); } class MyPlayerBar extends CustomPainter { MyPlayerBar(this.max, this.value, {this.background = Colors.lightBlue, this.fill = Colors.blue}); Color background = Colors.lightBlue; Color fill = Colors.blue; final int max; final int value; @override void paint(Canvas canvas, Size size) { Paint paint = Paint(); double cursor = (value * size.width) / max; Radius cornerRadius = Radius.circular(10.0); // Already played half color (your darker orange) paint.color = background; // Painting already played half canvas.drawRRect(RRect.fromRectAndCorners(Rect.fromLTWH(0.0, 0.0, cursor, size.height), topLeft: cornerRadius, bottomLeft: cornerRadius), paint); // Yet to play half color (your lighter orange) paint.color = fill; // Painting the remaining space canvas.drawRRect(RRect.fromRectAndCorners(Rect.fromLTWH(cursor, 0.0, size.width - cursor, size.height), bottomRight: cornerRadius, topRight: cornerRadius), paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => true; } class CatMapData { CatMapData(this.name, this.time, this.color); final String name; final int time; final Color color; } class ProductivityMapData { ProductivityMapData(this.day, this.productivity); final String day; final double productivity; } class TaskTypeMapData { TaskTypeMapData(this.task, this.time, this.color); final Color color; final String task; final int time; } String MinutesToTimeString(minutes) { int hours = (minutes / 60).floor(); int mins = minutes % 60; String str = ""; if (hours > 0) { str += hours.toString() + "h"; } if (mins > 0) { str += ((hours > 0) ? " " : "") + mins.toString() + "m"; } return str; }