Skip to content

Commit 32ba5bc

Browse files
began refactoring ui
1 parent af7fb6b commit 32ba5bc

22 files changed

+2907
-711
lines changed
Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import 'dart:async';
22
import 'dart:math';
33
import 'package:flutter/material.dart';
4-
import 'package:community_charts_flutter/community_charts_flutter.dart' as charts;
4+
import 'package:community_charts_flutter/community_charts_flutter.dart'
5+
as charts;
56

67
class RollingChart extends StatefulWidget {
78
final Stream<(int, double)> dataSteam;
89
final int timestampExponent; // e.g., 6 for microseconds to milliseconds
9-
final int timeWindow; // in milliseconds
10+
final int timeWindow; // in seconds
1011

1112
const RollingChart({
1213
super.key,
@@ -20,8 +21,9 @@ class RollingChart extends StatefulWidget {
2021
}
2122

2223
class _RollingChartState extends State<RollingChart> {
23-
List<charts.Series<_ChartPoint, int>> _seriesList = [];
24-
final List<_ChartPoint> _data = [];
24+
List<charts.Series<_ChartPoint, num>> _seriesList = [];
25+
final List<_RawChartPoint> _rawData = [];
26+
List<_ChartPoint> _normalizedData = [];
2527
StreamSubscription? _subscription;
2628

2729
@override
@@ -42,56 +44,87 @@ class _RollingChartState extends State<RollingChart> {
4244
void _listenToStream() {
4345
_subscription = widget.dataSteam.listen((event) {
4446
final (timestamp, value) = event;
45-
47+
4648
setState(() {
47-
_data.add(_ChartPoint(timestamp, value));
48-
49+
_rawData.add(_RawChartPoint(timestamp, value));
50+
4951
// Remove old data outside time window
50-
int cutoffTime = timestamp - (widget.timeWindow * pow(10, -widget.timestampExponent) as int);
51-
_data.removeWhere((data) => data.time < cutoffTime);
52-
52+
final ticksPerSecond = pow(10, -widget.timestampExponent).toDouble();
53+
final cutoffTime =
54+
timestamp - (widget.timeWindow * ticksPerSecond).round();
55+
_rawData.removeWhere((data) => data.timestamp < cutoffTime);
56+
5357
_updateSeries();
5458
});
5559
});
5660
}
5761

5862
void _updateSeries() {
63+
if (_rawData.isEmpty) {
64+
_normalizedData = [];
65+
_seriesList = [];
66+
return;
67+
}
68+
69+
final firstTimestamp = _rawData.first.timestamp;
70+
final secondsPerTick = pow(10, widget.timestampExponent).toDouble();
71+
72+
_normalizedData = _rawData
73+
.map(
74+
(point) => _ChartPoint(
75+
(point.timestamp - firstTimestamp) * secondsPerTick,
76+
point.value,
77+
),
78+
)
79+
.toList(growable: false);
80+
5981
_seriesList = [
60-
charts.Series<_ChartPoint, int>(
61-
id: 'Live Data',
62-
colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault,
63-
domainFn: (_ChartPoint point, _) => point.time,
64-
measureFn: (_ChartPoint point, _) => point.value,
65-
data: List.of(_data),
82+
charts.Series<_ChartPoint, num>(
83+
id: 'Live Data',
84+
colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault,
85+
domainFn: (_ChartPoint point, _) => point.timeSeconds,
86+
measureFn: (_ChartPoint point, _) => point.value,
87+
data: _normalizedData,
6688
),
6789
];
6890
}
6991

7092
@override
7193
Widget build(BuildContext context) {
72-
final filteredPoints = _data;
94+
final filteredPoints = _normalizedData;
7395

74-
final xValues = filteredPoints.map((e) => e.time).toList();
96+
final xValues = filteredPoints.map((e) => e.timeSeconds).toList();
7597
final yValues = filteredPoints.map((e) => e.value).toList();
7698

77-
final int? xMin = xValues.isNotEmpty ? xValues.reduce((a, b) => a < b ? a : b) : null;
78-
final int? xMax = xValues.isNotEmpty ? xValues.reduce((a, b) => a > b ? a : b) : null;
99+
final double xMin = 0;
100+
final double xMax = max(
101+
widget.timeWindow.toDouble(),
102+
xValues.isNotEmpty ? xValues.reduce((a, b) => a > b ? a : b) : 0,
103+
);
79104

80-
final double? yMin = yValues.isNotEmpty ? yValues.reduce((a, b) => a < b ? a : b) : null;
81-
final double? yMax = yValues.isNotEmpty ? yValues.reduce((a, b) => a > b ? a : b) : null;
105+
final double? yMin =
106+
yValues.isNotEmpty ? yValues.reduce((a, b) => a < b ? a : b) : null;
107+
final double? yMax =
108+
yValues.isNotEmpty ? yValues.reduce((a, b) => a > b ? a : b) : null;
82109

83110
return charts.LineChart(
84111
_seriesList,
85112
animate: false,
86113
domainAxis: charts.NumericAxisSpec(
87-
viewport: xMin != null && xMax != null
88-
? charts.NumericExtents(xMin, xMax)
89-
: null,
114+
viewport: charts.NumericExtents(xMin, xMax),
115+
tickFormatterSpec: charts.BasicNumericTickFormatterSpec((num? value) {
116+
if (value == null) return '';
117+
final rounded = value.roundToDouble();
118+
if ((value - rounded).abs() < 0.05) {
119+
return '${rounded.toInt()}s';
120+
}
121+
return '${value.toStringAsFixed(1)}s';
122+
}),
90123
),
91124
primaryMeasureAxis: charts.NumericAxisSpec(
92125
viewport: yMin != null && yMax != null
93-
? charts.NumericExtents(yMin, yMax)
94-
: null,
126+
? charts.NumericExtents(yMin, yMax)
127+
: null,
95128
),
96129
);
97130
}
@@ -103,9 +136,16 @@ class _RollingChartState extends State<RollingChart> {
103136
}
104137
}
105138

139+
class _RawChartPoint {
140+
final int timestamp;
141+
final double value;
142+
143+
_RawChartPoint(this.timestamp, this.value);
144+
}
145+
106146
class _ChartPoint {
107-
final int time;
147+
final double timeSeconds;
108148
final double value;
109149

110-
_ChartPoint(this.time, this.value);
150+
_ChartPoint(this.timeSeconds, this.value);
111151
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
bool wearableNameStartsWithPrefix(String wearableName, String prefix) {
2+
final normalizedWearableName = wearableName.trim().toLowerCase();
3+
final normalizedPrefix = prefix.trim().toLowerCase();
4+
if (normalizedWearableName.isEmpty || normalizedPrefix.isEmpty) return false;
5+
return normalizedWearableName.startsWith(normalizedPrefix);
6+
}
7+
8+
bool wearableIsCompatibleWithApp({
9+
required String wearableName,
10+
required List<String> supportedDevicePrefixes,
11+
}) {
12+
if (supportedDevicePrefixes.isEmpty) return true;
13+
return supportedDevicePrefixes.any(
14+
(prefix) => wearableNameStartsWithPrefix(wearableName, prefix),
15+
);
16+
}
17+
18+
bool hasConnectedWearableForPrefix({
19+
required String devicePrefix,
20+
required Iterable<String> connectedWearableNames,
21+
}) {
22+
return connectedWearableNames.any(
23+
(name) => wearableNameStartsWithPrefix(name, devicePrefix),
24+
);
25+
}
Lines changed: 176 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
3+
import 'package:open_wearable/apps/widgets/app_compatibility.dart';
34
import 'package:open_wearable/apps/widgets/apps_page.dart';
5+
import 'package:open_wearable/view_models/wearables_provider.dart';
6+
import 'package:provider/provider.dart';
47

58
class AppTile extends StatelessWidget {
69
final AppInfo app;
@@ -9,26 +12,183 @@ class AppTile extends StatelessWidget {
912

1013
@override
1114
Widget build(BuildContext context) {
12-
return PlatformListTile(
13-
title: PlatformText(app.title),
14-
subtitle: PlatformText(app.description),
15-
leading: SizedBox(
16-
height: 50.0,
17-
width: 50.0,
18-
child: ClipRRect(
19-
borderRadius: BorderRadius.circular(8.0),
20-
child: Image.asset(
21-
app.logoPath,
22-
fit: BoxFit.cover,
23-
),
24-
),
25-
),
15+
final connectedWearableNames = context
16+
.watch<WearablesProvider>()
17+
.wearables
18+
.map((wearable) => wearable.name)
19+
.toList(growable: false);
20+
final titleStyle = Theme.of(context).textTheme.titleMedium?.copyWith(
21+
fontWeight: FontWeight.w700,
22+
);
23+
24+
return Card(
25+
margin: const EdgeInsets.only(bottom: 10),
26+
clipBehavior: Clip.antiAlias,
27+
child: InkWell(
2628
onTap: () {
2729
Navigator.push(
2830
context,
29-
platformPageRoute(context: context, builder: (context) => app.widget),
31+
platformPageRoute(
32+
context: context,
33+
builder: (context) => app.widget,
34+
),
3035
);
3136
},
32-
);
37+
child: Padding(
38+
padding: const EdgeInsets.all(12),
39+
child: Row(
40+
children: [
41+
Container(
42+
height: 62.0,
43+
width: 62.0,
44+
decoration: BoxDecoration(
45+
borderRadius: BorderRadius.circular(14),
46+
border: Border.all(
47+
color: app.accentColor.withValues(alpha: 0.28),
48+
),
49+
),
50+
child: ClipRRect(
51+
borderRadius: BorderRadius.circular(13.0),
52+
child: Image.asset(
53+
app.logoPath,
54+
fit: BoxFit.cover,
55+
),
56+
),
57+
),
58+
const SizedBox(width: 12),
59+
Expanded(
60+
child: Column(
61+
crossAxisAlignment: CrossAxisAlignment.start,
62+
children: [
63+
Row(
64+
children: [
65+
Expanded(
66+
child: Text(
67+
app.title,
68+
style: titleStyle,
69+
maxLines: 1,
70+
overflow: TextOverflow.ellipsis,
71+
),
72+
),
73+
_LaunchAffordance(accentColor: app.accentColor),
74+
],
75+
),
76+
const SizedBox(height: 3),
77+
Text(
78+
app.description,
79+
maxLines: 2,
80+
overflow: TextOverflow.ellipsis,
81+
style: Theme.of(context).textTheme.bodyMedium,
82+
),
83+
const SizedBox(height: 8),
84+
Text(
85+
'Supported devices',
86+
style: Theme.of(context).textTheme.labelSmall?.copyWith(
87+
color: Theme.of(context)
88+
.textTheme
89+
.bodySmall
90+
?.color
91+
?.withValues(alpha: 0.72),
92+
fontWeight: FontWeight.w700,
93+
),
94+
),
95+
const SizedBox(height: 6),
96+
Wrap(
97+
spacing: 6,
98+
runSpacing: 6,
99+
children: app.supportedDevices
100+
.map(
101+
(device) => _SupportedDeviceChip(
102+
text: device,
103+
accentColor: app.accentColor,
104+
isConnected: hasConnectedWearableForPrefix(
105+
devicePrefix: device,
106+
connectedWearableNames: connectedWearableNames,
107+
),
108+
),
109+
)
110+
.toList(),
111+
),
112+
],
113+
),
114+
),
115+
],
116+
),
117+
),
118+
),
119+
);
120+
}
121+
}
122+
123+
class _LaunchAffordance extends StatelessWidget {
124+
final Color accentColor;
125+
126+
const _LaunchAffordance({
127+
required this.accentColor,
128+
});
129+
130+
@override
131+
Widget build(BuildContext context) {
132+
return Container(
133+
margin: const EdgeInsets.only(left: 8),
134+
height: 30,
135+
width: 30,
136+
decoration: BoxDecoration(
137+
color: accentColor.withValues(alpha: 0.12),
138+
borderRadius: BorderRadius.circular(999),
139+
),
140+
child: Icon(
141+
Icons.arrow_forward_rounded,
142+
size: 18,
143+
color: accentColor.withValues(alpha: 0.9),
144+
),
145+
);
146+
}
147+
}
148+
149+
class _SupportedDeviceChip extends StatelessWidget {
150+
final String text;
151+
final Color accentColor;
152+
final bool isConnected;
153+
154+
const _SupportedDeviceChip({
155+
required this.text,
156+
required this.accentColor,
157+
required this.isConnected,
158+
});
159+
160+
@override
161+
Widget build(BuildContext context) {
162+
const connectedDotColor = Color(0xFF2F8F5B);
163+
return Container(
164+
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
165+
decoration: BoxDecoration(
166+
color: accentColor.withValues(alpha: 0.1),
167+
borderRadius: BorderRadius.circular(999),
168+
),
169+
child: Row(
170+
mainAxisSize: MainAxisSize.min,
171+
children: [
172+
Text(
173+
text,
174+
style: Theme.of(context).textTheme.labelSmall?.copyWith(
175+
color: accentColor.withValues(alpha: 0.92),
176+
fontWeight: FontWeight.w600,
177+
),
178+
),
179+
if (isConnected) ...[
180+
const SizedBox(width: 6),
181+
Container(
182+
width: 7,
183+
height: 7,
184+
decoration: const BoxDecoration(
185+
color: connectedDotColor,
186+
shape: BoxShape.circle,
187+
),
188+
),
189+
],
190+
],
191+
),
192+
);
33193
}
34194
}

0 commit comments

Comments
 (0)