Skip to content

Commit 91e0eb3

Browse files
authored
Added time range selector (#497)
* Renamed ToggleButton to more descriptive ChartSeriesSelector * Added time range selector * Tests for TimeRangeSelector * Redesigned pH/temp toggle * Fetching log data in GraphPage instead - prevents re-fetching every time state of GraphView is changed. * Custom time range picker * Ensured custom time range cannot exceed avaliable time range
1 parent c43c197 commit 91e0eb3

12 files changed

Lines changed: 808 additions & 183 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import 'package:flutter/material.dart';
2+
3+
class ChartSeriesSelector extends StatefulWidget {
4+
const ChartSeriesSelector({required this.onPressed, super.key});
5+
final void Function(int) onPressed;
6+
7+
@override
8+
State<ChartSeriesSelector> createState() => _ChartSeriesSelectorState();
9+
}
10+
11+
class _ChartSeriesSelectorState extends State<ChartSeriesSelector> {
12+
final List<bool> _buttonState = [true, true];
13+
14+
void toggleButton(int index) {
15+
setState(() {
16+
_buttonState[index] = !_buttonState[index];
17+
widget.onPressed(index);
18+
});
19+
}
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
return Container(
24+
padding: const EdgeInsets.all(4),
25+
decoration: BoxDecoration(
26+
color: Colors.grey[100],
27+
borderRadius: BorderRadius.circular(8),
28+
),
29+
child: SizedBox(
30+
child: Row(
31+
mainAxisAlignment: MainAxisAlignment.center,
32+
mainAxisSize: MainAxisSize.min,
33+
children: [
34+
_chartSeriesOption('pH', Colors.green, 0),
35+
SizedBox(width: 4),
36+
_chartSeriesOption('temp', Colors.blue, 1),
37+
],
38+
),
39+
),
40+
);
41+
}
42+
43+
Widget _chartSeriesOption(String text, Color color, int index) {
44+
final active = _buttonState[index];
45+
return MouseRegion(
46+
cursor: SystemMouseCursors.click,
47+
child: GestureDetector(
48+
onTap: () => toggleButton(index),
49+
child: Container(
50+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
51+
decoration: BoxDecoration(
52+
color: active ? color : Colors.grey[100],
53+
borderRadius: BorderRadius.circular(8),
54+
boxShadow: active
55+
? [
56+
BoxShadow(
57+
color: Colors.grey[300]!,
58+
blurRadius: 4,
59+
offset: const Offset(0, 2),
60+
),
61+
]
62+
: null,
63+
),
64+
child: Text(
65+
text,
66+
style: TextStyle(color: active ? Colors.white : color),
67+
),
68+
),
69+
),
70+
);
71+
}
72+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import 'package:date_picker_plus/date_picker_plus.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:intl/intl.dart';
4+
import 'package:log_file_client/components/wheel_picker.dart';
5+
6+
enum RangeEndpointType { start, end }
7+
8+
class CustomTimeRangePicker extends StatefulWidget {
9+
const CustomTimeRangePicker({
10+
required this.timeRange,
11+
required this.onApplied,
12+
super.key,
13+
});
14+
15+
final DateTimeRange timeRange;
16+
final void Function(DateTimeRange) onApplied;
17+
18+
@override
19+
State<CustomTimeRangePicker> createState() => _CustomTimeRangePickerState();
20+
}
21+
22+
class _CustomTimeRangePickerState extends State<CustomTimeRangePicker> {
23+
late DateTime minTime;
24+
late DateTime maxTime;
25+
26+
@override
27+
void initState() {
28+
super.initState();
29+
minTime = widget.timeRange.start;
30+
maxTime = widget.timeRange.end;
31+
}
32+
33+
void onDatesSelected(DateTimeRange value) {
34+
minTime = DateTime(
35+
value.start.year,
36+
value.start.month,
37+
value.start.day,
38+
minTime.hour,
39+
minTime.minute,
40+
);
41+
maxTime = DateTime(
42+
value.end.year,
43+
value.end.month,
44+
value.end.day,
45+
maxTime.hour,
46+
maxTime.minute,
47+
);
48+
}
49+
50+
void onTimeSelected(TimeOfDay time, RangeEndpointType type) {
51+
if (type == RangeEndpointType.start) {
52+
minTime = DateTime(
53+
minTime.year,
54+
minTime.month,
55+
minTime.day,
56+
time.hour,
57+
time.minute,
58+
);
59+
} else if (type == RangeEndpointType.end) {
60+
maxTime = DateTime(
61+
maxTime.year,
62+
maxTime.month,
63+
maxTime.day,
64+
time.hour,
65+
time.minute,
66+
);
67+
}
68+
}
69+
70+
void onApplied() {
71+
// Ensure the time range is within the available range
72+
if (minTime.isBefore(widget.timeRange.start)) {
73+
minTime = widget.timeRange.start;
74+
}
75+
if (maxTime.isAfter(widget.timeRange.end)) {
76+
maxTime = widget.timeRange.end;
77+
}
78+
79+
widget.onApplied(DateTimeRange(start: minTime, end: maxTime));
80+
}
81+
82+
@override
83+
Widget build(BuildContext context) {
84+
return IntrinsicHeight(
85+
child: Column(
86+
children: [
87+
Row(
88+
mainAxisSize: MainAxisSize.min,
89+
children: [
90+
_calendarPicker(),
91+
const SizedBox(width: 28),
92+
_rangeDisplay(),
93+
const SizedBox(width: 28),
94+
],
95+
),
96+
Padding(
97+
padding: const EdgeInsets.only(bottom: 8),
98+
child: TextButton(
99+
onPressed: onApplied,
100+
child: const Text('Apply'),
101+
),
102+
),
103+
],
104+
),
105+
);
106+
}
107+
108+
Widget _rangeDisplay() {
109+
return Column(
110+
mainAxisAlignment: MainAxisAlignment.center,
111+
children: [
112+
const SizedBox(height: 16),
113+
_rangeDisplayItem(RangeEndpointType.start),
114+
const SizedBox(height: 32),
115+
_rangeDisplayItem(RangeEndpointType.end),
116+
],
117+
);
118+
}
119+
120+
Widget _rangeDisplayItem(RangeEndpointType type) {
121+
return Column(
122+
mainAxisAlignment: MainAxisAlignment.center,
123+
crossAxisAlignment: CrossAxisAlignment.start,
124+
children: [
125+
Text(
126+
type == RangeEndpointType.start ? 'From' : 'To',
127+
style: const TextStyle(
128+
fontSize: 12,
129+
fontWeight: FontWeight.bold,
130+
color: Colors.grey,
131+
),
132+
),
133+
const SizedBox(height: 4),
134+
_dateDisplay(type),
135+
const SizedBox(height: 4),
136+
_timePicker(type),
137+
],
138+
);
139+
}
140+
141+
Widget _dateDisplay(RangeEndpointType type) {
142+
return Text(
143+
type == RangeEndpointType.start
144+
? DateFormat.yMMMd('en_US').format(minTime)
145+
: DateFormat.yMMMd('en_US').format(maxTime),
146+
style: const TextStyle(fontSize: 16),
147+
);
148+
}
149+
150+
Widget _calendarPicker() {
151+
return SizedBox(
152+
width: 300,
153+
height: 300,
154+
child: RangeDatePicker(
155+
centerLeadingDate: true,
156+
minDate: minTime,
157+
maxDate: maxTime,
158+
onRangeSelected: onDatesSelected,
159+
splashColor: Colors.transparent,
160+
daysOfTheWeekTextStyle: TextStyle(
161+
fontSize: 12,
162+
fontWeight: FontWeight.bold,
163+
color: Colors.grey.shade400,
164+
),
165+
enabledCellsTextStyle: const CellTextStyle(),
166+
disabledCellsTextStyle: const CellTextStyle(color: Colors.grey),
167+
selectedCellsTextStyle: const CellTextStyle(),
168+
singleSelectedCellTextStyle: const CellTextStyle(color: Colors.white),
169+
currentDateTextStyle: const CellTextStyle(),
170+
),
171+
);
172+
}
173+
174+
Widget _timePicker(RangeEndpointType type) {
175+
final TimeOfDay selectedTime = type == RangeEndpointType.start
176+
? TimeOfDay(hour: minTime.hour, minute: minTime.minute)
177+
: TimeOfDay(hour: maxTime.hour, minute: maxTime.minute);
178+
179+
return SizedBox(
180+
height: 100,
181+
child: MyWheelPicker(
182+
selectedTime: selectedTime,
183+
onTimeSelected: (time) => onTimeSelected(time, type),
184+
),
185+
);
186+
}
187+
}
188+
189+
class CellTextStyle extends TextStyle {
190+
const CellTextStyle({super.color, fontSize = 16});
191+
}

0 commit comments

Comments
 (0)