-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
255 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
enum AnalyticsPeriod { | ||
day, | ||
week, | ||
month, | ||
year, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import 'package:emma/ui/analytics/analytics_period.dart'; | ||
import 'package:emma/ui/analytics/analytics_view_model.dart'; | ||
import 'package:emma/ui/app_icons.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:intl/intl.dart'; | ||
import 'package:signals/signals_flutter.dart'; | ||
|
||
class AnalyticsRangePicker extends StatefulWidget { | ||
const AnalyticsRangePicker({ | ||
super.key, | ||
required this.viewModel, | ||
}); | ||
|
||
final AnalyticsViewModel viewModel; | ||
|
||
@override | ||
State<AnalyticsRangePicker> createState() => _AnalyticsRangePickerState(); | ||
} | ||
|
||
class _AnalyticsRangePickerState extends State<AnalyticsRangePicker> { | ||
late final TextEditingController _rangeTextController; | ||
late final void Function() _disposeRangeTextEffect; | ||
|
||
AnalyticsViewModel get _vm => widget.viewModel; | ||
|
||
@override | ||
void initState() { | ||
_rangeTextController = TextEditingController( | ||
text: _getRangeText(_vm.period.value, _vm.range.value)); | ||
|
||
_disposeRangeTextEffect = effect( | ||
() => _rangeTextController.text = | ||
_getRangeText(_vm.period.value, _vm.range.value), | ||
debugLabel: "analytics.screen.rangeText", | ||
); | ||
|
||
super.initState(); | ||
} | ||
|
||
@override | ||
void dispose() { | ||
_disposeRangeTextEffect(); | ||
_rangeTextController.dispose(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Column( | ||
mainAxisSize: MainAxisSize.min, | ||
children: [ | ||
Row( | ||
children: [ | ||
Expanded( | ||
child: Watch( | ||
(context) => SegmentedButton<AnalyticsPeriod>( | ||
segments: const [ | ||
ButtonSegment( | ||
value: AnalyticsPeriod.day, | ||
label: Text("Tag"), | ||
), | ||
ButtonSegment( | ||
value: AnalyticsPeriod.week, | ||
label: Text("Woche"), | ||
), | ||
ButtonSegment( | ||
value: AnalyticsPeriod.month, | ||
label: Text("Monat"), | ||
), | ||
ButtonSegment( | ||
value: AnalyticsPeriod.year, | ||
label: Text("Jahr"), | ||
) | ||
], | ||
selected: {_vm.period.value}, | ||
showSelectedIcon: false, | ||
onSelectionChanged: (selection) => | ||
_vm.setPeriod(selection.first), | ||
), | ||
), | ||
), | ||
], | ||
), | ||
Row( | ||
crossAxisAlignment: CrossAxisAlignment.center, | ||
children: [ | ||
IconButton( | ||
onPressed: _vm.setPreviousRange, | ||
icon: const Icon(AppIcons.arrow_prev)), | ||
Expanded( | ||
child: TextField( | ||
controller: _rangeTextController, | ||
readOnly: true, | ||
textAlign: TextAlign.center, | ||
), | ||
), | ||
Watch( | ||
(context) => IconButton( | ||
onPressed: | ||
_vm.canSetNextRange.value ? _vm.setNextRange : null, | ||
icon: const Icon(AppIcons.arrow_next)), | ||
), | ||
], | ||
), | ||
], | ||
); | ||
} | ||
|
||
static String _getRangeText(AnalyticsPeriod period, DateTimeRange range) { | ||
final start = range.start; | ||
switch (period) { | ||
case AnalyticsPeriod.day: | ||
return DateFormat.yMd().format(start); | ||
|
||
case AnalyticsPeriod.week: | ||
final end = range.end.subtract(const Duration(seconds: 1)); | ||
final f = DateFormat.yMd(); | ||
return "${f.format(start)} - ${f.format(end)}"; | ||
|
||
case AnalyticsPeriod.month: | ||
return DateFormat.yMMM().format(start); | ||
|
||
case AnalyticsPeriod.year: | ||
return DateFormat.y().format(start); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,167 +1,36 @@ | ||
import 'package:emma/ui/analytics/analytics_range_picker.dart'; | ||
import 'package:emma/ui/analytics/analytics_view_model.dart'; | ||
import 'package:emma/ui/analytics/consumption_overview.dart'; | ||
import 'package:emma/ui/analytics/production_overview.dart'; | ||
import 'package:emma/ui/app_icons.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:intl/intl.dart'; | ||
|
||
// AutomaticKeepAliveClientMixin to keep alive | ||
// https://pub.dev/packages/month_year_picker | ||
class AnalyticsScreen extends StatefulWidget { | ||
const AnalyticsScreen({super.key}); | ||
|
||
@override | ||
State<AnalyticsScreen> createState() => _AnalyticsScreenState(); | ||
} | ||
|
||
enum AnalyticsPeriod { | ||
day, | ||
week, | ||
month, | ||
year, | ||
} | ||
|
||
class _AnalyticsScreenState extends State<AnalyticsScreen> { | ||
var _period = AnalyticsPeriod.day; | ||
var _range = DateTimeRange(start: _today(), end: _today()); | ||
|
||
String get _periodText { | ||
return switch (_period) { | ||
AnalyticsPeriod.day => "Tag", | ||
AnalyticsPeriod.week => "Woche", | ||
AnalyticsPeriod.month => "Monat", | ||
AnalyticsPeriod.year => "Jahr", | ||
}; | ||
} | ||
|
||
String get _rangeText { | ||
final start = _range.start; | ||
switch (_period) { | ||
case AnalyticsPeriod.day: | ||
return DateFormat.yMd().format(start); | ||
case AnalyticsPeriod.week: | ||
final end = _range.end.subtract(const Duration(seconds: 1)); | ||
final f = DateFormat.yMd(); | ||
return "${f.format(start)} - ${f.format(end)}"; | ||
case AnalyticsPeriod.month: | ||
return DateFormat.yMMM().format(start); | ||
case AnalyticsPeriod.year: | ||
return DateFormat.y().format(start); | ||
} | ||
} | ||
final _vm = AnalyticsViewModel(); | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Scaffold( | ||
body: Padding( | ||
padding: const EdgeInsets.all(16.0), | ||
child: Wrap( | ||
runSpacing: 32, | ||
children: [ | ||
Column( | ||
children: [ | ||
Row( | ||
children: [ | ||
IconButton( | ||
onPressed: _setPreviousPeriod, | ||
icon: const Icon(AppIcons.arrow_prev)), | ||
Expanded( | ||
child: Center( | ||
child: Text(_periodText), | ||
)), | ||
IconButton( | ||
onPressed: _setNextPeriod, | ||
icon: const Icon(AppIcons.arrow_next)), | ||
], | ||
), | ||
Row( | ||
children: [ | ||
IconButton( | ||
onPressed: _setPreviousRange, | ||
icon: const Icon(AppIcons.arrow_prev)), | ||
Expanded( | ||
child: Center( | ||
child: Text(_rangeText), | ||
)), | ||
IconButton( | ||
onPressed: _canSetNextRange() ? _setNextRange : null, | ||
icon: const Icon(AppIcons.arrow_next)), | ||
], | ||
), | ||
], | ||
), | ||
const ProductionOverview(), | ||
const ConsumptionOverview(), | ||
], | ||
), | ||
body: ListView( | ||
padding: const EdgeInsets.symmetric(horizontal: 16.0), | ||
children: [ | ||
Padding( | ||
padding: const EdgeInsets.only(top: 16.0), | ||
child: AnalyticsRangePicker(viewModel: _vm), | ||
), | ||
const SizedBox(height: 32), | ||
const ConsumptionOverview(), | ||
const SizedBox(height: 32), | ||
const ProductionOverview(), | ||
], | ||
), | ||
); | ||
} | ||
|
||
void _setNextPeriod() { | ||
var index = AnalyticsPeriod.values.indexOf(_period); | ||
index = (index + 1) % AnalyticsPeriod.values.length; | ||
_setPeriod(AnalyticsPeriod.values[index]); | ||
} | ||
|
||
void _setPreviousPeriod() { | ||
var index = AnalyticsPeriod.values.indexOf(_period); | ||
index = (index - 1) % AnalyticsPeriod.values.length; | ||
_setPeriod(AnalyticsPeriod.values[index]); | ||
} | ||
|
||
void _setPeriod(AnalyticsPeriod period) { | ||
setState(() { | ||
_period = period; | ||
_range = _computeDateRange(period, _today()); | ||
}); | ||
} | ||
|
||
void _setPreviousRange() { | ||
final DateTimeRange range = _computeDateRange( | ||
_period, _range.start.subtract(const Duration(days: 1))); | ||
|
||
setState(() { | ||
_range = range; | ||
}); | ||
} | ||
|
||
bool _canSetNextRange() { | ||
final max = _computeDateRange(_period, _today()); | ||
return _range.start.isBefore(max.start); | ||
} | ||
|
||
void _setNextRange() { | ||
final DateTimeRange range = | ||
_computeDateRange(_period, _range.end.add(const Duration(days: 1))); | ||
|
||
setState(() { | ||
_range = range; | ||
}); | ||
} | ||
|
||
static DateTimeRange _computeDateRange( | ||
AnalyticsPeriod period, DateTime seed) { | ||
switch (period) { | ||
case AnalyticsPeriod.day: | ||
return DateTimeRange(start: seed, end: seed); | ||
case AnalyticsPeriod.week: | ||
// Monday = 1 | ||
final dayOfWeek = seed.weekday; | ||
final start = seed.subtract(Duration(days: dayOfWeek - 1)); | ||
final end = start.add(const Duration(days: 7)); | ||
return DateTimeRange(start: start, end: end); | ||
case AnalyticsPeriod.month: | ||
return DateTimeRange( | ||
start: DateTime(seed.year, seed.month), | ||
end: DateTime(seed.year, seed.month + 1)); | ||
case AnalyticsPeriod.year: | ||
return DateTimeRange( | ||
start: DateTime(seed.year), end: DateTime(seed.year + 1)); | ||
} | ||
} | ||
|
||
static DateTime _today() { | ||
final now = DateTime.now(); | ||
return DateTime(now.year, now.month, now.day); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import 'package:emma/ui/analytics/analytics_period.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:signals/signals.dart'; | ||
|
||
class AnalyticsViewModel { | ||
final _period = signal( | ||
AnalyticsPeriod.day, | ||
debugLabel: "analytics.vm.period", | ||
); | ||
|
||
final _range = signal( | ||
DateTimeRange(start: _today(), end: _today()), | ||
debugLabel: "analytics.vm.range", | ||
); | ||
|
||
ReadonlySignal<AnalyticsPeriod> get period => _period; | ||
ReadonlySignal<DateTimeRange> get range => _range; | ||
late final ReadonlySignal<bool> canSetNextRange = computed( | ||
() { | ||
final max = _computeDateRange(_period.value, _today()); | ||
return _range.value.start.isBefore(max.start); | ||
}, | ||
debugLabel: "analytics.vm.canSetNextRange", | ||
); | ||
|
||
void setPeriod(AnalyticsPeriod period) { | ||
batch(() { | ||
_period.value = period; | ||
_range.value = _computeDateRange(period, _today()); | ||
}); | ||
} | ||
|
||
void setNextRange() { | ||
if (!canSetNextRange.value) { | ||
return; | ||
} | ||
|
||
_range.value = _computeDateRange( | ||
_period.value, | ||
_range.value.end.add(const Duration(days: 1)), | ||
); | ||
} | ||
|
||
void setPreviousRange() { | ||
_range.value = _computeDateRange( | ||
_period.value, | ||
_range.value.start.subtract(const Duration(days: 1)), | ||
); | ||
} | ||
|
||
static DateTimeRange _computeDateRange( | ||
AnalyticsPeriod period, DateTime seed) { | ||
switch (period) { | ||
case AnalyticsPeriod.day: | ||
return DateTimeRange(start: seed, end: seed); | ||
case AnalyticsPeriod.week: | ||
// Monday = 1 | ||
final dayOfWeek = seed.weekday; | ||
final start = seed.subtract(Duration(days: dayOfWeek - 1)); | ||
final end = start.add(const Duration(days: 7)); | ||
return DateTimeRange(start: start, end: end); | ||
case AnalyticsPeriod.month: | ||
return DateTimeRange( | ||
start: DateTime(seed.year, seed.month), | ||
end: DateTime(seed.year, seed.month + 1)); | ||
case AnalyticsPeriod.year: | ||
return DateTimeRange( | ||
start: DateTime(seed.year), end: DateTime(seed.year + 1)); | ||
} | ||
} | ||
|
||
static DateTime _today() { | ||
final now = DateTime.now(); | ||
return DateTime(now.year, now.month, now.day); | ||
} | ||
} |
Oops, something went wrong.