Initial (redacted) commit.
This commit is contained in:
commit
655f8a036a
368 changed files with 20949 additions and 0 deletions
|
@ -0,0 +1,465 @@
|
|||
//import 'dart:ffi';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/setting_entry.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/widget_settings.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class CompoundWidgetTimer extends ConsumerStatefulWidget {
|
||||
CompoundWidgetTimer({required this.item, super.key});
|
||||
|
||||
TimerItem item;
|
||||
|
||||
double getProgress() {
|
||||
return (item.current / item.goal) / 60;
|
||||
}
|
||||
|
||||
@override
|
||||
ConsumerState<CompoundWidgetTimer> createState() =>
|
||||
_CompoundWidgetTimerState();
|
||||
}
|
||||
|
||||
class _CompoundWidgetTimerState extends ConsumerState<CompoundWidgetTimer>
|
||||
with WidgetsBindingObserver {
|
||||
_CompoundWidgetTimerState();
|
||||
|
||||
String button1text = '';
|
||||
Timer _timer = Timer(Duration.zero, () => ());
|
||||
|
||||
DateTime calcDue() {
|
||||
final now = DateTime.now();
|
||||
const secondsToAdd = 10;
|
||||
const duration = Duration(seconds: secondsToAdd);
|
||||
final futureTime = now.add(duration);
|
||||
|
||||
Logger().i(futureTime);
|
||||
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
void handleButton1() {
|
||||
//start timer from 0
|
||||
const oneSec = Duration(seconds: 1);
|
||||
|
||||
if (widget.item.state == 'initial') {
|
||||
widget.item = widget.item.copyWith(state: 'running');
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
_timer = Timer.periodic(
|
||||
oneSec,
|
||||
(Timer funcTimer) {
|
||||
if (widget.item.current == (widget.item.goal * 60)) {
|
||||
setState(() {
|
||||
final now = DateTime.now();
|
||||
widget.item = widget.item.copyWith(
|
||||
state: 'completed',
|
||||
completedOn: now.toString(),
|
||||
);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
|
||||
funcTimer.cancel();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
widget.item =
|
||||
widget.item.copyWith(current: widget.item.current + 1);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// continue timer
|
||||
else if (widget.item.state == 'paused') {
|
||||
widget.item = widget.item.copyWith(state: 'running');
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
_timer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(Timer funcTimer) {
|
||||
if (widget.item.current == (widget.item.goal * 60)) {
|
||||
setState(() {
|
||||
final now = DateTime.now();
|
||||
widget.item = widget.item.copyWith(
|
||||
state: 'completed',
|
||||
completedOn: now.toString(),
|
||||
);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
|
||||
funcTimer.cancel();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
widget.item =
|
||||
widget.item.copyWith(current: widget.item.current + 1);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (widget.item.state == 'running') {
|
||||
widget.item = widget.item.copyWith(state: 'paused');
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
|
||||
_timer.cancel();
|
||||
} else if (widget.item.state == 'completed') {}
|
||||
}
|
||||
|
||||
void stopTimer() {
|
||||
setState(() {
|
||||
if (_timer.isActive) {
|
||||
_timer.cancel();
|
||||
}
|
||||
widget.item = widget.item.copyWith(
|
||||
current: 0,
|
||||
state: 'initial',
|
||||
);
|
||||
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
}
|
||||
|
||||
String _formatCurrent() {
|
||||
final hours = (widget.item.current / 3600).floor();
|
||||
final minutes = ((widget.item.current - (hours * 3600)) / 60).floor();
|
||||
return '$hours hours : $minutes minutes';
|
||||
}
|
||||
|
||||
String _formattedTime() {
|
||||
final minutesTotal = widget.item.goal;
|
||||
|
||||
final hours = (minutesTotal / 60).floor();
|
||||
final minutes = minutesTotal - (hours * 60);
|
||||
return '$hours hours : $minutes minutes';
|
||||
}
|
||||
|
||||
void _toggleExpansion() {
|
||||
setState(() {
|
||||
widget.item = widget.item.copyWith(isExpanded: !widget.item.isExpanded);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
}
|
||||
|
||||
String _getButton1Text(BuildContext context) {
|
||||
if (widget.item.state == 'initial') {
|
||||
return AppLocalizations.of(context)!.timerWidget_buttonStart;
|
||||
} else if (widget.item.state == 'running') {
|
||||
return AppLocalizations.of(context)!.timerWidget_buttonPause;
|
||||
} else if (widget.item.state == 'paused') {
|
||||
return AppLocalizations.of(context)!.timerWidget_buttonContinue;
|
||||
}
|
||||
return 'Done';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref
|
||||
..watch(homeControllerProvider)
|
||||
..watch(itemsProvider);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 10, top: 10, right: 10, bottom: 10),
|
||||
padding: const EdgeInsets.only(left: 10, top: 15, right: 10, bottom: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x00000000).withOpacity(0.25),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
// changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (!widget.item.isExpanded) ...[
|
||||
Icon(
|
||||
Icons.timer,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 10, right: 10),
|
||||
height: 30,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
), // Rounded corners
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
widget.getProgress(), // Progress value (0.0 - 1.0)
|
||||
backgroundColor:
|
||||
Colors.grey.withOpacity(0.5), // Background color
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Color(0xffA4E8FD),
|
||||
), // Progress color
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.name,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
// alignment: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.item.isExpanded) ...[
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.name,
|
||||
textScaler: const TextScaler.linear(2),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
|
||||
// alignment: TextAlign.left,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.settings,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () => _showSettingsMenu(ref),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
widget.item.isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons.arrow_drop_down_circle_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: _toggleExpansion,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.item.isExpanded) ...[
|
||||
// Additional child elements when expanded
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
// color: Colors.blueAccent,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 250,
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
// color: Colors.greenAccent,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'${AppLocalizations.of(context)!.timerWidget_current}: ${_formatCurrent()} \n ${AppLocalizations.of(context)!.timerWidget_goal}: ${_formattedTime()}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: MediaQuery.of(context).size.width * 0.15,
|
||||
width: MediaQuery.of(context).size.width * 0.60,
|
||||
height: 220,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
strokeWidth: 7,
|
||||
value: widget.getProgress(),
|
||||
semanticsLabel: 'Circular progress indicator',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: handleButton1,
|
||||
child: Text(
|
||||
_getButton1Text(context),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: stopTimer,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.timerWidget_buttonReset,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Add more widgets here as needed
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showSettingsMenu(WidgetRef ref) async {
|
||||
logger.i('Opening settings');
|
||||
|
||||
final settingEntries = WidgetSettingsData(
|
||||
entries: {
|
||||
'name': SettingEntryText(
|
||||
name: AppLocalizations.of(context)!.widgetSettings_name,
|
||||
defaultValue: widget.item.name,
|
||||
),
|
||||
'duration': SettingEntryDuration(
|
||||
name: AppLocalizations.of(context)!.timerWidgetSettings_duration,
|
||||
defaultValue: widget.item.goal,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.item.state == 'running') {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.timerWidget_pausedForEdit,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
content: WidgetSettings(
|
||||
entries: settingEntries,
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
ref.watch(itemsProvider);
|
||||
final items = ref.watch(homeControllerProvider);
|
||||
|
||||
widget.item = widget.item.copyWith(isVisible: false);
|
||||
setState(() {
|
||||
items.edit(widget.item);
|
||||
});
|
||||
|
||||
logger.i('Attempting delete');
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_deleteButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () {
|
||||
_timer.cancel();
|
||||
ref.watch(homeControllerProvider);
|
||||
|
||||
logger.i('Attempting edit of name and/or duration');
|
||||
|
||||
final name = settingEntries.getValue('name') as String;
|
||||
logger.i('New name: $name');
|
||||
final duration = settingEntries.getValue('duration') as int;
|
||||
widget.item =
|
||||
widget.item.copyWith(goal: duration, name: name);
|
||||
setState(() {
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
logger.i('NAME AND DURATION SUCCESSFULLY UPDATED');
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_saveButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,968 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/setting_entry.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/widget_settings.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
const seperator = '_SEPARATOR_';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class IndividualTodoWidget extends ConsumerStatefulWidget {
|
||||
IndividualTodoWidget({
|
||||
required this.todo,
|
||||
required this.due,
|
||||
required this.completed,
|
||||
required this.item,
|
||||
required this.isVisible,
|
||||
required this.createdOn,
|
||||
required this.completedOn,
|
||||
super.key,
|
||||
});
|
||||
late String todo;
|
||||
late bool completed;
|
||||
late DateTime due;
|
||||
late DateTime createdOn;
|
||||
late DateTime completedOn;
|
||||
late TasksItem item;
|
||||
late bool isVisible;
|
||||
|
||||
@override
|
||||
ConsumerState<IndividualTodoWidget> createState() =>
|
||||
_IndividualTodoWidgetState();
|
||||
|
||||
String stringRepr() {
|
||||
return [
|
||||
todo,
|
||||
due.toString().substring(0, 10),
|
||||
createdOn,
|
||||
completedOn,
|
||||
completed,
|
||||
isVisible,
|
||||
].join(seperator);
|
||||
}
|
||||
|
||||
void deleteItem(WidgetRef ref) {
|
||||
final searchTerm = stringRepr();
|
||||
|
||||
if (completed == true) {
|
||||
final completedTaskList = <String>[...item.completedTaskList];
|
||||
//search in completed tasks list
|
||||
for (var i = 0; i < completedTaskList.length; i++) {
|
||||
if (searchTerm == completedTaskList.elementAt(i)) {
|
||||
var newItem = searchTerm;
|
||||
final toConvert = newItem.split(seperator);
|
||||
final todo = toConvert.elementAtOrNull(0)!;
|
||||
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
|
||||
final createdOn = DateTime.parse(toConvert.elementAtOrNull(2)!);
|
||||
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
|
||||
|
||||
final completed =
|
||||
toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
|
||||
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
final formattedDate = dateFormat.format(due);
|
||||
newItem = [
|
||||
todo,
|
||||
formattedDate.substring(0, 10),
|
||||
createdOn,
|
||||
completedOn,
|
||||
completed,
|
||||
false,
|
||||
].join(seperator);
|
||||
|
||||
completedTaskList[i] = newItem;
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
item = item.copyWith(completedTaskList: completedTaskList);
|
||||
controller.edit(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final taskList = <String>[...item.taskList];
|
||||
for (var i = 0; i < taskList.length; i++) {
|
||||
if (searchTerm == taskList.elementAt(i)) {
|
||||
var newItem = searchTerm;
|
||||
final toConvert = newItem.split(seperator);
|
||||
final todo = toConvert.elementAtOrNull(0)!;
|
||||
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
|
||||
final createdOn = DateTime.parse(toConvert.elementAtOrNull(2)!);
|
||||
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
|
||||
|
||||
final completed =
|
||||
toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
|
||||
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
final formattedDate = dateFormat.format(due);
|
||||
newItem = [
|
||||
todo,
|
||||
formattedDate,
|
||||
createdOn,
|
||||
completedOn,
|
||||
completed,
|
||||
false,
|
||||
].join(seperator);
|
||||
|
||||
taskList[i] = newItem;
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
item = item.copyWith(taskList: taskList);
|
||||
|
||||
controller.edit(item);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool overdue() {
|
||||
final now = DateTime.now();
|
||||
|
||||
if (DateTime(now.year, now.month, now.day).isAfter(due)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _IndividualTodoWidgetState extends ConsumerState<IndividualTodoWidget> {
|
||||
_IndividualTodoWidgetState();
|
||||
|
||||
void _toggleChecked(bool? param) {
|
||||
if (widget.overdue() && !widget.completed) {
|
||||
} else {
|
||||
final searchTerm = widget.stringRepr();
|
||||
|
||||
final completedTaskList = <String>[...widget.item.completedTaskList];
|
||||
final taskList = <String>[...widget.item.taskList];
|
||||
if (widget.completed == true) {
|
||||
//search in completed tasks list
|
||||
for (var i = 0; i < completedTaskList.length; i++) {
|
||||
if (searchTerm == completedTaskList.elementAt(i)) {
|
||||
widget.completed = !widget.completed;
|
||||
final replacementStr = widget.stringRepr();
|
||||
|
||||
completedTaskList.removeAt(i);
|
||||
taskList.add(replacementStr);
|
||||
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
widget.item = widget.item.copyWith(
|
||||
taskList: taskList,
|
||||
completedTaskList: completedTaskList,
|
||||
);
|
||||
setState(() {
|
||||
controller.edit(widget.item);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < taskList.length; i++) {
|
||||
if (searchTerm == taskList.elementAt(i)) {
|
||||
widget.completed = !widget.completed;
|
||||
// ignore: cascade_invocations
|
||||
widget.completedOn = DateTime.now();
|
||||
final replacementStr = widget.stringRepr();
|
||||
|
||||
taskList.removeAt(i);
|
||||
completedTaskList.add(replacementStr);
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
widget.item = widget.item.copyWith(
|
||||
taskList: taskList,
|
||||
completedTaskList: completedTaskList,
|
||||
);
|
||||
setState(
|
||||
() {
|
||||
controller.edit(widget.item);
|
||||
},
|
||||
);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _editItem(String oldItem, String itemToAdd) {
|
||||
final completedTaskList = <String>[...widget.item.completedTaskList];
|
||||
final taskList = <String>[...widget.item.taskList];
|
||||
if (widget.completed == true) {
|
||||
//search in completed tasks list
|
||||
for (var i = 0; i < completedTaskList.length; i++) {
|
||||
if (oldItem == completedTaskList.elementAt(i)) {
|
||||
completedTaskList.replaceRange(i, i + 1, [itemToAdd]);
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
widget.item =
|
||||
widget.item.copyWith(completedTaskList: completedTaskList);
|
||||
setState(() {
|
||||
controller.edit(widget.item);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < taskList.length; i++) {
|
||||
if (oldItem == taskList.elementAt(i)) {
|
||||
taskList.replaceRange(i, i + 1, [itemToAdd]);
|
||||
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
widget.item = widget.item.copyWith(taskList: taskList);
|
||||
setState(
|
||||
() {
|
||||
controller.edit(widget.item);
|
||||
},
|
||||
);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editPopup(String oldItem) async {
|
||||
final todoFieldEditController = TextEditingController(text: widget.todo);
|
||||
DateTime? date = widget.due;
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.tasksWidget_editTask,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: todoFieldEditController,
|
||||
decoration: InputDecoration(hintText: widget.todo),
|
||||
autofillHints: null,
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.taskSettings_duePicker,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: widget.due,
|
||||
firstDate: DateTime(2024, 0, 0),
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
date = pickedDate;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_cancelButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
date ??= widget.due;
|
||||
if (todoFieldEditController.text.isEmpty) {
|
||||
todoFieldEditController.text = widget.todo;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop();
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
final formattedDate = dateFormat.format(date!);
|
||||
|
||||
final newItem = [
|
||||
todoFieldEditController.text,
|
||||
formattedDate,
|
||||
widget.createdOn,
|
||||
widget.completedOn,
|
||||
widget.completed,
|
||||
widget.isVisible,
|
||||
].join(seperator);
|
||||
|
||||
_editItem(oldItem, newItem);
|
||||
|
||||
todoFieldEditController.clear();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_saveButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final df = DateFormat('dd.MM.yyyy');
|
||||
var dateText =
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'${AppLocalizations.of(context)!.taskSettings_due}: ${df.format(widget.due)}';
|
||||
|
||||
var backgroundColor = Theme.of(context).colorScheme.secondary;
|
||||
if (widget.overdue() && !widget.completed) {
|
||||
dateText = AppLocalizations.of(context)!.tasksWidget_overdue;
|
||||
backgroundColor = Theme.of(context).colorScheme.tertiary;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2),
|
||||
padding: const EdgeInsets.all(3),
|
||||
width: 300,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1.5,
|
||||
),
|
||||
color: backgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x00000000).withOpacity(0.25),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: Checkbox(
|
||||
activeColor: Colors.green,
|
||||
fillColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return null;
|
||||
}
|
||||
return Theme.of(context).colorScheme.onSecondary;
|
||||
},
|
||||
),
|
||||
value: widget.completed,
|
||||
onChanged: _toggleChecked,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.todo,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
dateText,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => _editPopup(widget.stringRepr()),
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => setState(() {
|
||||
widget.deleteItem(ref);
|
||||
}),
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class CompoundWidgetTasks extends ConsumerStatefulWidget {
|
||||
CompoundWidgetTasks({required this.item, super.key});
|
||||
|
||||
TasksItem item;
|
||||
|
||||
double getProgress() {
|
||||
if (item.completedTaskList.isNotEmpty) {
|
||||
return item.completedTaskList.length /
|
||||
(item.taskList.length + item.completedTaskList.length);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
ConsumerState<CompoundWidgetTasks> createState() =>
|
||||
_CompoundWidgetTasksState();
|
||||
}
|
||||
|
||||
class _CompoundWidgetTasksState extends ConsumerState<CompoundWidgetTasks> {
|
||||
final TextEditingController _todoFieldController = TextEditingController();
|
||||
|
||||
void _toggleExpansion() {
|
||||
setState(() {
|
||||
widget.item = widget.item.copyWith(isExpanded: !widget.item.isExpanded);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
}
|
||||
|
||||
IndividualTodoWidget _todoFromString(String input) {
|
||||
final toConvert = input.split(seperator);
|
||||
final todo = toConvert.elementAtOrNull(0)!;
|
||||
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
|
||||
final createdOn = DateTime.parse(toConvert.elementAtOrNull(2)!);
|
||||
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
|
||||
|
||||
final completed = toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
|
||||
|
||||
final isVisible = toConvert.elementAt(5).toLowerCase() == 'true';
|
||||
|
||||
final newItem = IndividualTodoWidget(
|
||||
todo: todo,
|
||||
due: due,
|
||||
completed: completed,
|
||||
completedOn: completedOn,
|
||||
item: widget.item,
|
||||
isVisible: isVisible,
|
||||
createdOn: createdOn,
|
||||
);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
List<Widget> _buildTodoList() {
|
||||
final items = <IndividualTodoWidget>[];
|
||||
|
||||
final taskList = <String>[...widget.item.taskList];
|
||||
final completedTaskList = <String>[...widget.item.completedTaskList];
|
||||
taskList.sort(
|
||||
(a, b) =>
|
||||
DateTime.parse(a.split(seperator).elementAtOrNull(1)!).compareTo(
|
||||
DateTime.parse(b.split(seperator).elementAtOrNull(1)!),
|
||||
),
|
||||
);
|
||||
completedTaskList.sort(
|
||||
(a, b) =>
|
||||
DateTime.parse(a.split(seperator).elementAtOrNull(1)!).compareTo(
|
||||
DateTime.parse(b.split(seperator).elementAtOrNull(1)!),
|
||||
),
|
||||
);
|
||||
for (var i = 0; i < taskList.length; i++) {
|
||||
final todoAsString = taskList.elementAt(i);
|
||||
|
||||
final newItem = _todoFromString(todoAsString);
|
||||
if (newItem.isVisible) {
|
||||
items.add(newItem);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < completedTaskList.length; i++) {
|
||||
final todoAsString = completedTaskList.elementAt(i);
|
||||
final newItem = _todoFromString(todoAsString);
|
||||
if (newItem.isVisible) {
|
||||
items.add(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
void _addItem(String toAdd) {
|
||||
logger.i('ITEM TO ADD: $toAdd');
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
|
||||
final toCopy = <String>[...widget.item.taskList];
|
||||
// ignore: cascade_invocations
|
||||
toCopy.add(toAdd);
|
||||
widget.item = widget.item.copyWith(taskList: toCopy);
|
||||
setState(() {
|
||||
controller.edit(widget.item);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(itemsProvider);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 10, top: 10, right: 10, bottom: 10),
|
||||
padding: const EdgeInsets.only(left: 10, top: 15, right: 10, bottom: 15),
|
||||
//height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x00000000).withOpacity(0.25),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
// changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (!widget.item.isExpanded) ...[
|
||||
Icon(
|
||||
Icons.task,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 10, right: 10),
|
||||
// Width of the progress bar
|
||||
height: 30, // Height of the progress bar
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
), // Rounded corners
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
widget.getProgress(), // Progress value (0.0 - 1.0)
|
||||
backgroundColor:
|
||||
Colors.grey.withOpacity(0.5), // Background color
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Color(0xffA4E8FD),
|
||||
), // Progress color
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.name,
|
||||
style: TextStyle(
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.item.isExpanded) ...[
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.name,
|
||||
textScaler: const TextScaler.linear(2),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.settings,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () => _showSettingsMenu(ref, context),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
widget.item.isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons.arrow_drop_down_circle_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: _toggleExpansion,
|
||||
),
|
||||
//child: Icon(Icons.arrow_drop_down_circle_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.item.isExpanded) ...[
|
||||
// Additional child elements when expanded
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: _addPopup,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_task_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
// ignore: lines_longer_than_80_chars
|
||||
' ${AppLocalizations.of(context)!.tasksWidget_addTaskButtonLabel}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: _buildTodoList(),
|
||||
),
|
||||
if (anyTasksCompleted()) ...[
|
||||
OutlinedButton(
|
||||
onPressed: _deleteCompletedTasks,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.tasksWidget_deleteDone,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
/* if (anyTasksOverdue()) ...[
|
||||
OutlinedButton(
|
||||
onPressed: _rescheduleOverdueTasks,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.tasksWidget_rescheduleOverdue,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],*/
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool anyTasksCompleted() {
|
||||
for (final item in widget.item.completedTaskList) {
|
||||
final toConvert = item.split(seperator);
|
||||
|
||||
final isVisible = toConvert.elementAt(5).toLowerCase() == 'true';
|
||||
|
||||
if (isVisible) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _deleteCompletedTasks() {
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
|
||||
final newCompletedTaskList = <String>[]; // widget.item.completedTaskList;
|
||||
for (final itemString in widget.item.completedTaskList) {
|
||||
final oldItem = _todoFromString(itemString);
|
||||
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
final newItem = [
|
||||
oldItem.todo,
|
||||
dateFormat.format(oldItem.due),
|
||||
oldItem.createdOn,
|
||||
oldItem.completedOn,
|
||||
oldItem.completed,
|
||||
false,
|
||||
].join(seperator);
|
||||
newCompletedTaskList.add(newItem);
|
||||
}
|
||||
widget.item = widget.item.copyWith(completedTaskList: newCompletedTaskList);
|
||||
controller.edit(widget.item);
|
||||
}
|
||||
|
||||
bool anyTasksOverdue() {
|
||||
final taskList = <String>[...widget.item.taskList];
|
||||
for (final taskString in taskList) {
|
||||
if (_todoFromString(taskString).overdue()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Used for the Reschedule Task Button
|
||||
// ignore: unused_element
|
||||
void _rescheduleOverdueTasks() {
|
||||
final taskList = <String>[...widget.item.taskList];
|
||||
|
||||
for (var i = 0; i < taskList.length; i++) {
|
||||
final taskString = taskList[i];
|
||||
if (_todoFromString(taskString).overdue()) {
|
||||
final oldItem = _todoFromString(taskString);
|
||||
final now = DateTime.now();
|
||||
final newItem = IndividualTodoWidget(
|
||||
todo: oldItem.todo,
|
||||
due: DateTime(now.year, now.month, now.day),
|
||||
createdOn: DateTime.now(),
|
||||
completedOn: DateTime.now(),
|
||||
completed: oldItem.completed,
|
||||
item: widget.item,
|
||||
isVisible: true,
|
||||
);
|
||||
taskList.replaceRange(i, i + 1, [newItem.stringRepr()]);
|
||||
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
widget.item = widget.item.copyWith(taskList: taskList);
|
||||
setState(
|
||||
() {
|
||||
controller.edit(widget.item);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addPopup() async {
|
||||
DateTime? date = DateTime.now();
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.tasksWidget_addTaskButtonLabel,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
scrollable: true,
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: _todoFieldController,
|
||||
autofillHints: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context)!.taskSettings_name,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.taskSettings_duePicker,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(), //get today's date
|
||||
firstDate: DateTime.now(),
|
||||
//DateTime.now() - not to allow to choose before today
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
date = pickedDate;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_cancelButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
//DateTime due = DateTime.parse(formattedDate);
|
||||
// ignore: cascade_invocations
|
||||
|
||||
if (_todoFieldController.text.isEmpty) {
|
||||
_todoFieldController.text = 'todo';
|
||||
}
|
||||
|
||||
if (date != null && _todoFieldController.text.isNotEmpty) {
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
final formattedDate = dateFormat.format(date!);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
final itemToAdd = [
|
||||
_todoFieldController.text,
|
||||
formattedDate,
|
||||
DateTime.now(),
|
||||
DateTime.now(),
|
||||
false,
|
||||
true,
|
||||
].join(seperator);
|
||||
|
||||
_addItem(itemToAdd);
|
||||
_todoFieldController.clear();
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_saveButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showSettingsMenu(WidgetRef ref, BuildContext context) async {
|
||||
final settingEntries = WidgetSettingsData(
|
||||
entries: {
|
||||
'name': SettingEntryText(
|
||||
name: AppLocalizations.of(context)!.widgetSettings_name,
|
||||
defaultValue: widget.item.name,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
content: WidgetSettings(
|
||||
entries: settingEntries,
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
ref
|
||||
.watch(homeControllerProvider)
|
||||
.edit(widget.item.copyWith(isVisible: false));
|
||||
// await controller.delete(widget.item.id);
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_deleteButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
|
||||
final name = settingEntries.getValue('name') as String;
|
||||
|
||||
widget.item = widget.item.copyWith(name: name);
|
||||
setState(() {
|
||||
widget.item = widget.item.copyWith(name: name);
|
||||
|
||||
controller.edit(widget.item);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_saveButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/setting_entry.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/widget_settings.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:liquid_progress_indicator_v2/liquid_progress_indicator.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class CompoundWidgetWater extends ConsumerStatefulWidget {
|
||||
CompoundWidgetWater({required this.item, super.key});
|
||||
|
||||
late Hydration item;
|
||||
|
||||
double getProgress() {
|
||||
return item.current / item.goal;
|
||||
}
|
||||
|
||||
@override
|
||||
ConsumerState<CompoundWidgetWater> createState() =>
|
||||
_CompoundWidgetWaterState();
|
||||
}
|
||||
|
||||
class _CompoundWidgetWaterState extends ConsumerState<CompoundWidgetWater> {
|
||||
void _toggleExpansion() {
|
||||
setState(() {
|
||||
widget.item = widget.item.copyWith(isExpanded: !widget.item.isExpanded);
|
||||
ref.watch(homeControllerProvider).edit(widget.item);
|
||||
});
|
||||
}
|
||||
|
||||
void _addQuantity(int toAdd) {
|
||||
setState(() {
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
final oldval = widget.item.current;
|
||||
|
||||
logger.i('Old item: ${widget.item}');
|
||||
|
||||
widget.item = widget.item.copyWith(current: oldval + toAdd);
|
||||
logger.i('New item: ${widget.item}');
|
||||
|
||||
controller.edit(widget.item);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.watch(itemsProvider);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 10, top: 10, right: 10, bottom: 10),
|
||||
padding: const EdgeInsets.only(left: 10, top: 15, right: 10, bottom: 15),
|
||||
//height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x00000000).withOpacity(0.25),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
// changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (!widget.item.isExpanded) ...[
|
||||
Icon(
|
||||
Icons.local_drink,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 10, right: 10),
|
||||
// Width of the progress bar
|
||||
height: 30, // Height of the progress bar
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
), // Rounded corners
|
||||
child: LinearProgressIndicator(
|
||||
value:
|
||||
widget.getProgress(), // Progress value (0.0 - 1.0)
|
||||
backgroundColor:
|
||||
Colors.grey.withOpacity(0.5), // Background color
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Color(0xffA4E8FD),
|
||||
), // Progress color
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.item.isExpanded) ...[
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.item.name,
|
||||
textScaler: const TextScaler.linear(2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.settings,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: () => _showSettingsMenu(ref),
|
||||
),
|
||||
//child: Icon(Icons.arrow_drop_down_circle_outlined),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
flex: 0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
widget.item.isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons.arrow_drop_down_circle_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: _toggleExpansion,
|
||||
),
|
||||
//child: Icon(Icons.arrow_drop_down_circle_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.item.isExpanded) ...[
|
||||
// Additional child elements when expanded
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
children: [
|
||||
//INSERT WIDGET SPECIFIC STUFF HERE
|
||||
SizedBox(
|
||||
height: 300,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OvershootLiquidLinearProgressIndicator(
|
||||
current: widget.item.current,
|
||||
goal: widget.item.goal,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'${widget.item.current / 1000} / ${widget.item.goal / 1000} L',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
onPressed: () => {
|
||||
_addQuantity(
|
||||
widget.item.button1Amount,
|
||||
),
|
||||
},
|
||||
child: Text(
|
||||
'${widget.item.button1Amount} mL',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
onPressed: () => {
|
||||
_addQuantity(
|
||||
widget.item.button2Amount,
|
||||
),
|
||||
},
|
||||
child: Text(
|
||||
'${widget.item.button2Amount} mL',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
onPressed: _showPopupCustomAmount,
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.waterWidget_customAmountButton,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Add more widgets here as needed
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showSettingsMenu(WidgetRef ref) async {
|
||||
final settingEntries = WidgetSettingsData(
|
||||
entries: {
|
||||
'name': SettingEntryText(
|
||||
name: AppLocalizations.of(context)!.widgetSettings_name,
|
||||
defaultValue: widget.item.name,
|
||||
),
|
||||
'button1Amount': SettingEntryNumeric(
|
||||
name: AppLocalizations.of(context)!.waterWidgetSettings_button1,
|
||||
defaultValue: widget.item.button1Amount,
|
||||
),
|
||||
'button2Amount': SettingEntryNumeric(
|
||||
name: AppLocalizations.of(context)!.waterWidgetSettings_button2,
|
||||
defaultValue: widget.item.button2Amount,
|
||||
),
|
||||
'targetGoal': SettingEntrySlider(
|
||||
name: AppLocalizations.of(context)!.waterWidgetSettings_goal,
|
||||
defaultValue: double.parse(widget.item.goal.toString()),
|
||||
divisions: 80,
|
||||
topValue: 4000,
|
||||
),
|
||||
'currentAmount': SettingEntryNumeric(
|
||||
name: AppLocalizations.of(context)!.waterWidgetSettings_current,
|
||||
defaultValue: widget.item.current,
|
||||
),
|
||||
},
|
||||
);
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
content: WidgetSettings(
|
||||
entries: settingEntries,
|
||||
),
|
||||
actions: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
//widget.parent.delete(ref);
|
||||
ref.watch(itemsProvider);
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
|
||||
widget.item = widget.item.copyWith(isVisible: false);
|
||||
controller.edit(widget.item);
|
||||
|
||||
logger.i('Attempting delete');
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_deleteButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
|
||||
logger.i('Attempting edit of water widget stuff');
|
||||
// widget.settingEntries.notify();
|
||||
final name = settingEntries.getValue('name') as String;
|
||||
final currentAmount =
|
||||
settingEntries.getValue('currentAmount') as int;
|
||||
final targetGoal =
|
||||
(settingEntries.getValue('targetGoal') as double).round();
|
||||
final button1Amount =
|
||||
settingEntries.getValue('button1Amount') as int;
|
||||
final button2Amount =
|
||||
settingEntries.getValue('button2Amount') as int;
|
||||
|
||||
widget.item = widget.item.copyWith(
|
||||
name: name,
|
||||
current: currentAmount,
|
||||
goal: targetGoal,
|
||||
button1Amount: button1Amount,
|
||||
button2Amount: button2Amount,
|
||||
);
|
||||
controller.edit(widget.item);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_saveButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showPopupCustomAmount() async {
|
||||
final customFieldController = TextEditingController();
|
||||
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.waterWidget_customAmountMessage,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
children: <Widget>[
|
||||
TextField(
|
||||
controller: customFieldController,
|
||||
keyboardType: TextInputType.number,
|
||||
autofocus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_cancelButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
final result = int.tryParse(customFieldController.text) ?? 0;
|
||||
_addQuantity(result);
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_saveButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OvershootLiquidLinearProgressIndicator extends StatelessWidget {
|
||||
const OvershootLiquidLinearProgressIndicator({
|
||||
required this.current,
|
||||
required this.goal,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final int current;
|
||||
final int goal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var mainColor = Colors.blue;
|
||||
var backColor = Colors.white;
|
||||
var value = 0.0;
|
||||
if (current < goal) {
|
||||
mainColor = Colors.blue;
|
||||
backColor = Colors.white;
|
||||
value = (current - goal * 0) / goal;
|
||||
} else if (current < goal * 2) {
|
||||
mainColor = Colors.orange;
|
||||
backColor = Colors.blue;
|
||||
value = (current - goal * 1) / goal;
|
||||
} else {
|
||||
mainColor = Colors.red;
|
||||
backColor = Colors.orange;
|
||||
value = (current - goal * 2) / goal;
|
||||
}
|
||||
return LiquidLinearProgressIndicator(
|
||||
value: value,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
mainColor,
|
||||
),
|
||||
backgroundColor: backColor,
|
||||
borderColor: Colors.black,
|
||||
borderWidth: 3,
|
||||
borderRadius: 12,
|
||||
direction: Axis.vertical,
|
||||
);
|
||||
}
|
||||
}
|
375
app/lib/function_widgets/widget_settings_menu/setting_entry.dart
Normal file
375
app/lib/function_widgets/widget_settings_menu/setting_entry.dart
Normal file
|
@ -0,0 +1,375 @@
|
|||
import 'package:duration_picker/duration_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
abstract class SettingEntry extends StatefulWidget {
|
||||
const SettingEntry({required this.name, super.key});
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SettingEntryState();
|
||||
|
||||
dynamic getValue() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void setValue(dynamic newValue) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingEntryState extends State<SettingEntry> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Text('Abstract, not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
// ###############################TEXT#########################################
|
||||
class SettingEntryText extends SettingEntry {
|
||||
SettingEntryText({
|
||||
required super.name,
|
||||
this.defaultValue = 'Some Text',
|
||||
super.key,
|
||||
}) {
|
||||
setValue(defaultValue);
|
||||
}
|
||||
|
||||
final TextEditingController valueController = TextEditingController();
|
||||
final String defaultValue;
|
||||
|
||||
@override
|
||||
State<SettingEntryText> createState() => _SettingEntryTextState();
|
||||
|
||||
@override
|
||||
String getValue() {
|
||||
logger.i('GETTING VALUE ${valueController.text}');
|
||||
return valueController.text;
|
||||
}
|
||||
|
||||
@override
|
||||
void setValue(dynamic newValue) {
|
||||
if (newValue is! String) {
|
||||
throw Exception('Value of SettingEntryText can only be a String!');
|
||||
}
|
||||
valueController.text = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingEntryTextState extends State<SettingEntryText> {
|
||||
void monitorTextChange() {
|
||||
final text = widget.valueController.text;
|
||||
logger.i('TEXT: $text');
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Start listening to changes.
|
||||
widget.valueController.addListener(monitorTextChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up the controller when the widget is removed from the widget tree.
|
||||
// This also removes the _printLatestValue listener.
|
||||
widget.valueController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
|
||||
// color: Colors.black,
|
||||
child: Text(
|
||||
widget.name,
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
controller: widget.valueController,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ###############################NUMERIC#######################################
|
||||
class SettingEntryNumeric extends SettingEntry {
|
||||
SettingEntryNumeric({required super.name, int defaultValue = 0, super.key}) {
|
||||
setValue(defaultValue);
|
||||
}
|
||||
|
||||
final TextEditingController valueController = TextEditingController();
|
||||
|
||||
@override
|
||||
State<SettingEntryNumeric> createState() => _SettingEntryNumericState();
|
||||
|
||||
@override
|
||||
int getValue() {
|
||||
return int.parse(valueController.text);
|
||||
}
|
||||
|
||||
@override
|
||||
void setValue(dynamic newValue) {
|
||||
if (newValue is! int) {
|
||||
throw Exception('Value of SettingEntryNumeric can only be an integer!');
|
||||
}
|
||||
valueController.text = newValue.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingEntryNumericState extends State<SettingEntryNumeric> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(widget.name),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
controller: widget.valueController,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ###############################SLIDER########################################
|
||||
class SettingEntrySlider extends SettingEntry {
|
||||
SettingEntrySlider({
|
||||
required this.topValue,
|
||||
required this.divisions,
|
||||
required super.name,
|
||||
double defaultValue = 0.0,
|
||||
super.key,
|
||||
}) {
|
||||
setValue(defaultValue);
|
||||
}
|
||||
|
||||
final double topValue;
|
||||
final int divisions;
|
||||
final value = DoubleSaver();
|
||||
|
||||
@override
|
||||
State<SettingEntrySlider> createState() => _SettingEntrySliderState();
|
||||
|
||||
@override
|
||||
double getValue() {
|
||||
return value.v;
|
||||
}
|
||||
|
||||
@override
|
||||
void setValue(dynamic newValue) {
|
||||
if (newValue is! double) {
|
||||
throw Exception('Value of SettingEntrySlider can only be a Double!');
|
||||
}
|
||||
value.v = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
class DoubleSaver {
|
||||
double v = 0;
|
||||
}
|
||||
|
||||
class _SettingEntrySliderState extends State<SettingEntrySlider> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
//Expanded(flex: 2, child: Text(widget.name)),
|
||||
Text(widget.name),
|
||||
Text(' ${widget.getValue()}'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Text('0 '),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: widget.getValue(),
|
||||
divisions: widget.divisions,
|
||||
max: widget.topValue,
|
||||
onChanged: sliderChange,
|
||||
),
|
||||
),
|
||||
Text(' ${widget.topValue}'),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void sliderChange(double newValue) {
|
||||
setState(() {
|
||||
widget.setValue(newValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// #############################DURATION#######################################
|
||||
|
||||
class IntSaver {
|
||||
IntSaver({this.v = 30});
|
||||
int v;
|
||||
}
|
||||
|
||||
class SettingEntryDuration extends SettingEntry {
|
||||
SettingEntryDuration({
|
||||
required super.name,
|
||||
required this.defaultValue,
|
||||
super.key,
|
||||
}) {
|
||||
val = IntSaver(v: defaultValue);
|
||||
}
|
||||
final int defaultValue;
|
||||
late final IntSaver val;
|
||||
|
||||
@override
|
||||
State<SettingEntryDuration> createState() => _SettingEntryDurationState();
|
||||
|
||||
@override
|
||||
int getValue() {
|
||||
return val.v;
|
||||
}
|
||||
|
||||
@override
|
||||
void setValue(dynamic newValue) {
|
||||
if (newValue is! int) {
|
||||
throw Exception('Value of SettingEntryDuration can only be a Double!');
|
||||
}
|
||||
val.v = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingEntryDurationState extends State<SettingEntryDuration> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textCurrent =
|
||||
AppLocalizations.of(context)!.widgetSettings_durationPickerCurrent;
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: Text(
|
||||
'$textCurrent ${widget.getValue()}m',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () async {
|
||||
var resultingDuration = await showDurationPicker(
|
||||
context: context,
|
||||
initialTime: Duration(minutes: widget.val.v),
|
||||
);
|
||||
resultingDuration ??= Duration(minutes: widget.defaultValue);
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Chose duration: ${resultingDuration.inMinutes}'),
|
||||
),
|
||||
);
|
||||
widget.setValue(resultingDuration.inMinutes);
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.widgetSettings_durationPickerButton,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ###############################DATE#########################################
|
||||
class DateSaver {
|
||||
DateTime v = DateTime.now();
|
||||
}
|
||||
|
||||
class SettingEntryDate extends SettingEntry {
|
||||
SettingEntryDate({required super.name, DateTime? defaultValue, super.key}) {
|
||||
this.defaultValue = defaultValue ?? DateTime.now();
|
||||
setValue(defaultValue);
|
||||
}
|
||||
|
||||
final date = DateSaver();
|
||||
|
||||
late final DateTime defaultValue;
|
||||
|
||||
@override
|
||||
State<SettingEntryDate> createState() => _SettingEntryDateState();
|
||||
|
||||
@override
|
||||
DateTime getValue() {
|
||||
return date.v;
|
||||
}
|
||||
|
||||
@override
|
||||
void setValue(dynamic newValue) {
|
||||
if (newValue is! DateTime) {
|
||||
throw Exception('Value of SettingEntryNumeric can only be a DateTime!');
|
||||
}
|
||||
date.v = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingEntryDateState extends State<SettingEntryDate> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat('dd. MMMM');
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'${widget.name}: ${dateFormat.format(widget.date.v)} ',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: datePicker,
|
||||
child: const Text('Choose a date'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> datePicker() async {
|
||||
final pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: widget.date.v, //get today's date
|
||||
firstDate: DateTime.now(),
|
||||
//DateTime.now() - not to allow to choose before today.
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
final cleanDate = pickedDate ?? widget.date.v;
|
||||
widget.setValue(cleanDate);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:habitrack_app/function_widgets/widget_settings_menu/setting_entry.dart';
|
||||
|
||||
class WidgetSettingsData {
|
||||
WidgetSettingsData({required this.entries});
|
||||
|
||||
final Map<State, void Function(void Function())> listeners = {};
|
||||
final Map<String, SettingEntry> entries;
|
||||
|
||||
void addListener(State toAdd, void Function(void Function()) updateCall) {
|
||||
listeners[toAdd] = updateCall;
|
||||
}
|
||||
|
||||
void removeListener(State toRemove) {
|
||||
listeners.remove(toRemove);
|
||||
}
|
||||
|
||||
void notify() {
|
||||
for (final listener in listeners.keys) {
|
||||
listeners[listener]!(() => ());
|
||||
}
|
||||
}
|
||||
|
||||
dynamic getValue(String key) {
|
||||
return entries[key]?.getValue();
|
||||
}
|
||||
|
||||
void setValue(String key, dynamic value) {
|
||||
entries[key]?.setValue(value);
|
||||
notify();
|
||||
}
|
||||
|
||||
List<SettingEntry> asList() {
|
||||
return entries.values.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetSettings extends StatefulWidget {
|
||||
const WidgetSettings({
|
||||
required this.entries,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WidgetSettingsData entries;
|
||||
|
||||
@override
|
||||
State<WidgetSettings> createState() => _WidgetSettingsState();
|
||||
}
|
||||
|
||||
class _WidgetSettingsState extends State<WidgetSettings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.settingsHeader,
|
||||
textScaler: const TextScaler.linear(1.5),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: ListView(
|
||||
children: widget.entries.asList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
42
app/lib/infrastructure/bottom_navigation.dart
Normal file
42
app/lib/infrastructure/bottom_navigation.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ScaffoldWithBottomNavigationBar extends StatelessWidget {
|
||||
const ScaffoldWithBottomNavigationBar({
|
||||
required this.navigationShell,
|
||||
Key? key,
|
||||
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
|
||||
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(child: navigationShell),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
fixedColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.list),
|
||||
label: 'Widget Wall',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard),
|
||||
label: 'Dashboard',
|
||||
),
|
||||
],
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int index) => _onTap(context, index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap(BuildContext context, int index) {
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
initialLocation: index == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
}
|
57
app/lib/infrastructure/routing.dart
Normal file
57
app/lib/infrastructure/routing.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:habitrack_app/infrastructure/bottom_navigation.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_page.dart';
|
||||
import 'package:habitrack_app/pages/widget_page.dart';
|
||||
|
||||
// navigators, root and each destination of bottom navigation bar
|
||||
final _rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final _shellNavigatorWidgetWallKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'widgetWall');
|
||||
|
||||
final _shellNavigatorDashboardKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'dashboard');
|
||||
|
||||
const String _widgetWallPath = '/widgetWall';
|
||||
const String _dashboardPath = '/dashboard';
|
||||
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: _widgetWallPath,
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
debugLogDiagnostics: true,
|
||||
routes: [
|
||||
StatefulShellRoute.indexedStack(
|
||||
builder: (context, state, navigationShell) {
|
||||
return ScaffoldWithBottomNavigationBar(
|
||||
navigationShell: navigationShell,
|
||||
);
|
||||
},
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _shellNavigatorWidgetWallKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: _widgetWallPath,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: WidgetPage(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _shellNavigatorDashboardKey,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: _dashboardPath,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: DashboardPage(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
27
app/lib/infrastructure/widget_wall/add_widget_button.dart
Normal file
27
app/lib/infrastructure/widget_wall/add_widget_button.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/add_widget_menu.dart';
|
||||
|
||||
class AddWidgetButton extends StatelessWidget {
|
||||
const AddWidgetButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const AddWidgetMenu(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
150
app/lib/infrastructure/widget_wall/add_widget_menu.dart
Normal file
150
app/lib/infrastructure/widget_wall/add_widget_menu.dart
Normal file
|
@ -0,0 +1,150 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
|
||||
//Add Widget to List- Button Class ######################################
|
||||
class _AddWidgetToList extends ConsumerWidget {
|
||||
const _AddWidgetToList({
|
||||
required this.toAdd,
|
||||
required this.buttonText,
|
||||
required this.iconData,
|
||||
});
|
||||
|
||||
final dynamic toAdd;
|
||||
final String buttonText;
|
||||
final IconData iconData;
|
||||
|
||||
void _buttonFunc(BuildContext context, WidgetRef ref) {
|
||||
//ref.read(widgetListNotifierProvider.notifier).addWidget(toAdd());
|
||||
//ref.read(homeControllerProvider).add()
|
||||
ref.watch(homeControllerProvider).add(toAdd);
|
||||
|
||||
Navigator.pop(context);
|
||||
logger.i('Button Func Called');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x00000000).withOpacity(0.25),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
width: 300,
|
||||
child: TextButton(
|
||||
onPressed: () => {_buttonFunc(context, ref)},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
iconData,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
buttonText,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AddWidgetMenu ##########################################################
|
||||
class AddWidgetMenu extends StatelessWidget {
|
||||
const AddWidgetMenu({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//AddWidgetToList - Button-Instances #######################################
|
||||
final addWaterWidget = _AddWidgetToList(
|
||||
toAdd: Hydration(
|
||||
createdOn: DateTime.now().toString(),
|
||||
completedOn: '',
|
||||
isVisible: true,
|
||||
widgetType: 'Hydration',
|
||||
name: AppLocalizations.of(context)!.waterWidget_defaultName,
|
||||
button1Amount: 100,
|
||||
button2Amount: 250,
|
||||
goal: 2500,
|
||||
current: 0,
|
||||
isExpanded: false,
|
||||
),
|
||||
buttonText: AppLocalizations.of(context)!.addWidget_water,
|
||||
iconData: Icons.local_drink,
|
||||
);
|
||||
|
||||
final addCompoundTimerWidget = _AddWidgetToList(
|
||||
toAdd: TimerItem(
|
||||
widgetType: 'Timer',
|
||||
name: AppLocalizations.of(context)!.timerWidget_defaultName,
|
||||
current: 0,
|
||||
goal: 90,
|
||||
isExpanded: false,
|
||||
createdOn: DateTime.now().toString(),
|
||||
completedOn: '',
|
||||
isVisible: true,
|
||||
state: 'initial',
|
||||
),
|
||||
buttonText: AppLocalizations.of(context)!.addWidget_timer,
|
||||
iconData: Icons.timer,
|
||||
);
|
||||
final addTaskWidget = _AddWidgetToList(
|
||||
toAdd: TasksItem(
|
||||
isVisible: true,
|
||||
widgetType: 'TODO',
|
||||
name: AppLocalizations.of(context)!.tasksWidget_defaultName,
|
||||
isExpanded: false,
|
||||
taskList: [],
|
||||
completedTaskList: [],
|
||||
),
|
||||
buttonText: AppLocalizations.of(context)!.addWidget_tasks,
|
||||
iconData: Icons.task,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.addWidgetHeader,
|
||||
textScaler: const TextScaler.linear(1.5),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
body: Container(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
addWaterWidget,
|
||||
addTaskWidget,
|
||||
addCompoundTimerWidget,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
109
app/lib/infrastructure/widget_wall/graph_widget.dart
Normal file
109
app/lib/infrastructure/widget_wall/graph_widget.dart
Normal file
|
@ -0,0 +1,109 @@
|
|||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DataPoint {
|
||||
DataPoint({required this.date, required this.progress});
|
||||
DateTime date;
|
||||
double progress;
|
||||
}
|
||||
|
||||
class GraphWidget extends ConsumerStatefulWidget {
|
||||
GraphWidget({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _GraphWidgetState();
|
||||
|
||||
final entries = <DataPoint>[
|
||||
DataPoint(date: DateTime(2024, 7, 4), progress: 1),
|
||||
DataPoint(date: DateTime(2024, 7, 5), progress: 0.5),
|
||||
DataPoint(date: DateTime(2024, 7, 6), progress: 1.25),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
];
|
||||
}
|
||||
|
||||
class _GraphWidgetState extends ConsumerState<GraphWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final firstDate = widget.entries[0].date;
|
||||
final flSpots = <FlSpot>[];
|
||||
for (final dataPoint in widget.entries) {
|
||||
final xValue = _daysBetween(firstDate, dataPoint.date).toDouble();
|
||||
final lcbd = FlSpot(xValue, dataPoint.progress);
|
||||
flSpots.add(lcbd);
|
||||
}
|
||||
final df = DateFormat('dd. MMMM');
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.85,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: 1.5,
|
||||
lineBarsData: [
|
||||
LineChartBarData(spots: flSpots, isCurved: true),
|
||||
],
|
||||
gridData: const FlGridData(show: false),
|
||||
extraLinesData: ExtraLinesData(
|
||||
horizontalLines: [
|
||||
HorizontalLine(
|
||||
y: 1,
|
||||
color: Colors.red,
|
||||
),
|
||||
],
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) => Text(value.toString()),
|
||||
interval: 0.25,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
df.format(
|
||||
DateTime(
|
||||
firstDate.year,
|
||||
firstDate.month,
|
||||
firstDate.day,
|
||||
).add(Duration(days: value.round())),
|
||||
),
|
||||
),
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('You worked for X hours this past week'),
|
||||
const Text('Out of a total of Y hours planned'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int _daysBetween(DateTime from, DateTime to) {
|
||||
final d1 = DateTime(from.year, from.month, from.day);
|
||||
final d2 = DateTime(to.year, to.month, to.day);
|
||||
return (d2.difference(d1).inHours / 24).round();
|
||||
}
|
||||
}
|
27
app/lib/infrastructure/widget_wall/items_controller.dart
Normal file
27
app/lib/infrastructure/widget_wall/items_controller.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/sembast/item_repository.dart';
|
||||
import 'package:habitrack_app/sembast/sembast_item_repository.dart';
|
||||
|
||||
final homeControllerProvider = Provider(
|
||||
(ref) => HomeController(
|
||||
itemRepository: ref.watch(itemRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class HomeController {
|
||||
HomeController({required this.itemRepository});
|
||||
|
||||
final ItemRepository itemRepository;
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
await itemRepository.deleteItem(id);
|
||||
}
|
||||
|
||||
Future<void> edit(dynamic item) async {
|
||||
await itemRepository.updateItem(item);
|
||||
}
|
||||
|
||||
Future<void> add(dynamic newItem) async {
|
||||
await itemRepository.insertItem(newItem);
|
||||
}
|
||||
}
|
6
app/lib/infrastructure/widget_wall/items_state.dart
Normal file
6
app/lib/infrastructure/widget_wall/items_state.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/sembast/sembast_item_repository.dart';
|
||||
|
||||
final itemsProvider = StreamProvider(
|
||||
(ref) => ref.watch(itemRepositoryProvider).getAllItemsStream(),
|
||||
);
|
111
app/lib/infrastructure/widget_wall/widget_wall.dart
Normal file
111
app/lib/infrastructure/widget_wall/widget_wall.dart
Normal file
|
@ -0,0 +1,111 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/function_widgets/compound_widgets/compound_timer_widget.dart';
|
||||
import 'package:habitrack_app/function_widgets/compound_widgets/compound_widget_tasks.dart';
|
||||
import 'package:habitrack_app/function_widgets/compound_widgets/compound_widget_water.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/add_widget_button.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
|
||||
class WidgetWall extends ConsumerStatefulWidget {
|
||||
const WidgetWall({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<WidgetWall> createState() => _WidgetWallState();
|
||||
}
|
||||
|
||||
class _WidgetWallState extends ConsumerState<WidgetWall> {
|
||||
Future<List<Widget>> buildList() async {
|
||||
ref.watch(homeControllerProvider);
|
||||
//var itemCount = 0;
|
||||
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
// ignore: unused_local_variable
|
||||
final val = items.value;
|
||||
|
||||
return <Widget>[];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// this.buildList();
|
||||
final controller = ref.watch(itemsProvider);
|
||||
switch (controller) {
|
||||
case AsyncError(:final error):
|
||||
return Text('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
final items = <dynamic>[];
|
||||
for (var i = 0; i < allItems.length; i++) {
|
||||
// ignore: avoid_dynamic_calls
|
||||
if (allItems.elementAt(i).isVisible == true) {
|
||||
logger.i('INSERTING VISIBLE ITEM');
|
||||
items.add(allItems.elementAt(i));
|
||||
}
|
||||
}
|
||||
final itemWidgets = <Widget>[];
|
||||
final itemCount = items.length;
|
||||
for (var i = 0; i < itemCount; i++) {
|
||||
final item = items.elementAt(i);
|
||||
if (item is Hydration && item.isVisible) {
|
||||
final itemwidget = CompoundWidgetWater(item: item);
|
||||
itemWidgets.insert(i, itemwidget);
|
||||
} else if (item is TimerItem && item.isVisible) {
|
||||
if (item.isVisible == true) {
|
||||
logger.i('VISIBLE');
|
||||
final itemwidget = CompoundWidgetTimer(item: item);
|
||||
itemWidgets.insert(i, itemwidget);
|
||||
} else {
|
||||
logger.i('IS NOT VISIBLE');
|
||||
}
|
||||
} else if (item is TasksItem && item.isVisible) {
|
||||
final itemwidget = CompoundWidgetTasks(item: item);
|
||||
itemWidgets.insert(i, itemwidget);
|
||||
}
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
textScaler: const TextScaler.linear(2),
|
||||
'Habitrack ',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
// fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: AddWidgetButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: itemWidgets,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
default:
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
}
|
||||
}
|
59
app/lib/l10n/app_de.arb
Normal file
59
app/lib/l10n/app_de.arb
Normal file
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
|
||||
|
||||
"settingsHeader": "Einstellungen",
|
||||
|
||||
"widgetSettings_durationPickerCurrent": "Derzeitige Laufzeit:",
|
||||
"widgetSettings_durationPickerButton": "Laufzeit auswählen",
|
||||
|
||||
"addWidgetHeader": "Widget Hinzufügen",
|
||||
"addWidget_water": "Wasser-Buddy hinzufügen",
|
||||
"addWidget_timer": "Fokus-Timer hinzufügen",
|
||||
"addWidget_tasks": "Tasks Widget hinzufügen",
|
||||
|
||||
"widgetSettings_name": "Name des Widgets",
|
||||
"widgetSettings_saveButton": "Speichern",
|
||||
"widgetSettings_deleteButton": "Löschen",
|
||||
"widgetSettings_cancelButton": "Abbrechen",
|
||||
|
||||
"waterWidget_defaultName": "Trink-Buddy",
|
||||
"waterWidgetSettings_button1": "Button 1 in ml",
|
||||
"waterWidgetSettings_button2": "Button 2 in ml",
|
||||
"waterWidgetSettings_goal": "Tagesziel",
|
||||
"waterWidgetSettings_current": "Derzeit getrunken",
|
||||
|
||||
"waterWidget_customAmountButton": "+ Beliebig",
|
||||
"waterWidget_customAmountMessage": "Geben sie die Geünschte Wassermenge ein:",
|
||||
|
||||
"tasksWidget_defaultName": "Tasks Widget",
|
||||
"tasksWidget_addTaskButtonLabel": "Aufgabe Hinzufügen",
|
||||
|
||||
"tasksWidget_editTask": "Aufgabe bearbeiten",
|
||||
|
||||
"taskDefaultName": "Neue Aufgabe",
|
||||
"taskSettings_name": "Aufgabenname",
|
||||
"taskSettings_due": "Fällig am",
|
||||
"taskSettings_duePicker": "Fälligkeitsdatum auswählen",
|
||||
|
||||
"tasksWidget_overdue": "Überfällig!",
|
||||
|
||||
"tasksWidget_deleteDone": "Erledigte Aufgaben löschen",
|
||||
"tasksWidget_rescheduleOverdue": "Überfällige Aufgaben neu planen",
|
||||
|
||||
"timerWidget_defaultName": "Fokus-Timer",
|
||||
"timerWidget_buttonStart": "Starten",
|
||||
"timerWidget_buttonPause": "Anhalten",
|
||||
"timerWidget_buttonContinue": "Weiter",
|
||||
"timerWidget_buttonReset": "Zurücksetzen",
|
||||
|
||||
"timerWidgetSettings_duration": "Tägliches Ziel",
|
||||
|
||||
"timerWidget_current": "Jetzt",
|
||||
"timerWidget_goal": "Ziel",
|
||||
|
||||
"timerWidget_pausedForEdit": "Halte den Timer an, um ihn bearbeiten zu können!"
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
64
app/lib/l10n/app_en.arb
Normal file
64
app/lib/l10n/app_en.arb
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
|
||||
|
||||
|
||||
"settingsHeader": "Settings",
|
||||
|
||||
"widgetSettings_durationPickerCurrent": "Current Duration:",
|
||||
"widgetSettings_durationPickerButton": "Choose Duration",
|
||||
|
||||
"addWidgetHeader": "Widget Picker",
|
||||
"addWidget_water": "Add Hydration Tracker",
|
||||
"addWidget_timer": "Add Focus Timer",
|
||||
"addWidget_tasks": "Add Tasks Widget",
|
||||
|
||||
"widgetSettings_name": "Widget Name",
|
||||
"widgetSettings_saveButton": "Confirm",
|
||||
"widgetSettings_deleteButton": "Delete",
|
||||
"widgetSettings_cancelButton": "Cancel",
|
||||
|
||||
"waterWidget_defaultName": "Hydration Tracker",
|
||||
"waterWidgetSettings_button1": "Button 1 in ml",
|
||||
"waterWidgetSettings_button2": "Button 2 in ml",
|
||||
"waterWidgetSettings_goal": "Daily Goal",
|
||||
"waterWidgetSettings_current": "Current Amount",
|
||||
|
||||
"waterWidget_customAmountButton": "+ custom",
|
||||
"waterWidget_customAmountMessage": "Enter an amount as desired:",
|
||||
|
||||
|
||||
"tasksWidget_defaultName": "Tasks Widget",
|
||||
"tasksWidget_addTaskButtonLabel": "Add Task",
|
||||
|
||||
"tasksWidget_editTask": "Edit Task",
|
||||
|
||||
"taskDefaultName": "Examplar Task",
|
||||
"taskSettings_name": "Task Name",
|
||||
"taskSettings_due": "Due Date",
|
||||
"taskSettings_duePicker": "Choose a due date",
|
||||
|
||||
"tasksWidget_overdue": "Overdue!",
|
||||
|
||||
"tasksWidget_deleteDone": "Delete completed tasks",
|
||||
"tasksWidget_rescheduleOverdue": "Reschedule overdue tasks",
|
||||
|
||||
|
||||
"timerWidget_defaultName": "Focus Timer",
|
||||
"timerWidget_buttonStart": "Start",
|
||||
"timerWidget_buttonPause": "Stop",
|
||||
"timerWidget_buttonContinue": "Continue",
|
||||
"timerWidget_buttonReset": "Reset",
|
||||
|
||||
"timerWidgetSettings_duration": "Daily Goal",
|
||||
|
||||
"timerWidget_current": "Current",
|
||||
"timerWidget_goal": "Goal",
|
||||
|
||||
"timerWidget_pausedForEdit": "Timer must be paused before editing!"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
86
app/lib/main.dart
Normal file
86
app/lib/main.dart
Normal file
|
@ -0,0 +1,86 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background/flutter_background.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/infrastructure/routing.dart';
|
||||
import 'package:habitrack_app/sembast/global_providers.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
import 'package:sembast_web/sembast_web.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
if (kIsWeb) {
|
||||
final factory = databaseFactoryWeb;
|
||||
final db = await factory.openDatabase('test');
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [databaseProvider.overrideWithValue(db)],
|
||||
child: const MainApp(),
|
||||
),
|
||||
);
|
||||
// running on the web!
|
||||
} else {
|
||||
final appPath = await getApplicationDocumentsDirectory();
|
||||
|
||||
appPath.createSync(recursive: true);
|
||||
final dbPath = join(appPath.path, 'widgets.db');
|
||||
final database = await databaseFactoryIo.openDatabase(dbPath);
|
||||
const androidConfig = FlutterBackgroundAndroidConfig(
|
||||
notificationTitle: 'flutter_background example app',
|
||||
notificationText:
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'Background notification for keeping the example app running in the background',
|
||||
notificationIcon: AndroidResource(
|
||||
name: 'background_icon',
|
||||
// ignore: avoid_redundant_argument_values
|
||||
defType: 'drawable',
|
||||
), // Default is ic_launcher from folder mipmap
|
||||
);
|
||||
|
||||
await FlutterBackground.initialize(androidConfig: androidConfig);
|
||||
await FlutterBackground.enableBackgroundExecution();
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
databaseProvider.overrideWithValue(database),
|
||||
],
|
||||
child: const MainApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logger logger = Logger();
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
const MainApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Habitrack',
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.purple,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber,
|
||||
),
|
||||
),
|
||||
),
|
||||
routerConfig: goRouter,
|
||||
);
|
||||
}
|
||||
}
|
76
app/lib/pages/dashboard_hydration_subpage.dart
Normal file
76
app/lib/pages/dashboard_hydration_subpage.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/pages/hydration_graph_widget.dart';
|
||||
|
||||
class SubpageHydrationButton extends StatelessWidget {
|
||||
const SubpageHydrationButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 20, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const DashboardHydrationSubpage(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_drink,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
' Hydration Widgets',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardHydrationSubpage extends StatefulWidget {
|
||||
const DashboardHydrationSubpage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DashboardHydrationSubpageState();
|
||||
}
|
||||
|
||||
class _DashboardHydrationSubpageState extends State<DashboardHydrationSubpage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Text(
|
||||
'Statistics: Hydration Widgets',
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: const HydrationGraphWidget(),
|
||||
);
|
||||
}
|
||||
}
|
47
app/lib/pages/dashboard_page.dart
Normal file
47
app/lib/pages/dashboard_page.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_hydration_subpage.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_task_subpage.dart';
|
||||
import 'package:habitrack_app/pages/dashboard_timer_subpage.dart';
|
||||
import 'package:habitrack_app/pages/reset_subpage.dart';
|
||||
|
||||
class DashboardPage extends ConsumerStatefulWidget {
|
||||
const DashboardPage({super.key});
|
||||
@override
|
||||
ConsumerState<DashboardPage> createState() => _DashboardPageState();
|
||||
}
|
||||
|
||||
class _DashboardPageState extends ConsumerState<DashboardPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//final items = ref.watch(itemsProvider);
|
||||
|
||||
//final len = items.value!.length;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Dashboard',
|
||||
textScaler: const TextScaler.linear(1.4),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
body: ColoredBox(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: const Column(
|
||||
children: [
|
||||
SubpageHydrationButton(),
|
||||
SubpageTaskButton(),
|
||||
SubpageTimerButton(),
|
||||
ResetSubpageButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
76
app/lib/pages/dashboard_task_subpage.dart
Normal file
76
app/lib/pages/dashboard_task_subpage.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/pages/tasks_graph_widget.dart';
|
||||
|
||||
class SubpageTaskButton extends StatelessWidget {
|
||||
const SubpageTaskButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const DashboardTaskSubpage(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.task,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
' Task Widgets',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardTaskSubpage extends StatefulWidget {
|
||||
const DashboardTaskSubpage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DashboardTaskSubpageState();
|
||||
}
|
||||
|
||||
class _DashboardTaskSubpageState extends State<DashboardTaskSubpage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Text(
|
||||
'Statistics: Tasks Widgets',
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TasksGraphWidget(),
|
||||
);
|
||||
}
|
||||
}
|
76
app/lib/pages/dashboard_timer_subpage.dart
Normal file
76
app/lib/pages/dashboard_timer_subpage.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/pages/timer_graph_widget.dart';
|
||||
|
||||
class SubpageTimerButton extends StatelessWidget {
|
||||
const SubpageTimerButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<dynamic>(
|
||||
builder: (context) => const DashboardTimerSubpage(),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
' Timer Widgets',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardTimerSubpage extends StatefulWidget {
|
||||
const DashboardTimerSubpage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DashboardTimerSubpageState();
|
||||
}
|
||||
|
||||
class _DashboardTimerSubpageState extends State<DashboardTimerSubpage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
appBar: AppBar(
|
||||
iconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
title: Text(
|
||||
'Statistics: Timer Widgets',
|
||||
textScaler: const TextScaler.linear(1.2),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: TimerGraphWidget(),
|
||||
);
|
||||
}
|
||||
}
|
478
app/lib/pages/hydration_graph_widget.dart
Normal file
478
app/lib/pages/hydration_graph_widget.dart
Normal file
|
@ -0,0 +1,478 @@
|
|||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DataPoint {
|
||||
DataPoint({required this.date, required this.progress});
|
||||
DateTime date;
|
||||
double progress;
|
||||
}
|
||||
|
||||
class HydrationGraphWidget extends ConsumerStatefulWidget {
|
||||
const HydrationGraphWidget({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_TasksGraphWidgetState();
|
||||
}
|
||||
|
||||
class _TasksGraphWidgetState extends ConsumerState<HydrationGraphWidget> {
|
||||
void _buildList(List<dynamic> value) {
|
||||
final items = value;
|
||||
_thisWeekCompleted = 0;
|
||||
_thisWeekPlanned = 0;
|
||||
_maxAmount = 0;
|
||||
|
||||
setState(() {
|
||||
_todayCompleted = 0;
|
||||
_todayPlanned = 0;
|
||||
_weeklyPlannedEntries = [];
|
||||
_weeklyWorkedEntries = [];
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
|
||||
_weeklyPlannedEntries.add(itemToInsert);
|
||||
}
|
||||
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
_weeklyWorkedEntries.add(itemToInsert);
|
||||
}
|
||||
});
|
||||
_thisWeekPlanned = 0;
|
||||
|
||||
for (final item in items) {
|
||||
if (_selectedValue == 'weekly') {
|
||||
var alreadyAdded = false;
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
DateTime parsedDate;
|
||||
|
||||
if (item is Hydration) {
|
||||
if (item.current / item.goal >= 1 && item.completedOn != '') {
|
||||
//item is completed
|
||||
parsedDate = DateTime.parse(item.completedOn);
|
||||
} else {
|
||||
parsedDate = DateTime.parse(item.createdOn);
|
||||
logger.i('GOAL: $item.goal');
|
||||
}
|
||||
|
||||
logger
|
||||
.i('BEFORE $i days ago and AFTER ${i + 1} days ago, element at '
|
||||
'index ${6 - i} will be updated.');
|
||||
|
||||
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
||||
parsedDate
|
||||
.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
||||
logger.i('LOOPING');
|
||||
if (!alreadyAdded) {
|
||||
alreadyAdded = true;
|
||||
_thisWeekCompleted += item.current / 1000;
|
||||
|
||||
_thisWeekPlanned += item.goal / 1000;
|
||||
}
|
||||
|
||||
if (item.goal > _maxAmount) {
|
||||
_maxAmount = (item.goal.toDouble() / 1000).ceilToDouble();
|
||||
}
|
||||
if (item.current > _maxAmount && item.current > item.goal) {
|
||||
_maxAmount = (item.current.toDouble() / 1000).ceilToDouble();
|
||||
}
|
||||
// Update maxAmount
|
||||
setState(() {
|
||||
_weeklyPlannedEntries.elementAt(6 - i).progress =
|
||||
item.goal.toDouble() / 1000;
|
||||
_weeklyWorkedEntries.elementAt(6 - i).progress =
|
||||
item.current.toDouble() / 1000;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_selectedValue == 'daily' && item is Hydration) {
|
||||
logger.i('DATES');
|
||||
final parsedDate = DateTime.parse(item.createdOn);
|
||||
logger.i('LATEST DATE: $_latestDate');
|
||||
|
||||
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
|
||||
|
||||
if (parsedDate.isBefore(oneDayAgo)) {
|
||||
logger.i('More than a day old');
|
||||
} else if (parsedDate.isAfter(oneDayAgo) &&
|
||||
parsedDate.isBefore(_latestDate)) {
|
||||
logger.i('TOday');
|
||||
_todayCompleted = item.current / 1000;
|
||||
_todayPlanned = item.goal / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showPreviousWeek() {
|
||||
setState(() {
|
||||
_todayPlanned = 0;
|
||||
_todayCompleted = 0;
|
||||
_thisWeekPlanned = 0;
|
||||
_thisWeekCompleted = 0;
|
||||
});
|
||||
|
||||
if (_selectedValue == 'weekly') {
|
||||
_latestDate = _latestDate.subtract(const Duration(days: 7));
|
||||
} else if (_selectedValue == 'daily') {
|
||||
logger.i('HMMM');
|
||||
_latestDate = _latestDate.subtract(const Duration(days: 1));
|
||||
}
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
} // get current date
|
||||
// show past 7 days starting 14 days ago
|
||||
}
|
||||
|
||||
void _showNextWeek() {
|
||||
if (_selectedValue == 'weekly') {
|
||||
_latestDate = _latestDate.add(const Duration(days: 7));
|
||||
} else if (_selectedValue == 'daily') {
|
||||
logger.i('HMMM');
|
||||
_latestDate = _latestDate.add(const Duration(days: 1));
|
||||
}
|
||||
if (!_latestDate.isAfter(DateTime.now())) {
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
} // get current date
|
||||
}
|
||||
|
||||
// show past 7 days starting 14 days ago
|
||||
}
|
||||
|
||||
String _getText() {
|
||||
final dateFormat = DateFormat('dd. MMM yyyy');
|
||||
|
||||
if (_selectedValue == 'weekly' &&
|
||||
_latestDate.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||||
return ' this past week';
|
||||
} else if (_selectedValue == 'weekly' &&
|
||||
!_latestDate
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||||
return ' '
|
||||
'from'
|
||||
' ${dateFormat.format(_latestDate.subtract(const Duration(days: 6)))}'
|
||||
' to ${dateFormat.format(_latestDate)}';
|
||||
}
|
||||
final formattedDate = dateFormat.format(_latestDate);
|
||||
// return ' on ${_latestDate.toString().substring(6, 10)}';
|
||||
return ' on $formattedDate';
|
||||
}
|
||||
|
||||
List<dynamic> thisWeekItems = [];
|
||||
List<dynamic> todayItems = [];
|
||||
String? _selectedValue = 'weekly';
|
||||
|
||||
double _thisWeekCompleted = 0;
|
||||
double _thisWeekPlanned = 0;
|
||||
double _maxAmount = 0;
|
||||
DateTime _latestDate = DateTime.now();
|
||||
double _todayCompleted = 0;
|
||||
double _todayPlanned = 0;
|
||||
|
||||
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
|
||||
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
}
|
||||
|
||||
final firstDate = _weeklyPlannedEntries.elementAtOrNull(0)!.date;
|
||||
logger.i('HMMM $firstDate');
|
||||
final flSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyPlannedEntries) {
|
||||
final xValue = _daysBetween(_weeklyPlannedEntries[0].date, dataPoint.date)
|
||||
.toDouble();
|
||||
|
||||
final lcbd = FlSpot(
|
||||
xValue,
|
||||
dataPoint.progress,
|
||||
);
|
||||
|
||||
flSpots.add(lcbd);
|
||||
}
|
||||
|
||||
final weeklyWorkedSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyWorkedEntries) {
|
||||
final xValue =
|
||||
_daysBetween(_weeklyWorkedEntries[0].date, dataPoint.date).toDouble();
|
||||
final lcbd = FlSpot(xValue, dataPoint.progress);
|
||||
|
||||
weeklyWorkedSpots.add(lcbd);
|
||||
}
|
||||
final gradientColors = [
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
];
|
||||
final gradient2Colors = [
|
||||
Colors.amber,
|
||||
Colors.amberAccent,
|
||||
];
|
||||
final df = DateFormat('dd. MMMM');
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 75,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _showPreviousWeek,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Previous'),
|
||||
),
|
||||
DropdownButton<String>(
|
||||
value: _selectedValue,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_latestDate = DateTime.now();
|
||||
_selectedValue = value;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'daily',
|
||||
child: Text('Daily'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'weekly',
|
||||
child: Text('Weekly'),
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _showNextWeek,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Next'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_selectedValue == 'weekly') ...[
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 15, top: 15, right: 15, left: 5),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: _maxAmount,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: flSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors
|
||||
.map((color) => color.withOpacity(0.6))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Colors.amber,
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: weeklyWorkedSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradient2Colors
|
||||
.map((color) => color.withOpacity(0.6))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
titlesData: FlTitlesData(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
'$value l',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
// interval: 0.25,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
df.format(
|
||||
DateTime(
|
||||
firstDate.year,
|
||||
firstDate.month,
|
||||
firstDate.day,
|
||||
).add(Duration(days: value.round())),
|
||||
),
|
||||
),
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_selectedValue == 'daily') ...[
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 0,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: _todayCompleted, // Progress
|
||||
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||||
radius: 60,
|
||||
title: (_todayPlanned > 0)
|
||||
// ignore: lines_longer_than_80_chars
|
||||
? '${((_todayCompleted / _todayPlanned) * 100).floorToDouble()} %'
|
||||
: '0 %',
|
||||
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||||
),
|
||||
value: (_todayPlanned > 0 && _todayCompleted > 0)
|
||||
? (_todayPlanned - _todayCompleted)
|
||||
: 1, // Total - progress
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
radius: 60,
|
||||
showTitle: _todayPlanned == 0 ||
|
||||
(_todayPlanned > 0 && _todayCompleted == 0),
|
||||
title: '0 %',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'${(_selectedValue == 'weekly') ? _thisWeekCompleted : _todayCompleted} liters drunk${_getText()}',
|
||||
),
|
||||
Text(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'Out of a total goal of ${(_selectedValue == 'weekly') ? _thisWeekPlanned : _todayPlanned} liters',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int _daysBetween(DateTime from, DateTime to) {
|
||||
final d1 = DateTime(from.year, from.month, from.day);
|
||||
final d2 = DateTime(to.year, to.month, to.day);
|
||||
return (d2.difference(d1).inHours / 24).round();
|
||||
}
|
||||
}
|
123
app/lib/pages/reset_subpage.dart
Normal file
123
app/lib/pages/reset_subpage.dart
Normal file
|
@ -0,0 +1,123 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
|
||||
class ResetSubpageButton extends ConsumerStatefulWidget {
|
||||
const ResetSubpageButton({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ResetSubpageButton> createState() => _ResetSubpageButtonState();
|
||||
}
|
||||
|
||||
class _ResetSubpageButtonState extends ConsumerState<ResetSubpageButton> {
|
||||
Future<void> _confirmPopup() async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
title: Text(
|
||||
'Are you sure?',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 25,
|
||||
),
|
||||
),
|
||||
scrollable: true,
|
||||
actions: <Widget>[
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
final controller = ref.watch(homeControllerProvider);
|
||||
final items = ref.watch(itemsProvider);
|
||||
switch (items) {
|
||||
case AsyncData(:final value):
|
||||
final items = value;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final item = items.elementAt(i);
|
||||
if (item is Hydration) {
|
||||
controller.delete(item.id);
|
||||
} else if (item is TasksItem) {
|
||||
controller.delete(item.id);
|
||||
} else if (item is TimerItem) {
|
||||
controller.delete(item.id);
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
//DateTime due = DateTime.parse(formattedDate);
|
||||
// ignore: cascade_invocations
|
||||
},
|
||||
child: Text(
|
||||
'Yes',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(top: 7.5, bottom: 7.5),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
minimumSize: const Size(10, 70),
|
||||
),
|
||||
onPressed: _confirmPopup,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Text(
|
||||
'Clear Database',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
687
app/lib/pages/tasks_graph_widget.dart
Normal file
687
app/lib/pages/tasks_graph_widget.dart
Normal file
|
@ -0,0 +1,687 @@
|
|||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DataPoint {
|
||||
DataPoint({required this.date, required this.progress});
|
||||
DateTime date;
|
||||
double progress;
|
||||
}
|
||||
|
||||
class TasksGraphWidget extends ConsumerStatefulWidget {
|
||||
TasksGraphWidget({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_TasksGraphWidgetState();
|
||||
|
||||
final entries = <DataPoint>[
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
];
|
||||
}
|
||||
|
||||
class _TasksGraphWidgetState extends ConsumerState<TasksGraphWidget> {
|
||||
void _buildList(List<dynamic> value) {
|
||||
final items = value;
|
||||
_tasksCompleted = 0;
|
||||
_tasksPlanned = 0;
|
||||
_weeklyOverdue = 0;
|
||||
_totalTasksCompleted = 0;
|
||||
_totalTasksPlanned = 0;
|
||||
_maxTasks = 0;
|
||||
_weeklyPlannedEntries = [];
|
||||
_weeklyOverdueEntries = [];
|
||||
_todayOverdue = 0;
|
||||
|
||||
_totalOverdue = 0;
|
||||
const seperator = '_SEPARATOR_';
|
||||
|
||||
setState(() {
|
||||
_todayCompleted = 0;
|
||||
_todayPlanned = 0;
|
||||
});
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
|
||||
_weeklyPlannedEntries.add(itemToInsert);
|
||||
}
|
||||
|
||||
_weeklyWorkedEntries = [];
|
||||
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
_weeklyWorkedEntries.add(itemToInsert);
|
||||
}
|
||||
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
_weeklyOverdueEntries.add(itemToInsert);
|
||||
}
|
||||
|
||||
for (final item in items) {
|
||||
if (_selectedValue == 'weekly' && item is TasksItem) {
|
||||
logger.i('HMM');
|
||||
|
||||
_tasksCompleted = 0;
|
||||
|
||||
for (final individualToDo in item.completedTaskList) {
|
||||
final toConvert = individualToDo.split(seperator);
|
||||
final parsedDate = DateTime.parse(toConvert.elementAtOrNull(2)!);
|
||||
|
||||
var alreadyAdded = false;
|
||||
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
||||
parsedDate
|
||||
.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
||||
logger.i('LOOPING');
|
||||
if (!alreadyAdded) {
|
||||
alreadyAdded = true;
|
||||
_tasksCompleted += 1;
|
||||
_totalTasksCompleted += 1;
|
||||
}
|
||||
|
||||
// Update maxAmount
|
||||
setState(() {
|
||||
logger.i('COMPLETED: $_tasksCompleted');
|
||||
_weeklyWorkedEntries.elementAt(6 - i).progress =
|
||||
_tasksCompleted.toDouble();
|
||||
_weeklyPlannedEntries.elementAt(6 - i).progress =
|
||||
_tasksPlanned.toDouble() +
|
||||
_tasksCompleted.toDouble() +
|
||||
_weeklyOverdue;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_tasksPlanned = 0;
|
||||
_weeklyOverdue = 0;
|
||||
|
||||
for (final individualToDo in item.taskList) {
|
||||
final toConvert = individualToDo.split(seperator);
|
||||
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
|
||||
|
||||
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
|
||||
|
||||
final parsedDate = completedOn;
|
||||
|
||||
var alreadyAdded = false;
|
||||
logger.i('NANI');
|
||||
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
||||
parsedDate
|
||||
.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
||||
logger.i('LOOPING');
|
||||
|
||||
// Update maxAmount
|
||||
setState(() {
|
||||
if (!alreadyAdded) {
|
||||
alreadyAdded = true;
|
||||
final now = DateTime.now();
|
||||
// _maxTasks += 1;
|
||||
if (DateTime(now.year, now.month, now.day).isAfter(due)) {
|
||||
logger
|
||||
..i('OVERDUE TASK')
|
||||
..i('NOW: $now')
|
||||
..i('DUE: $due');
|
||||
_totalOverdue += 1;
|
||||
_weeklyOverdue += 1;
|
||||
_weeklyOverdueEntries.elementAt(6 - i).progress =
|
||||
_weeklyOverdue;
|
||||
} else {
|
||||
logger.i('Task is NOT overdue');
|
||||
_tasksPlanned += 1;
|
||||
_totalTasksPlanned += 1;
|
||||
}
|
||||
}
|
||||
_weeklyPlannedEntries.elementAt(6 - i).progress =
|
||||
_tasksPlanned.toDouble() +
|
||||
_tasksCompleted.toDouble() +
|
||||
_weeklyOverdue;
|
||||
});
|
||||
}
|
||||
}
|
||||
logger
|
||||
..i('TASKS PLANNED FOR THIS DAY: $_tasksPlanned')
|
||||
..i('TASKS COMPLETED FOR THIS DAY: $_tasksCompleted')
|
||||
..i('TOTAL TASKS: ${_tasksPlanned + _tasksCompleted}');
|
||||
|
||||
if (_tasksPlanned + _tasksCompleted + _weeklyOverdue > _maxTasks) {
|
||||
_maxTasks = (_tasksPlanned + _tasksCompleted + _weeklyOverdue)
|
||||
.ceilToDouble();
|
||||
logger.i('MAX TASKS: $_maxTasks');
|
||||
}
|
||||
}
|
||||
} else if (_selectedValue == 'daily' && item is TasksItem) {
|
||||
for (final individualToDo in item.completedTaskList) {
|
||||
final toConvert = individualToDo.split(seperator);
|
||||
|
||||
final completedOn = DateTime.parse(toConvert.elementAtOrNull(3)!);
|
||||
|
||||
final completed =
|
||||
toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
|
||||
|
||||
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
|
||||
final parsedDate = completedOn;
|
||||
if (parsedDate.isBefore(oneDayAgo)) {
|
||||
logger.i('More than a day old');
|
||||
} else if (parsedDate.isAfter(oneDayAgo) &&
|
||||
parsedDate.isBefore(_latestDate)) {
|
||||
logger.i('TOday');
|
||||
if (completed) {
|
||||
_todayCompleted += 1;
|
||||
_todayPlanned += 1;
|
||||
} else {
|
||||
_todayPlanned += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final individualToDo in item.taskList) {
|
||||
final toConvert = individualToDo.split(seperator);
|
||||
final due = DateTime.parse(toConvert.elementAtOrNull(1)!);
|
||||
|
||||
final createdOn = DateTime.parse(toConvert.elementAtOrNull(2)!);
|
||||
|
||||
final completed =
|
||||
toConvert.elementAtOrNull(4)!.toLowerCase() == 'true';
|
||||
|
||||
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
|
||||
final parsedDate = createdOn;
|
||||
if (parsedDate.isBefore(oneDayAgo)) {
|
||||
logger.i('More than a day old');
|
||||
} else if (parsedDate.isAfter(oneDayAgo) &&
|
||||
parsedDate.isBefore(_latestDate)) {
|
||||
logger.i('TOday');
|
||||
if (completed) {
|
||||
_todayCompleted += 1;
|
||||
_todayPlanned += 1;
|
||||
} else {
|
||||
_todayPlanned += 1;
|
||||
final now = DateTime.now();
|
||||
if (DateTime(now.year, now.month, now.day).isAfter(due)) {
|
||||
logger
|
||||
..i('OVERDUE TASK')
|
||||
..i('NOW: $now')
|
||||
..i('DUE: $due');
|
||||
_totalOverdue += 1;
|
||||
_todayOverdue += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showPreviousWeek() {
|
||||
if (_selectedValue == 'weekly') {
|
||||
_latestDate = _latestDate.subtract(const Duration(days: 7));
|
||||
} else if (_selectedValue == 'daily') {
|
||||
_latestDate = _latestDate.subtract(const Duration(days: 1));
|
||||
}
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
} // get current date
|
||||
// show past 7 days starting 14 days ago
|
||||
}
|
||||
|
||||
void _showNextWeek() {
|
||||
if (_selectedValue == 'weekly') {
|
||||
_latestDate = _latestDate.add(const Duration(days: 7));
|
||||
} else if (_selectedValue == 'daily') {
|
||||
_latestDate = _latestDate.add(const Duration(days: 1));
|
||||
}
|
||||
if (!_latestDate.isAfter(DateTime.now())) {
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
ref.watch(homeControllerProvider);
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
} // get current date
|
||||
}
|
||||
|
||||
// show past 7 days starting 14 days ago
|
||||
}
|
||||
|
||||
String _getText() {
|
||||
final dateFormat = DateFormat('dd. MMM yyyy');
|
||||
|
||||
if (_selectedValue == 'weekly' &&
|
||||
_latestDate.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||||
return ' this past week';
|
||||
} else if (_selectedValue == 'weekly' &&
|
||||
!_latestDate
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||||
return ' '
|
||||
'from'
|
||||
' ${dateFormat.format(_latestDate.subtract(const Duration(days: 6)))}'
|
||||
' to ${dateFormat.format(_latestDate)}';
|
||||
}
|
||||
final formattedDate = dateFormat.format(_latestDate);
|
||||
// return ' on ${_latestDate.toString().substring(6, 10)}';
|
||||
return ' on $formattedDate';
|
||||
}
|
||||
|
||||
final gradientColors = [
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
];
|
||||
final gradient2Colors = [
|
||||
Colors.amber,
|
||||
Colors.amberAccent,
|
||||
];
|
||||
final gradient3Colors = [
|
||||
Colors.deepPurple,
|
||||
Colors.deepPurpleAccent,
|
||||
];
|
||||
List<dynamic> thisWeekItems = [];
|
||||
List<dynamic> todayItems = [];
|
||||
String? _selectedValue = 'weekly';
|
||||
|
||||
int _tasksCompleted = 0;
|
||||
int _tasksPlanned = 0;
|
||||
int _totalTasksCompleted = 0;
|
||||
int _totalTasksPlanned = 0;
|
||||
double _maxTasks = 0;
|
||||
DateTime _latestDate = DateTime.now();
|
||||
double _todayCompleted = 0;
|
||||
double _todayPlanned = 0;
|
||||
double _todayOverdue = 0;
|
||||
double _weeklyOverdue = 0;
|
||||
double _totalOverdue = 0;
|
||||
|
||||
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
|
||||
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
|
||||
List<DataPoint> _weeklyOverdueEntries = <DataPoint>[];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
// ignore: unused_element
|
||||
double getProgress() {
|
||||
if (_todayPlanned == 0) {
|
||||
return 0;
|
||||
}
|
||||
logger.i(
|
||||
'GLORIOUS PROGRESS${_todayCompleted / _todayPlanned}',
|
||||
);
|
||||
return _todayCompleted; // - this._todayCompleted;
|
||||
}
|
||||
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
}
|
||||
|
||||
final firstDate = _weeklyPlannedEntries.elementAtOrNull(0)!.date;
|
||||
final flSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyPlannedEntries) {
|
||||
final xValue = _daysBetween(_weeklyPlannedEntries[0].date, dataPoint.date)
|
||||
.toDouble();
|
||||
|
||||
final lcbd = FlSpot(
|
||||
xValue,
|
||||
dataPoint.progress,
|
||||
);
|
||||
|
||||
flSpots.add(lcbd);
|
||||
}
|
||||
|
||||
final weeklyWorkedSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyWorkedEntries) {
|
||||
final xValue =
|
||||
_daysBetween(_weeklyWorkedEntries.elementAt(0).date, dataPoint.date)
|
||||
.toDouble();
|
||||
final lcbd = FlSpot(xValue, dataPoint.progress);
|
||||
|
||||
weeklyWorkedSpots.add(lcbd);
|
||||
}
|
||||
|
||||
final weeklyOverdueSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyOverdueEntries) {
|
||||
final xValue = _daysBetween(_weeklyOverdueEntries[0].date, dataPoint.date)
|
||||
.toDouble();
|
||||
final lcbd = FlSpot(xValue, dataPoint.progress);
|
||||
weeklyOverdueSpots.add(lcbd);
|
||||
// weeklyWorkedSpots.add(lcbd);
|
||||
}
|
||||
|
||||
final df = DateFormat('dd. MMMM');
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 75,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _showPreviousWeek,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Previous'),
|
||||
),
|
||||
DropdownButton<String>(
|
||||
value: _selectedValue,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_latestDate = DateTime.now();
|
||||
_selectedValue = value;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'daily',
|
||||
child: Text('Daily'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'weekly',
|
||||
child: Text('Weekly'),
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _showNextWeek,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Next'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_selectedValue == 'weekly') ...[
|
||||
Container(
|
||||
// color: Theme.of(context).colorScheme.primaryContainer,
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 15, top: 15, right: 15, left: 5),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: _maxTasks,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: flSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors
|
||||
.map((color) => color.withOpacity(0.6))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Colors.amber,
|
||||
isCurved: true,
|
||||
// isStrokeCapRound: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: weeklyWorkedSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradient2Colors
|
||||
.map((color) => color.withOpacity(0.6))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Colors.purple,
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: weeklyOverdueSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradient3Colors
|
||||
.map((color) => color.withOpacity(0.6))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
titlesData: FlTitlesData(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 50,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
'${value.toInt()} task${value == 1 ? '' : 's'}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
interval: 1,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
df.format(
|
||||
DateTime(
|
||||
firstDate.year,
|
||||
firstDate.month,
|
||||
firstDate.day,
|
||||
).add(Duration(days: value.round())),
|
||||
),
|
||||
),
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_selectedValue == 'daily') ...[
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 0,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: _todayCompleted, // Progress
|
||||
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||||
|
||||
// Cyan
|
||||
radius: 60,
|
||||
title: (_todayPlanned != 0)
|
||||
// ignore: lines_longer_than_80_chars
|
||||
? '${((_todayCompleted / _todayPlanned) * 100).floorToDouble()} %'
|
||||
: '0 %',
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||||
),
|
||||
value: (_todayPlanned > 0 && _todayCompleted > 0)
|
||||
? (_todayPlanned - _todayCompleted)
|
||||
: 1, //
|
||||
// Total - progress
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
radius: 60,
|
||||
showTitle: _todayPlanned == 0 ||
|
||||
(_todayPlanned > 0 && _todayCompleted == 0),
|
||||
title: '0 %',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
// ignore: lines_longer_than_80_chars
|
||||
_getCompletedDescription() + _getText(),
|
||||
),
|
||||
Text(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
// ignore: lines_longer_than_80_chars
|
||||
'Out of a total goal of ${(_selectedValue == 'weekly') ? (_totalTasksPlanned + _totalTasksCompleted + _totalOverdue).toInt() : _todayPlanned.toInt()}${_getText()}',
|
||||
),
|
||||
Text(
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
_getMissedDescription(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getCompletedDescription() {
|
||||
if (_selectedValue == 'weekly') {
|
||||
if (_totalTasksCompleted == 0) {
|
||||
return '0 tasks completed';
|
||||
} else if (_totalTasksCompleted == 1) {
|
||||
return '1 task completed';
|
||||
} else {
|
||||
return '$_totalTasksCompleted tasks completed';
|
||||
}
|
||||
} else if (_selectedValue == 'daily') {
|
||||
if (_todayCompleted == 0) {
|
||||
return '0 tasks completed';
|
||||
} else if (_todayCompleted == 1) {
|
||||
return '1 task completed';
|
||||
} else {
|
||||
return '${_todayCompleted.toInt()} tasks completed';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
String _getMissedDescription() {
|
||||
if (_selectedValue == 'weekly') {
|
||||
if (_totalOverdue == 0) {
|
||||
return 'No tasks were missed';
|
||||
} else if (_totalOverdue == 1) {
|
||||
return '1 task was missed';
|
||||
} else {
|
||||
return '${_totalOverdue.toInt()} tasks were missed';
|
||||
}
|
||||
} else if (_selectedValue == 'daily') {
|
||||
if (_todayOverdue == 0) {
|
||||
return 'No tasks were missed';
|
||||
} else if (_todayOverdue == 1) {
|
||||
return '1 task was missed';
|
||||
} else {
|
||||
return '${_todayOverdue.toInt()} tasks were missed';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
int _daysBetween(DateTime from, DateTime to) {
|
||||
final d1 = DateTime(from.year, from.month, from.day);
|
||||
final d2 = DateTime(to.year, to.month, to.day);
|
||||
return (d2.difference(d1).inHours / 24).round();
|
||||
}
|
||||
}
|
487
app/lib/pages/timer_graph_widget.dart
Normal file
487
app/lib/pages/timer_graph_widget.dart
Normal file
|
@ -0,0 +1,487 @@
|
|||
// ignore_for_file: cascade_invocations
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_controller.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/items_state.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DataPoint {
|
||||
DataPoint({required this.date, required this.progress});
|
||||
DateTime date;
|
||||
double progress;
|
||||
}
|
||||
|
||||
class TimerGraphWidget extends ConsumerStatefulWidget {
|
||||
TimerGraphWidget({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_TimerGraphWidgetState();
|
||||
|
||||
final entries = <DataPoint>[
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
DataPoint(date: DateTime(2024, 7, 7), progress: 0.75),
|
||||
];
|
||||
}
|
||||
|
||||
class _TimerGraphWidgetState extends ConsumerState<TimerGraphWidget> {
|
||||
final gradientColors = [
|
||||
Colors.cyan,
|
||||
Colors.blueAccent,
|
||||
];
|
||||
final gradient2Colors = [
|
||||
Colors.amber,
|
||||
Colors.amberAccent,
|
||||
];
|
||||
String _formatTime() {
|
||||
final minutesTotal =
|
||||
(_selectedValue == 'weekly') ? _timePlanned : _todayGoal;
|
||||
final hours = (minutesTotal / 60).floor();
|
||||
final minutes = (minutesTotal - (hours * 60)).toInt();
|
||||
return '$hours hours : $minutes minutes';
|
||||
}
|
||||
|
||||
String _formatCurrent() {
|
||||
final secondsTotal =
|
||||
(_selectedValue == 'weekly') ? _timeCompleted : _todayCurrent;
|
||||
final hours = (secondsTotal / 3600).floor();
|
||||
final minutes = ((secondsTotal - (hours * 3600)) / 60).floor();
|
||||
return '$hours hours : $minutes minutes';
|
||||
}
|
||||
|
||||
void _buildList(List<dynamic> value) {
|
||||
final items = value;
|
||||
_timeCompleted = 0;
|
||||
_timePlanned = 0;
|
||||
|
||||
setState(() {
|
||||
_todayCurrent = 0;
|
||||
_todayGoal = 0;
|
||||
_weeklyPlannedEntries = [];
|
||||
_weeklyWorkedEntries = [];
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
|
||||
_weeklyPlannedEntries.add(itemToInsert);
|
||||
}
|
||||
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
final itemToInsert = DataPoint(
|
||||
date: _latestDate.subtract(Duration(days: 6 - i)),
|
||||
progress: 0,
|
||||
);
|
||||
_weeklyWorkedEntries.add(itemToInsert);
|
||||
}
|
||||
});
|
||||
for (final item in items) {
|
||||
if (_selectedValue == 'weekly' && item is TimerItem) {
|
||||
final parsedDate = DateTime.parse(item.createdOn);
|
||||
for (var i = 0; i <= 6; i++) {
|
||||
if (parsedDate.isBefore(_latestDate.subtract(Duration(days: i))) &&
|
||||
parsedDate.isAfter(_latestDate.subtract(Duration(days: i + 1)))) {
|
||||
logger.i('LOOPING');
|
||||
|
||||
_timeCompleted += item.current;
|
||||
_timePlanned += item.goal;
|
||||
// _maxTasks += 1;
|
||||
final hours = (item.goal / 60).ceil().toDouble();
|
||||
|
||||
if (hours > _maxTime) {
|
||||
_maxTime = hours;
|
||||
}
|
||||
|
||||
// Update maxAmount
|
||||
setState(() {
|
||||
_weeklyPlannedEntries.elementAt(6).progress = item.goal / 60;
|
||||
_weeklyWorkedEntries.elementAt(6).progress = item.current / 3600;
|
||||
|
||||
// _weeklyWorkedEntries.elementAt(6 - i).progress =
|
||||
// _tasksCompleted.toDouble();
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (_selectedValue == 'daily' && item is TimerItem) {
|
||||
final parsedDate = DateTime.parse(item.createdOn);
|
||||
|
||||
final oneDayAgo = _latestDate.subtract(const Duration(days: 1));
|
||||
|
||||
if (parsedDate.isBefore(oneDayAgo)) {
|
||||
logger.i('More than a day old');
|
||||
} else if (parsedDate.isAfter(oneDayAgo) &&
|
||||
parsedDate.isBefore(_latestDate)) {
|
||||
logger.i('TOday');
|
||||
_todayCurrent += item.current;
|
||||
_todayGoal += item.goal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showPreviousWeek() {
|
||||
if (_selectedValue == 'weekly') {
|
||||
_latestDate = _latestDate.subtract(const Duration(days: 7));
|
||||
} else if (_selectedValue == 'daily') {
|
||||
logger.i('HMMM');
|
||||
_latestDate = _latestDate.subtract(const Duration(days: 1));
|
||||
}
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
} // get current date
|
||||
// show past 7 days starting 14 days ago
|
||||
}
|
||||
|
||||
void _showNextWeek() {
|
||||
logger.i('CURRENT: ${_todayCurrent / 60}');
|
||||
|
||||
logger.i('MINUTES TOTAL: $_todayGoal');
|
||||
logger.i('PERCENTAGE: ${_todayCurrent / (_todayGoal * 60)}');
|
||||
if (_selectedValue == 'weekly') {
|
||||
_latestDate = _latestDate.add(const Duration(days: 7));
|
||||
} else if (_selectedValue == 'daily') {
|
||||
logger.i('HMMM');
|
||||
_latestDate = _latestDate.add(const Duration(days: 1));
|
||||
}
|
||||
if (!_latestDate.isAfter(DateTime.now())) {
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
ref.watch(homeControllerProvider);
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
} // get current date
|
||||
}
|
||||
|
||||
// show past 7 days starting 14 days ago
|
||||
}
|
||||
|
||||
String _getText() {
|
||||
final dateFormat = DateFormat('dd. MMM yyyy');
|
||||
|
||||
if (_selectedValue == 'weekly' &&
|
||||
_latestDate.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||||
return ' this past week';
|
||||
} else if (_selectedValue == 'weekly' &&
|
||||
!_latestDate
|
||||
.isAfter(DateTime.now().subtract(const Duration(days: 6)))) {
|
||||
return ' '
|
||||
'from'
|
||||
' ${dateFormat.format(_latestDate.subtract(const Duration(days: 6)))}'
|
||||
' to ${dateFormat.format(_latestDate)}';
|
||||
}
|
||||
final formattedDate = dateFormat.format(_latestDate);
|
||||
// return ' on ${_latestDate.toString().substring(6, 10)}';
|
||||
return ' on $formattedDate';
|
||||
}
|
||||
|
||||
List<dynamic> thisWeekItems = [];
|
||||
List<dynamic> todayItems = [];
|
||||
String? _selectedValue = 'weekly';
|
||||
|
||||
int _timeCompleted = 0;
|
||||
int _timePlanned = 0;
|
||||
double _maxTime = 0;
|
||||
DateTime _latestDate = DateTime.now();
|
||||
double _todayCurrent = 0;
|
||||
double _todayGoal = 0;
|
||||
|
||||
List<DataPoint> _weeklyPlannedEntries = <DataPoint>[];
|
||||
List<DataPoint> _weeklyWorkedEntries = <DataPoint>[];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = ref.watch(itemsProvider);
|
||||
|
||||
// ignore: unused_element
|
||||
double getProgress() {
|
||||
logger.i('CURRENT: $_todayCurrent');
|
||||
logger.i('GOAL: $_todayGoal');
|
||||
if (_todayGoal == 0) {
|
||||
return 0;
|
||||
}
|
||||
return _todayCurrent / _todayGoal;
|
||||
}
|
||||
|
||||
ref.watch(homeControllerProvider);
|
||||
switch (items) {
|
||||
case AsyncError(:final error):
|
||||
logger.i('Error: $error');
|
||||
case AsyncData(:final value):
|
||||
final allItems = value;
|
||||
_buildList(allItems);
|
||||
default:
|
||||
logger.i('Hmmm, how can we help?');
|
||||
}
|
||||
|
||||
final firstDate = _weeklyPlannedEntries[0].date;
|
||||
|
||||
final flSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyPlannedEntries) {
|
||||
final xValue = _daysBetween(_weeklyPlannedEntries[0].date, dataPoint.date)
|
||||
.toDouble();
|
||||
final lcbd = FlSpot(
|
||||
xValue,
|
||||
dataPoint.progress,
|
||||
);
|
||||
|
||||
flSpots.add(lcbd);
|
||||
}
|
||||
final weeklyWorkedSpots = <FlSpot>[];
|
||||
for (final dataPoint in _weeklyWorkedEntries) {
|
||||
final xValue =
|
||||
_daysBetween(_weeklyWorkedEntries[0].date, dataPoint.date).toDouble();
|
||||
final lcbd = FlSpot(xValue, dataPoint.progress);
|
||||
|
||||
weeklyWorkedSpots.add(lcbd);
|
||||
}
|
||||
|
||||
final df = DateFormat('dd. MMMM');
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 75,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _showPreviousWeek,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Previous'),
|
||||
),
|
||||
DropdownButton<String>(
|
||||
value: _selectedValue,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_latestDate = DateTime.now();
|
||||
_selectedValue = value;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'daily',
|
||||
child: Text('Daily'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'weekly',
|
||||
child: Text('Weekly'),
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _showNextWeek,
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('Next'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_selectedValue == 'weekly') ...[
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: 15, top: 15, right: 15, left: 5),
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: _maxTime,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: flSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradientColors
|
||||
.map((color) => color.withOpacity(0.3))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
LineChartBarData(
|
||||
color: Colors.amber,
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
preventCurveOverShooting: true,
|
||||
spots: weeklyWorkedSpots,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: gradient2Colors
|
||||
.map((color) => color.withOpacity(0.3))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
titlesData: FlTitlesData(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
interval: 20,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 50,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
'$value h',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
interval: 0.5,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
df.format(
|
||||
DateTime(
|
||||
firstDate.year,
|
||||
firstDate.month,
|
||||
firstDate.day,
|
||||
).add(Duration(days: value.round())),
|
||||
),
|
||||
),
|
||||
showTitles: true,
|
||||
interval: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_selectedValue == 'daily') ...[
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 0,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
value: _todayCurrent / 60, // Progress
|
||||
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||||
radius: 60,
|
||||
title: (_todayGoal > 0)
|
||||
// ignore: lines_longer_than_80_chars
|
||||
? '${((_todayCurrent * 100 / 60) / _todayGoal).round()} %'
|
||||
: '0 %',
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
value: (_todayCurrent > 0)
|
||||
? (_todayGoal - (_todayCurrent / 60))
|
||||
: 1, // Total - progress
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
radius: 60,
|
||||
showTitle: _todayCurrent <= 0,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primaryFixedDim,
|
||||
),
|
||||
title: '0%',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.90,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'You worked for ${_formatCurrent()}${_getText()}',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Out of a total goal of ${_formatTime()}${_getText()}',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
int _daysBetween(DateTime from, DateTime to) {
|
||||
final d1 = DateTime(from.year, from.month, from.day);
|
||||
final d2 = DateTime(to.year, to.month, to.day);
|
||||
return (d2.difference(d1).inHours / 24).round();
|
||||
}
|
||||
}
|
11
app/lib/pages/widget_page.dart
Normal file
11
app/lib/pages/widget_page.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:habitrack_app/infrastructure/widget_wall/widget_wall.dart';
|
||||
|
||||
class WidgetPage extends StatelessWidget {
|
||||
const WidgetPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const WidgetWall();
|
||||
}
|
||||
}
|
5
app/lib/sembast/global_providers.dart
Normal file
5
app/lib/sembast/global_providers.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
|
||||
final databaseProvider =
|
||||
Provider<Database>((_) => throw Exception('Database not initialized'));
|
31
app/lib/sembast/hydration.dart
Normal file
31
app/lib/sembast/hydration.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'hydration.freezed.dart';
|
||||
part 'hydration.g.dart';
|
||||
|
||||
@freezed
|
||||
class Hydration with _$Hydration {
|
||||
const factory Hydration({
|
||||
required String widgetType,
|
||||
required String name,
|
||||
required int button1Amount,
|
||||
required int button2Amount,
|
||||
required int goal,
|
||||
required int current,
|
||||
required bool isExpanded,
|
||||
required bool isVisible,
|
||||
required String createdOn,
|
||||
required String completedOn,
|
||||
@Default(-1) int id,
|
||||
}) = _Hydration;
|
||||
|
||||
factory Hydration.fromJson(Map<String, Object?> json) =>
|
||||
_$HydrationFromJson(json);
|
||||
}
|
||||
|
||||
extension JsonWithoutId on Hydration {
|
||||
Map<String, dynamic> toJsonWithoutId() {
|
||||
final map = toJson()..remove('id');
|
||||
return map;
|
||||
}
|
||||
}
|
392
app/lib/sembast/hydration.freezed.dart
Normal file
392
app/lib/sembast/hydration.freezed.dart
Normal file
|
@ -0,0 +1,392 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'hydration.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
Hydration _$HydrationFromJson(Map<String, dynamic> json) {
|
||||
return _Hydration.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Hydration {
|
||||
String get widgetType => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
int get button1Amount => throw _privateConstructorUsedError;
|
||||
int get button2Amount => throw _privateConstructorUsedError;
|
||||
int get goal => throw _privateConstructorUsedError;
|
||||
int get current => throw _privateConstructorUsedError;
|
||||
bool get isExpanded => throw _privateConstructorUsedError;
|
||||
bool get isVisible => throw _privateConstructorUsedError;
|
||||
String get createdOn => throw _privateConstructorUsedError;
|
||||
String get completedOn => throw _privateConstructorUsedError;
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$HydrationCopyWith<Hydration> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $HydrationCopyWith<$Res> {
|
||||
factory $HydrationCopyWith(Hydration value, $Res Function(Hydration) then) =
|
||||
_$HydrationCopyWithImpl<$Res, Hydration>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String widgetType,
|
||||
String name,
|
||||
int button1Amount,
|
||||
int button2Amount,
|
||||
int goal,
|
||||
int current,
|
||||
bool isExpanded,
|
||||
bool isVisible,
|
||||
String createdOn,
|
||||
String completedOn,
|
||||
int id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$HydrationCopyWithImpl<$Res, $Val extends Hydration>
|
||||
implements $HydrationCopyWith<$Res> {
|
||||
_$HydrationCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? widgetType = null,
|
||||
Object? name = null,
|
||||
Object? button1Amount = null,
|
||||
Object? button2Amount = null,
|
||||
Object? goal = null,
|
||||
Object? current = null,
|
||||
Object? isExpanded = null,
|
||||
Object? isVisible = null,
|
||||
Object? createdOn = null,
|
||||
Object? completedOn = null,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
widgetType: null == widgetType
|
||||
? _value.widgetType
|
||||
: widgetType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
button1Amount: null == button1Amount
|
||||
? _value.button1Amount
|
||||
: button1Amount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
button2Amount: null == button2Amount
|
||||
? _value.button2Amount
|
||||
: button2Amount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
goal: null == goal
|
||||
? _value.goal
|
||||
: goal // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
current: null == current
|
||||
? _value.current
|
||||
: current // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isExpanded: null == isExpanded
|
||||
? _value.isExpanded
|
||||
: isExpanded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isVisible: null == isVisible
|
||||
? _value.isVisible
|
||||
: isVisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
createdOn: null == createdOn
|
||||
? _value.createdOn
|
||||
: createdOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
completedOn: null == completedOn
|
||||
? _value.completedOn
|
||||
: completedOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$HydrationImplCopyWith<$Res>
|
||||
implements $HydrationCopyWith<$Res> {
|
||||
factory _$$HydrationImplCopyWith(
|
||||
_$HydrationImpl value, $Res Function(_$HydrationImpl) then) =
|
||||
__$$HydrationImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String widgetType,
|
||||
String name,
|
||||
int button1Amount,
|
||||
int button2Amount,
|
||||
int goal,
|
||||
int current,
|
||||
bool isExpanded,
|
||||
bool isVisible,
|
||||
String createdOn,
|
||||
String completedOn,
|
||||
int id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$HydrationImplCopyWithImpl<$Res>
|
||||
extends _$HydrationCopyWithImpl<$Res, _$HydrationImpl>
|
||||
implements _$$HydrationImplCopyWith<$Res> {
|
||||
__$$HydrationImplCopyWithImpl(
|
||||
_$HydrationImpl _value, $Res Function(_$HydrationImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? widgetType = null,
|
||||
Object? name = null,
|
||||
Object? button1Amount = null,
|
||||
Object? button2Amount = null,
|
||||
Object? goal = null,
|
||||
Object? current = null,
|
||||
Object? isExpanded = null,
|
||||
Object? isVisible = null,
|
||||
Object? createdOn = null,
|
||||
Object? completedOn = null,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(_$HydrationImpl(
|
||||
widgetType: null == widgetType
|
||||
? _value.widgetType
|
||||
: widgetType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
button1Amount: null == button1Amount
|
||||
? _value.button1Amount
|
||||
: button1Amount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
button2Amount: null == button2Amount
|
||||
? _value.button2Amount
|
||||
: button2Amount // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
goal: null == goal
|
||||
? _value.goal
|
||||
: goal // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
current: null == current
|
||||
? _value.current
|
||||
: current // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isExpanded: null == isExpanded
|
||||
? _value.isExpanded
|
||||
: isExpanded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isVisible: null == isVisible
|
||||
? _value.isVisible
|
||||
: isVisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
createdOn: null == createdOn
|
||||
? _value.createdOn
|
||||
: createdOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
completedOn: null == completedOn
|
||||
? _value.completedOn
|
||||
: completedOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$HydrationImpl with DiagnosticableTreeMixin implements _Hydration {
|
||||
const _$HydrationImpl(
|
||||
{required this.widgetType,
|
||||
required this.name,
|
||||
required this.button1Amount,
|
||||
required this.button2Amount,
|
||||
required this.goal,
|
||||
required this.current,
|
||||
required this.isExpanded,
|
||||
required this.isVisible,
|
||||
required this.createdOn,
|
||||
required this.completedOn,
|
||||
this.id = -1});
|
||||
|
||||
factory _$HydrationImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$HydrationImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String widgetType;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final int button1Amount;
|
||||
@override
|
||||
final int button2Amount;
|
||||
@override
|
||||
final int goal;
|
||||
@override
|
||||
final int current;
|
||||
@override
|
||||
final bool isExpanded;
|
||||
@override
|
||||
final bool isVisible;
|
||||
@override
|
||||
final String createdOn;
|
||||
@override
|
||||
final String completedOn;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int id;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'Hydration(widgetType: $widgetType, name: $name, button1Amount: $button1Amount, button2Amount: $button2Amount, goal: $goal, current: $current, isExpanded: $isExpanded, isVisible: $isVisible, createdOn: $createdOn, completedOn: $completedOn, id: $id)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'Hydration'))
|
||||
..add(DiagnosticsProperty('widgetType', widgetType))
|
||||
..add(DiagnosticsProperty('name', name))
|
||||
..add(DiagnosticsProperty('button1Amount', button1Amount))
|
||||
..add(DiagnosticsProperty('button2Amount', button2Amount))
|
||||
..add(DiagnosticsProperty('goal', goal))
|
||||
..add(DiagnosticsProperty('current', current))
|
||||
..add(DiagnosticsProperty('isExpanded', isExpanded))
|
||||
..add(DiagnosticsProperty('isVisible', isVisible))
|
||||
..add(DiagnosticsProperty('createdOn', createdOn))
|
||||
..add(DiagnosticsProperty('completedOn', completedOn))
|
||||
..add(DiagnosticsProperty('id', id));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$HydrationImpl &&
|
||||
(identical(other.widgetType, widgetType) ||
|
||||
other.widgetType == widgetType) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.button1Amount, button1Amount) ||
|
||||
other.button1Amount == button1Amount) &&
|
||||
(identical(other.button2Amount, button2Amount) ||
|
||||
other.button2Amount == button2Amount) &&
|
||||
(identical(other.goal, goal) || other.goal == goal) &&
|
||||
(identical(other.current, current) || other.current == current) &&
|
||||
(identical(other.isExpanded, isExpanded) ||
|
||||
other.isExpanded == isExpanded) &&
|
||||
(identical(other.isVisible, isVisible) ||
|
||||
other.isVisible == isVisible) &&
|
||||
(identical(other.createdOn, createdOn) ||
|
||||
other.createdOn == createdOn) &&
|
||||
(identical(other.completedOn, completedOn) ||
|
||||
other.completedOn == completedOn) &&
|
||||
(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
widgetType,
|
||||
name,
|
||||
button1Amount,
|
||||
button2Amount,
|
||||
goal,
|
||||
current,
|
||||
isExpanded,
|
||||
isVisible,
|
||||
createdOn,
|
||||
completedOn,
|
||||
id);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$HydrationImplCopyWith<_$HydrationImpl> get copyWith =>
|
||||
__$$HydrationImplCopyWithImpl<_$HydrationImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$HydrationImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Hydration implements Hydration {
|
||||
const factory _Hydration(
|
||||
{required final String widgetType,
|
||||
required final String name,
|
||||
required final int button1Amount,
|
||||
required final int button2Amount,
|
||||
required final int goal,
|
||||
required final int current,
|
||||
required final bool isExpanded,
|
||||
required final bool isVisible,
|
||||
required final String createdOn,
|
||||
required final String completedOn,
|
||||
final int id}) = _$HydrationImpl;
|
||||
|
||||
factory _Hydration.fromJson(Map<String, dynamic> json) =
|
||||
_$HydrationImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get widgetType;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
int get button1Amount;
|
||||
@override
|
||||
int get button2Amount;
|
||||
@override
|
||||
int get goal;
|
||||
@override
|
||||
int get current;
|
||||
@override
|
||||
bool get isExpanded;
|
||||
@override
|
||||
bool get isVisible;
|
||||
@override
|
||||
String get createdOn;
|
||||
@override
|
||||
String get completedOn;
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$HydrationImplCopyWith<_$HydrationImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
37
app/lib/sembast/hydration.g.dart
Normal file
37
app/lib/sembast/hydration.g.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'hydration.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$HydrationImpl _$$HydrationImplFromJson(Map<String, dynamic> json) =>
|
||||
_$HydrationImpl(
|
||||
widgetType: json['widgetType'] as String,
|
||||
name: json['name'] as String,
|
||||
button1Amount: (json['button1Amount'] as num).toInt(),
|
||||
button2Amount: (json['button2Amount'] as num).toInt(),
|
||||
goal: (json['goal'] as num).toInt(),
|
||||
current: (json['current'] as num).toInt(),
|
||||
isExpanded: json['isExpanded'] as bool,
|
||||
isVisible: json['isVisible'] as bool,
|
||||
createdOn: json['createdOn'] as String,
|
||||
completedOn: json['completedOn'] as String,
|
||||
id: (json['id'] as num?)?.toInt() ?? -1,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$HydrationImplToJson(_$HydrationImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'widgetType': instance.widgetType,
|
||||
'name': instance.name,
|
||||
'button1Amount': instance.button1Amount,
|
||||
'button2Amount': instance.button2Amount,
|
||||
'goal': instance.goal,
|
||||
'current': instance.current,
|
||||
'isExpanded': instance.isExpanded,
|
||||
'isVisible': instance.isVisible,
|
||||
'createdOn': instance.createdOn,
|
||||
'completedOn': instance.completedOn,
|
||||
'id': instance.id,
|
||||
};
|
9
app/lib/sembast/item_repository.dart
Normal file
9
app/lib/sembast/item_repository.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
abstract class ItemRepository {
|
||||
Future<int> insertItem(dynamic item);
|
||||
|
||||
Future<void> updateItem(dynamic item);
|
||||
|
||||
Future<void> deleteItem(int itemId);
|
||||
|
||||
Stream<List<dynamic>> getAllItemsStream();
|
||||
}
|
71
app/lib/sembast/sembast_item_repository.dart
Normal file
71
app/lib/sembast/sembast_item_repository.dart
Normal file
|
@ -0,0 +1,71 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:habitrack_app/main.dart';
|
||||
import 'package:habitrack_app/sembast/global_providers.dart';
|
||||
import 'package:habitrack_app/sembast/hydration.dart';
|
||||
import 'package:habitrack_app/sembast/item_repository.dart';
|
||||
import 'package:habitrack_app/sembast/tasks_list.dart';
|
||||
import 'package:habitrack_app/sembast/timer.dart';
|
||||
import 'package:sembast/sembast.dart';
|
||||
|
||||
final itemRepositoryProvider = Provider(
|
||||
(ref) => SembastItemRepository(
|
||||
database: ref.watch(databaseProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SembastItemRepository implements ItemRepository {
|
||||
SembastItemRepository({required this.database}) {
|
||||
_store = intMapStoreFactory.store('item_store');
|
||||
}
|
||||
final Database database;
|
||||
late final StoreRef<int, Map<String, dynamic>> _store;
|
||||
|
||||
@override
|
||||
Future<int> insertItem(dynamic item) {
|
||||
if (item is Hydration) {
|
||||
_store.add(database, item.toJson());
|
||||
} else if (item is TimerItem) {
|
||||
_store.add(database, item.toJson());
|
||||
} else if (item is TasksItem) {
|
||||
_store.add(database, item.toJson());
|
||||
}
|
||||
return Future.value(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateItem(dynamic item) {
|
||||
if (item is Hydration) {
|
||||
_store.record(item.id).update(database, item.toJson());
|
||||
} else if (item is TimerItem) {
|
||||
_store.record(item.id).update(database, item.toJson());
|
||||
} else if (item is TasksItem) {
|
||||
logger.i('UPDATING TASKS ITEM');
|
||||
_store.record(item.id).update(database, item.toJson());
|
||||
}
|
||||
logger.i('Item got past update: $item');
|
||||
return Future.value();
|
||||
// throw Error();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteItem(int itemId) => _store.record(itemId).delete(database);
|
||||
|
||||
@override
|
||||
Stream<List<dynamic>> getAllItemsStream() =>
|
||||
_store.query().onSnapshots(database).map(
|
||||
(snapshot) => snapshot.map((item) {
|
||||
logger.i('Querying the database! ${item.value}');
|
||||
if (item.value.toString().contains('widgetType: Hydration')) {
|
||||
logger.i('Le hydration has arrived');
|
||||
|
||||
return Hydration.fromJson(item.value).copyWith(id: item.key);
|
||||
} else if (item.value.toString().contains('widgetType: Timer')) {
|
||||
logger.i('Le timer has arrived');
|
||||
return TimerItem.fromJson(item.value).copyWith(id: item.key);
|
||||
} else if (item.value.toString().contains('widgetType: TODO')) {
|
||||
logger.i('Le TODO has arrived');
|
||||
return TasksItem.fromJson(item.value).copyWith(id: item.key);
|
||||
}
|
||||
}).toList(growable: false),
|
||||
);
|
||||
}
|
53
app/lib/sembast/tasks_list.dart
Normal file
53
app/lib/sembast/tasks_list.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
// ignore_for_file: flutter_style_todos
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'tasks_list.freezed.dart';
|
||||
part 'tasks_list.g.dart';
|
||||
|
||||
@freezed
|
||||
class IndividualTodo with _$IndividualTodo {
|
||||
const factory IndividualTodo({
|
||||
required String todo,
|
||||
required bool isCompleted,
|
||||
required DateTime dueDate,
|
||||
}) = _IndividualTodo;
|
||||
factory IndividualTodo.fromJson(Map<String, Object?> json) =>
|
||||
_$IndividualTodoFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class TasksItem with _$TasksItem {
|
||||
const factory TasksItem({
|
||||
required String widgetType,
|
||||
required String name,
|
||||
required bool isExpanded,
|
||||
required List<String> taskList,
|
||||
required List<String> completedTaskList,
|
||||
required bool isVisible,
|
||||
@Default(-1) int id,
|
||||
}) = _TasksItem;
|
||||
|
||||
factory TasksItem.fromJson(Map<String, Object?> json) =>
|
||||
_$TasksItemFromJson(json);
|
||||
}
|
||||
|
||||
extension JsonWithoutId on TasksItem {
|
||||
Map<String, dynamic> toJsonWithoutId() {
|
||||
final map = toJson()..remove('id');
|
||||
// map['taskList'] = taskList.map((todo) => todo.toJson()).toList();
|
||||
//map['completedTaskList'] =
|
||||
// completedTaskList.map((todo) => todo.toJson()).toList();
|
||||
return map;
|
||||
}
|
||||
}
|
||||
/*
|
||||
extension JsonWithoutID on TODO {
|
||||
Map<String, dynamic> toJsonWithoutId() {
|
||||
final map = toJson()..remove('id');
|
||||
map['taskList'] = taskList.map((todo) => todo.toJson()).toList();
|
||||
map['completedTaskList'] =
|
||||
completedTaskList.map((todo) => todo.toJson()).toList();
|
||||
return map;
|
||||
}
|
||||
}*/
|
502
app/lib/sembast/tasks_list.freezed.dart
Normal file
502
app/lib/sembast/tasks_list.freezed.dart
Normal file
|
@ -0,0 +1,502 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'tasks_list.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
IndividualTodo _$IndividualTodoFromJson(Map<String, dynamic> json) {
|
||||
return _IndividualTodo.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$IndividualTodo {
|
||||
String get todo => throw _privateConstructorUsedError;
|
||||
bool get isCompleted => throw _privateConstructorUsedError;
|
||||
DateTime get dueDate => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$IndividualTodoCopyWith<IndividualTodo> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $IndividualTodoCopyWith<$Res> {
|
||||
factory $IndividualTodoCopyWith(
|
||||
IndividualTodo value, $Res Function(IndividualTodo) then) =
|
||||
_$IndividualTodoCopyWithImpl<$Res, IndividualTodo>;
|
||||
@useResult
|
||||
$Res call({String todo, bool isCompleted, DateTime dueDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$IndividualTodoCopyWithImpl<$Res, $Val extends IndividualTodo>
|
||||
implements $IndividualTodoCopyWith<$Res> {
|
||||
_$IndividualTodoCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? todo = null,
|
||||
Object? isCompleted = null,
|
||||
Object? dueDate = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
todo: null == todo
|
||||
? _value.todo
|
||||
: todo // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isCompleted: null == isCompleted
|
||||
? _value.isCompleted
|
||||
: isCompleted // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
dueDate: null == dueDate
|
||||
? _value.dueDate
|
||||
: dueDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$IndividualTodoImplCopyWith<$Res>
|
||||
implements $IndividualTodoCopyWith<$Res> {
|
||||
factory _$$IndividualTodoImplCopyWith(_$IndividualTodoImpl value,
|
||||
$Res Function(_$IndividualTodoImpl) then) =
|
||||
__$$IndividualTodoImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String todo, bool isCompleted, DateTime dueDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$IndividualTodoImplCopyWithImpl<$Res>
|
||||
extends _$IndividualTodoCopyWithImpl<$Res, _$IndividualTodoImpl>
|
||||
implements _$$IndividualTodoImplCopyWith<$Res> {
|
||||
__$$IndividualTodoImplCopyWithImpl(
|
||||
_$IndividualTodoImpl _value, $Res Function(_$IndividualTodoImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? todo = null,
|
||||
Object? isCompleted = null,
|
||||
Object? dueDate = null,
|
||||
}) {
|
||||
return _then(_$IndividualTodoImpl(
|
||||
todo: null == todo
|
||||
? _value.todo
|
||||
: todo // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isCompleted: null == isCompleted
|
||||
? _value.isCompleted
|
||||
: isCompleted // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
dueDate: null == dueDate
|
||||
? _value.dueDate
|
||||
: dueDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$IndividualTodoImpl
|
||||
with DiagnosticableTreeMixin
|
||||
implements _IndividualTodo {
|
||||
const _$IndividualTodoImpl(
|
||||
{required this.todo, required this.isCompleted, required this.dueDate});
|
||||
|
||||
factory _$IndividualTodoImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$IndividualTodoImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String todo;
|
||||
@override
|
||||
final bool isCompleted;
|
||||
@override
|
||||
final DateTime dueDate;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'IndividualTodo(todo: $todo, isCompleted: $isCompleted, dueDate: $dueDate)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'IndividualTodo'))
|
||||
..add(DiagnosticsProperty('todo', todo))
|
||||
..add(DiagnosticsProperty('isCompleted', isCompleted))
|
||||
..add(DiagnosticsProperty('dueDate', dueDate));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$IndividualTodoImpl &&
|
||||
(identical(other.todo, todo) || other.todo == todo) &&
|
||||
(identical(other.isCompleted, isCompleted) ||
|
||||
other.isCompleted == isCompleted) &&
|
||||
(identical(other.dueDate, dueDate) || other.dueDate == dueDate));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, todo, isCompleted, dueDate);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$IndividualTodoImplCopyWith<_$IndividualTodoImpl> get copyWith =>
|
||||
__$$IndividualTodoImplCopyWithImpl<_$IndividualTodoImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$IndividualTodoImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _IndividualTodo implements IndividualTodo {
|
||||
const factory _IndividualTodo(
|
||||
{required final String todo,
|
||||
required final bool isCompleted,
|
||||
required final DateTime dueDate}) = _$IndividualTodoImpl;
|
||||
|
||||
factory _IndividualTodo.fromJson(Map<String, dynamic> json) =
|
||||
_$IndividualTodoImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get todo;
|
||||
@override
|
||||
bool get isCompleted;
|
||||
@override
|
||||
DateTime get dueDate;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$IndividualTodoImplCopyWith<_$IndividualTodoImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
TasksItem _$TasksItemFromJson(Map<String, dynamic> json) {
|
||||
return _TasksItem.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TasksItem {
|
||||
String get widgetType => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
bool get isExpanded => throw _privateConstructorUsedError;
|
||||
List<String> get taskList => throw _privateConstructorUsedError;
|
||||
List<String> get completedTaskList => throw _privateConstructorUsedError;
|
||||
bool get isVisible => throw _privateConstructorUsedError;
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TasksItemCopyWith<TasksItem> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $TasksItemCopyWith<$Res> {
|
||||
factory $TasksItemCopyWith(TasksItem value, $Res Function(TasksItem) then) =
|
||||
_$TasksItemCopyWithImpl<$Res, TasksItem>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String widgetType,
|
||||
String name,
|
||||
bool isExpanded,
|
||||
List<String> taskList,
|
||||
List<String> completedTaskList,
|
||||
bool isVisible,
|
||||
int id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$TasksItemCopyWithImpl<$Res, $Val extends TasksItem>
|
||||
implements $TasksItemCopyWith<$Res> {
|
||||
_$TasksItemCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? widgetType = null,
|
||||
Object? name = null,
|
||||
Object? isExpanded = null,
|
||||
Object? taskList = null,
|
||||
Object? completedTaskList = null,
|
||||
Object? isVisible = null,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
widgetType: null == widgetType
|
||||
? _value.widgetType
|
||||
: widgetType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isExpanded: null == isExpanded
|
||||
? _value.isExpanded
|
||||
: isExpanded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
taskList: null == taskList
|
||||
? _value.taskList
|
||||
: taskList // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
completedTaskList: null == completedTaskList
|
||||
? _value.completedTaskList
|
||||
: completedTaskList // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
isVisible: null == isVisible
|
||||
? _value.isVisible
|
||||
: isVisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$TasksItemImplCopyWith<$Res>
|
||||
implements $TasksItemCopyWith<$Res> {
|
||||
factory _$$TasksItemImplCopyWith(
|
||||
_$TasksItemImpl value, $Res Function(_$TasksItemImpl) then) =
|
||||
__$$TasksItemImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String widgetType,
|
||||
String name,
|
||||
bool isExpanded,
|
||||
List<String> taskList,
|
||||
List<String> completedTaskList,
|
||||
bool isVisible,
|
||||
int id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$TasksItemImplCopyWithImpl<$Res>
|
||||
extends _$TasksItemCopyWithImpl<$Res, _$TasksItemImpl>
|
||||
implements _$$TasksItemImplCopyWith<$Res> {
|
||||
__$$TasksItemImplCopyWithImpl(
|
||||
_$TasksItemImpl _value, $Res Function(_$TasksItemImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? widgetType = null,
|
||||
Object? name = null,
|
||||
Object? isExpanded = null,
|
||||
Object? taskList = null,
|
||||
Object? completedTaskList = null,
|
||||
Object? isVisible = null,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(_$TasksItemImpl(
|
||||
widgetType: null == widgetType
|
||||
? _value.widgetType
|
||||
: widgetType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
isExpanded: null == isExpanded
|
||||
? _value.isExpanded
|
||||
: isExpanded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
taskList: null == taskList
|
||||
? _value._taskList
|
||||
: taskList // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
completedTaskList: null == completedTaskList
|
||||
? _value._completedTaskList
|
||||
: completedTaskList // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
isVisible: null == isVisible
|
||||
? _value.isVisible
|
||||
: isVisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$TasksItemImpl with DiagnosticableTreeMixin implements _TasksItem {
|
||||
const _$TasksItemImpl(
|
||||
{required this.widgetType,
|
||||
required this.name,
|
||||
required this.isExpanded,
|
||||
required final List<String> taskList,
|
||||
required final List<String> completedTaskList,
|
||||
required this.isVisible,
|
||||
this.id = -1})
|
||||
: _taskList = taskList,
|
||||
_completedTaskList = completedTaskList;
|
||||
|
||||
factory _$TasksItemImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$TasksItemImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String widgetType;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final bool isExpanded;
|
||||
final List<String> _taskList;
|
||||
@override
|
||||
List<String> get taskList {
|
||||
if (_taskList is EqualUnmodifiableListView) return _taskList;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_taskList);
|
||||
}
|
||||
|
||||
final List<String> _completedTaskList;
|
||||
@override
|
||||
List<String> get completedTaskList {
|
||||
if (_completedTaskList is EqualUnmodifiableListView)
|
||||
return _completedTaskList;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_completedTaskList);
|
||||
}
|
||||
|
||||
@override
|
||||
final bool isVisible;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int id;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'TasksItem(widgetType: $widgetType, name: $name, isExpanded: $isExpanded, taskList: $taskList, completedTaskList: $completedTaskList, isVisible: $isVisible, id: $id)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'TasksItem'))
|
||||
..add(DiagnosticsProperty('widgetType', widgetType))
|
||||
..add(DiagnosticsProperty('name', name))
|
||||
..add(DiagnosticsProperty('isExpanded', isExpanded))
|
||||
..add(DiagnosticsProperty('taskList', taskList))
|
||||
..add(DiagnosticsProperty('completedTaskList', completedTaskList))
|
||||
..add(DiagnosticsProperty('isVisible', isVisible))
|
||||
..add(DiagnosticsProperty('id', id));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$TasksItemImpl &&
|
||||
(identical(other.widgetType, widgetType) ||
|
||||
other.widgetType == widgetType) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.isExpanded, isExpanded) ||
|
||||
other.isExpanded == isExpanded) &&
|
||||
const DeepCollectionEquality().equals(other._taskList, _taskList) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._completedTaskList, _completedTaskList) &&
|
||||
(identical(other.isVisible, isVisible) ||
|
||||
other.isVisible == isVisible) &&
|
||||
(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
widgetType,
|
||||
name,
|
||||
isExpanded,
|
||||
const DeepCollectionEquality().hash(_taskList),
|
||||
const DeepCollectionEquality().hash(_completedTaskList),
|
||||
isVisible,
|
||||
id);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$TasksItemImplCopyWith<_$TasksItemImpl> get copyWith =>
|
||||
__$$TasksItemImplCopyWithImpl<_$TasksItemImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$TasksItemImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TasksItem implements TasksItem {
|
||||
const factory _TasksItem(
|
||||
{required final String widgetType,
|
||||
required final String name,
|
||||
required final bool isExpanded,
|
||||
required final List<String> taskList,
|
||||
required final List<String> completedTaskList,
|
||||
required final bool isVisible,
|
||||
final int id}) = _$TasksItemImpl;
|
||||
|
||||
factory _TasksItem.fromJson(Map<String, dynamic> json) =
|
||||
_$TasksItemImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get widgetType;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
bool get isExpanded;
|
||||
@override
|
||||
List<String> get taskList;
|
||||
@override
|
||||
List<String> get completedTaskList;
|
||||
@override
|
||||
bool get isVisible;
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$TasksItemImplCopyWith<_$TasksItemImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
47
app/lib/sembast/tasks_list.g.dart
Normal file
47
app/lib/sembast/tasks_list.g.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tasks_list.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$IndividualTodoImpl _$$IndividualTodoImplFromJson(Map<String, dynamic> json) =>
|
||||
_$IndividualTodoImpl(
|
||||
todo: json['todo'] as String,
|
||||
isCompleted: json['isCompleted'] as bool,
|
||||
dueDate: DateTime.parse(json['dueDate'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$IndividualTodoImplToJson(
|
||||
_$IndividualTodoImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'todo': instance.todo,
|
||||
'isCompleted': instance.isCompleted,
|
||||
'dueDate': instance.dueDate.toIso8601String(),
|
||||
};
|
||||
|
||||
_$TasksItemImpl _$$TasksItemImplFromJson(Map<String, dynamic> json) =>
|
||||
_$TasksItemImpl(
|
||||
widgetType: json['widgetType'] as String,
|
||||
name: json['name'] as String,
|
||||
isExpanded: json['isExpanded'] as bool,
|
||||
taskList:
|
||||
(json['taskList'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
completedTaskList: (json['completedTaskList'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
isVisible: json['isVisible'] as bool,
|
||||
id: (json['id'] as num?)?.toInt() ?? -1,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TasksItemImplToJson(_$TasksItemImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'widgetType': instance.widgetType,
|
||||
'name': instance.name,
|
||||
'isExpanded': instance.isExpanded,
|
||||
'taskList': instance.taskList,
|
||||
'completedTaskList': instance.completedTaskList,
|
||||
'isVisible': instance.isVisible,
|
||||
'id': instance.id,
|
||||
};
|
50
app/lib/sembast/timer.dart
Normal file
50
app/lib/sembast/timer.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'timer.freezed.dart';
|
||||
part 'timer.g.dart';
|
||||
|
||||
@freezed
|
||||
class TimerItem with _$TimerItem {
|
||||
const factory TimerItem({
|
||||
required String widgetType,
|
||||
required String name,
|
||||
required int current,
|
||||
required int goal,
|
||||
required bool isExpanded,
|
||||
required bool isVisible,
|
||||
required String createdOn,
|
||||
required String completedOn,
|
||||
required String state,
|
||||
@Default(-1) int id,
|
||||
}) = _TimerItem;
|
||||
|
||||
factory TimerItem.fromJson(Map<String, Object?> json) =>
|
||||
_$TimerItemFromJson(json);
|
||||
}
|
||||
|
||||
extension JsonWithoutId on TimerItem {
|
||||
Map<String, dynamic> toJsonWithoutId() {
|
||||
final map = toJson()..remove('id');
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@freezed
|
||||
class Student with _$Student {
|
||||
const factory Student({
|
||||
required String name,
|
||||
int? id,
|
||||
}) = _Student;
|
||||
|
||||
factory Student.fromJson(Map<String, Object?> json) =>
|
||||
_$StudentFromJson(json);
|
||||
}
|
||||
|
||||
extension JsonWithoutId on Student {
|
||||
Map<String, dynamic> toJsonWithoutId() {
|
||||
final map = toJson()..remove('id');
|
||||
return map;
|
||||
}
|
||||
}
|
||||
*/
|
358
app/lib/sembast/timer.freezed.dart
Normal file
358
app/lib/sembast/timer.freezed.dart
Normal file
|
@ -0,0 +1,358 @@
|
|||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'timer.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
TimerItem _$TimerItemFromJson(Map<String, dynamic> json) {
|
||||
return _TimerItem.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$TimerItem {
|
||||
String get widgetType => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
int get current => throw _privateConstructorUsedError;
|
||||
int get goal => throw _privateConstructorUsedError;
|
||||
bool get isExpanded => throw _privateConstructorUsedError;
|
||||
bool get isVisible => throw _privateConstructorUsedError;
|
||||
String get createdOn => throw _privateConstructorUsedError;
|
||||
String get completedOn => throw _privateConstructorUsedError;
|
||||
String get state => throw _privateConstructorUsedError;
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$TimerItemCopyWith<TimerItem> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $TimerItemCopyWith<$Res> {
|
||||
factory $TimerItemCopyWith(TimerItem value, $Res Function(TimerItem) then) =
|
||||
_$TimerItemCopyWithImpl<$Res, TimerItem>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{String widgetType,
|
||||
String name,
|
||||
int current,
|
||||
int goal,
|
||||
bool isExpanded,
|
||||
bool isVisible,
|
||||
String createdOn,
|
||||
String completedOn,
|
||||
String state,
|
||||
int id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$TimerItemCopyWithImpl<$Res, $Val extends TimerItem>
|
||||
implements $TimerItemCopyWith<$Res> {
|
||||
_$TimerItemCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? widgetType = null,
|
||||
Object? name = null,
|
||||
Object? current = null,
|
||||
Object? goal = null,
|
||||
Object? isExpanded = null,
|
||||
Object? isVisible = null,
|
||||
Object? createdOn = null,
|
||||
Object? completedOn = null,
|
||||
Object? state = null,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
widgetType: null == widgetType
|
||||
? _value.widgetType
|
||||
: widgetType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
current: null == current
|
||||
? _value.current
|
||||
: current // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
goal: null == goal
|
||||
? _value.goal
|
||||
: goal // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isExpanded: null == isExpanded
|
||||
? _value.isExpanded
|
||||
: isExpanded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isVisible: null == isVisible
|
||||
? _value.isVisible
|
||||
: isVisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
createdOn: null == createdOn
|
||||
? _value.createdOn
|
||||
: createdOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
completedOn: null == completedOn
|
||||
? _value.completedOn
|
||||
: completedOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
state: null == state
|
||||
? _value.state
|
||||
: state // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$TimerItemImplCopyWith<$Res>
|
||||
implements $TimerItemCopyWith<$Res> {
|
||||
factory _$$TimerItemImplCopyWith(
|
||||
_$TimerItemImpl value, $Res Function(_$TimerItemImpl) then) =
|
||||
__$$TimerItemImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{String widgetType,
|
||||
String name,
|
||||
int current,
|
||||
int goal,
|
||||
bool isExpanded,
|
||||
bool isVisible,
|
||||
String createdOn,
|
||||
String completedOn,
|
||||
String state,
|
||||
int id});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$TimerItemImplCopyWithImpl<$Res>
|
||||
extends _$TimerItemCopyWithImpl<$Res, _$TimerItemImpl>
|
||||
implements _$$TimerItemImplCopyWith<$Res> {
|
||||
__$$TimerItemImplCopyWithImpl(
|
||||
_$TimerItemImpl _value, $Res Function(_$TimerItemImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? widgetType = null,
|
||||
Object? name = null,
|
||||
Object? current = null,
|
||||
Object? goal = null,
|
||||
Object? isExpanded = null,
|
||||
Object? isVisible = null,
|
||||
Object? createdOn = null,
|
||||
Object? completedOn = null,
|
||||
Object? state = null,
|
||||
Object? id = null,
|
||||
}) {
|
||||
return _then(_$TimerItemImpl(
|
||||
widgetType: null == widgetType
|
||||
? _value.widgetType
|
||||
: widgetType // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
current: null == current
|
||||
? _value.current
|
||||
: current // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
goal: null == goal
|
||||
? _value.goal
|
||||
: goal // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
isExpanded: null == isExpanded
|
||||
? _value.isExpanded
|
||||
: isExpanded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isVisible: null == isVisible
|
||||
? _value.isVisible
|
||||
: isVisible // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
createdOn: null == createdOn
|
||||
? _value.createdOn
|
||||
: createdOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
completedOn: null == completedOn
|
||||
? _value.completedOn
|
||||
: completedOn // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
state: null == state
|
||||
? _value.state
|
||||
: state // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$TimerItemImpl with DiagnosticableTreeMixin implements _TimerItem {
|
||||
const _$TimerItemImpl(
|
||||
{required this.widgetType,
|
||||
required this.name,
|
||||
required this.current,
|
||||
required this.goal,
|
||||
required this.isExpanded,
|
||||
required this.isVisible,
|
||||
required this.createdOn,
|
||||
required this.completedOn,
|
||||
required this.state,
|
||||
this.id = -1});
|
||||
|
||||
factory _$TimerItemImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$TimerItemImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String widgetType;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final int current;
|
||||
@override
|
||||
final int goal;
|
||||
@override
|
||||
final bool isExpanded;
|
||||
@override
|
||||
final bool isVisible;
|
||||
@override
|
||||
final String createdOn;
|
||||
@override
|
||||
final String completedOn;
|
||||
@override
|
||||
final String state;
|
||||
@override
|
||||
@JsonKey()
|
||||
final int id;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'TimerItem(widgetType: $widgetType, name: $name, current: $current, goal: $goal, isExpanded: $isExpanded, isVisible: $isVisible, createdOn: $createdOn, completedOn: $completedOn, state: $state, id: $id)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'TimerItem'))
|
||||
..add(DiagnosticsProperty('widgetType', widgetType))
|
||||
..add(DiagnosticsProperty('name', name))
|
||||
..add(DiagnosticsProperty('current', current))
|
||||
..add(DiagnosticsProperty('goal', goal))
|
||||
..add(DiagnosticsProperty('isExpanded', isExpanded))
|
||||
..add(DiagnosticsProperty('isVisible', isVisible))
|
||||
..add(DiagnosticsProperty('createdOn', createdOn))
|
||||
..add(DiagnosticsProperty('completedOn', completedOn))
|
||||
..add(DiagnosticsProperty('state', state))
|
||||
..add(DiagnosticsProperty('id', id));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$TimerItemImpl &&
|
||||
(identical(other.widgetType, widgetType) ||
|
||||
other.widgetType == widgetType) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.current, current) || other.current == current) &&
|
||||
(identical(other.goal, goal) || other.goal == goal) &&
|
||||
(identical(other.isExpanded, isExpanded) ||
|
||||
other.isExpanded == isExpanded) &&
|
||||
(identical(other.isVisible, isVisible) ||
|
||||
other.isVisible == isVisible) &&
|
||||
(identical(other.createdOn, createdOn) ||
|
||||
other.createdOn == createdOn) &&
|
||||
(identical(other.completedOn, completedOn) ||
|
||||
other.completedOn == completedOn) &&
|
||||
(identical(other.state, state) || other.state == state) &&
|
||||
(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, widgetType, name, current, goal,
|
||||
isExpanded, isVisible, createdOn, completedOn, state, id);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$TimerItemImplCopyWith<_$TimerItemImpl> get copyWith =>
|
||||
__$$TimerItemImplCopyWithImpl<_$TimerItemImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$TimerItemImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _TimerItem implements TimerItem {
|
||||
const factory _TimerItem(
|
||||
{required final String widgetType,
|
||||
required final String name,
|
||||
required final int current,
|
||||
required final int goal,
|
||||
required final bool isExpanded,
|
||||
required final bool isVisible,
|
||||
required final String createdOn,
|
||||
required final String completedOn,
|
||||
required final String state,
|
||||
final int id}) = _$TimerItemImpl;
|
||||
|
||||
factory _TimerItem.fromJson(Map<String, dynamic> json) =
|
||||
_$TimerItemImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get widgetType;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
int get current;
|
||||
@override
|
||||
int get goal;
|
||||
@override
|
||||
bool get isExpanded;
|
||||
@override
|
||||
bool get isVisible;
|
||||
@override
|
||||
String get createdOn;
|
||||
@override
|
||||
String get completedOn;
|
||||
@override
|
||||
String get state;
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$TimerItemImplCopyWith<_$TimerItemImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
35
app/lib/sembast/timer.g.dart
Normal file
35
app/lib/sembast/timer.g.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timer.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$TimerItemImpl _$$TimerItemImplFromJson(Map<String, dynamic> json) =>
|
||||
_$TimerItemImpl(
|
||||
widgetType: json['widgetType'] as String,
|
||||
name: json['name'] as String,
|
||||
current: (json['current'] as num).toInt(),
|
||||
goal: (json['goal'] as num).toInt(),
|
||||
isExpanded: json['isExpanded'] as bool,
|
||||
isVisible: json['isVisible'] as bool,
|
||||
createdOn: json['createdOn'] as String,
|
||||
completedOn: json['completedOn'] as String,
|
||||
state: json['state'] as String,
|
||||
id: (json['id'] as num?)?.toInt() ?? -1,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$TimerItemImplToJson(_$TimerItemImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'widgetType': instance.widgetType,
|
||||
'name': instance.name,
|
||||
'current': instance.current,
|
||||
'goal': instance.goal,
|
||||
'isExpanded': instance.isExpanded,
|
||||
'isVisible': instance.isVisible,
|
||||
'createdOn': instance.createdOn,
|
||||
'completedOn': instance.completedOn,
|
||||
'state': instance.state,
|
||||
'id': instance.id,
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue