Skip to content

Commit 4cf1afe

Browse files
authored
Refactor home card UI (#512)
* fix: package * fix: all new page card ui * init: init
1 parent 0dd8b12 commit 4cf1afe

12 files changed

Lines changed: 514 additions & 82 deletions

File tree

lib/app/new_home/_widgets/assistant_hint.dart

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,35 @@ class AssistantHint extends StatelessWidget {
2727
builder: (context, data, _) {
2828
final text = data != null ? _buildHintText(data.$1, data.$2) : '載入天氣資料中…';
2929

30-
return Padding(
31-
padding: const .symmetric(horizontal: 12, vertical: 8),
32-
child: Card(
33-
child: Padding(
34-
padding: const .all(12),
35-
child: Row(
36-
spacing: 8,
37-
crossAxisAlignment: .start,
38-
children: [
39-
Icon(
40-
Symbols.auto_awesome_rounded,
41-
fill: 1,
42-
color: context.colors.onSurfaceVariant,
43-
),
44-
Expanded(
45-
child: Text(
46-
text,
47-
style: TextStyle(fontSize: 16, color: context.colors.onSurfaceVariant),
48-
maxLines: 2,
49-
overflow: .ellipsis,
50-
),
30+
return Container(
31+
margin: const .symmetric(horizontal: 16, vertical: 8),
32+
decoration: BoxDecoration(
33+
color: context.colors.surfaceContainerLow,
34+
borderRadius: .circular(16),
35+
border: Border.all(
36+
color: context.colors.outlineVariant.withValues(alpha: 0.5),
37+
),
38+
),
39+
child: Padding(
40+
padding: const .all(12),
41+
child: Row(
42+
spacing: 8,
43+
crossAxisAlignment: .start,
44+
children: [
45+
Icon(
46+
Symbols.auto_awesome_rounded,
47+
fill: 1,
48+
color: context.colors.onSurfaceVariant,
49+
),
50+
Expanded(
51+
child: Text(
52+
text,
53+
style: TextStyle(fontSize: 16, color: context.colors.onSurfaceVariant),
54+
maxLines: 2,
55+
overflow: .ellipsis,
5156
),
52-
],
53-
),
57+
),
58+
],
5459
),
5560
),
5661
);

lib/app/new_home/_widgets/day_cycle.dart

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,39 @@ class DayCycle extends StatelessWidget {
1818

1919
@override
2020
Widget build(BuildContext context) {
21-
return Padding(
22-
padding: const .symmetric(horizontal: 12, vertical: 4),
23-
child: Card(
24-
child: Padding(
25-
padding: const .all(16),
26-
child: Column(
27-
crossAxisAlignment: .start,
28-
spacing: 8,
29-
children: [
30-
Row(
31-
spacing: 4,
32-
children: [
33-
const Icon(
34-
Symbols.wb_twilight_rounded,
35-
fill: 1,
36-
color: Colors.orangeAccent,
37-
),
38-
BodyText.large('日出日落', weight: .bold),
39-
],
40-
),
41-
const SizedBox(height: 16),
42-
_SunCycleGraph(
43-
sunrise: const TimeOfDay(hour: 5, minute: 30),
44-
sunset: const TimeOfDay(hour: 18, minute: 30),
45-
now: TimeOfDay.now(),
46-
),
47-
],
48-
),
21+
return Container(
22+
margin: const .symmetric(horizontal: 16, vertical: 8),
23+
decoration: BoxDecoration(
24+
color: context.colors.surfaceContainerLow,
25+
borderRadius: .circular(16),
26+
border: Border.all(
27+
color: context.colors.outlineVariant.withValues(alpha: 0.5),
28+
),
29+
),
30+
child: Padding(
31+
padding: const .all(16),
32+
child: Column(
33+
crossAxisAlignment: .start,
34+
spacing: 8,
35+
children: [
36+
Row(
37+
spacing: 4,
38+
children: [
39+
const Icon(
40+
Symbols.wb_twilight_rounded,
41+
fill: 1,
42+
color: Colors.orangeAccent,
43+
),
44+
BodyText.large('日出日落', weight: .bold),
45+
],
46+
),
47+
const SizedBox(height: 16),
48+
_SunCycleGraph(
49+
sunrise: const TimeOfDay(hour: 5, minute: 30),
50+
sunset: const TimeOfDay(hour: 18, minute: 30),
51+
now: TimeOfDay.now(),
52+
),
53+
],
4954
),
5055
),
5156
);
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/// 24 小時預報。
2+
library;
3+
4+
import 'package:dpip/app/new_home/_models/home_model.dart';
5+
import 'package:dpip/core/i18n.dart';
6+
import 'package:dpip/utils/extensions/build_context.dart';
7+
import 'package:dpip/utils/log.dart';
8+
import 'package:dpip/widgets/responsive/responsive_container.dart';
9+
import 'package:flutter/material.dart';
10+
import 'package:material_symbols_icons/material_symbols_icons.dart';
11+
import 'package:provider/provider.dart';
12+
13+
const double _kColumnWidth = 56.0;
14+
15+
class Forecast extends StatelessWidget {
16+
const Forecast({super.key});
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return Selector<HomeModel, Map<String, dynamic>?>(
21+
selector: (_, m) => m.forecast,
22+
builder: (context, forecast, _) {
23+
if (forecast == null) return const SizedBox.shrink();
24+
try {
25+
final data = forecast['forecast'] as List<dynamic>?;
26+
if (data == null || data.isEmpty) return const SizedBox.shrink();
27+
28+
return ResponsiveContainer(
29+
child: Container(
30+
margin: const .symmetric(horizontal: 16, vertical: 8),
31+
decoration: BoxDecoration(
32+
color: context.colors.surfaceContainerLow,
33+
borderRadius: .circular(16),
34+
border: Border.all(
35+
color: context.colors.outlineVariant.withValues(alpha: 0.5),
36+
),
37+
),
38+
child: Padding(
39+
padding: const .symmetric(vertical: 14),
40+
child: Column(
41+
crossAxisAlignment: .start,
42+
children: [
43+
Padding(
44+
padding: const .symmetric(horizontal: 14),
45+
child: Text(
46+
'24 小時預報'.i18n,
47+
style: context.texts.labelMedium?.copyWith(
48+
color: context.colors.onSurfaceVariant,
49+
fontWeight: .w600,
50+
letterSpacing: 0.5,
51+
),
52+
),
53+
),
54+
const SizedBox(height: 12),
55+
SingleChildScrollView(
56+
scrollDirection: .horizontal,
57+
padding: const .symmetric(horizontal: 6),
58+
child: Row(
59+
crossAxisAlignment: .start,
60+
children: [
61+
for (var i = 0; i < data.length; i++)
62+
_HourColumn(
63+
item: data[i] as Map<String, dynamic>,
64+
isNow: i == 0,
65+
),
66+
],
67+
),
68+
),
69+
],
70+
),
71+
),
72+
),
73+
);
74+
} catch (e, s) {
75+
TalkerManager.instance.error('Failed to render forecast card', e, s);
76+
}
77+
return const SizedBox.shrink();
78+
},
79+
);
80+
}
81+
}
82+
83+
class _HourColumn extends StatelessWidget {
84+
final Map<String, dynamic> item;
85+
final bool isNow;
86+
87+
const _HourColumn({required this.item, required this.isNow});
88+
89+
static (IconData, Color?) _weatherIcon(BuildContext context, String weather) => switch (weather) {
90+
final s when s.contains('晴') => (Symbols.sunny_rounded, Colors.orangeAccent),
91+
final s when s.contains('雨') => (Symbols.rainy_light_rounded, Colors.blueAccent),
92+
final s when s.contains('雲') || s.contains('陰') => (
93+
Symbols.cloud_rounded,
94+
context.colors.onSurfaceVariant,
95+
),
96+
final s when s.contains('雷') => (Symbols.flash_on_rounded, Colors.amber),
97+
final s when s.contains('雪') => (Symbols.snowflake_rounded, Colors.lightBlue[200]),
98+
_ => (Symbols.wb_cloudy_rounded, context.colors.onSurfaceVariant),
99+
};
100+
101+
@override
102+
Widget build(BuildContext context) {
103+
final time = item['time'] as String? ?? '';
104+
final weather = item['weather'] as String? ?? '';
105+
final temp = (item['temperature'] as num?)?.toDouble() ?? 0.0;
106+
final pop = item['pop'] as int? ?? 0;
107+
108+
final (icon, color) = _weatherIcon(context, weather);
109+
110+
final primaryColor = isNow
111+
? context.colors.onSurface
112+
: context.colors.onSurface.withValues(alpha: 0.82);
113+
final labelColor = isNow
114+
? context.colors.onSurface
115+
: context.colors.onSurfaceVariant.withValues(alpha: 0.75);
116+
117+
return SizedBox(
118+
width: _kColumnWidth,
119+
child: Column(
120+
mainAxisSize: .min,
121+
children: [
122+
Text(
123+
isNow ? '現在'.i18n : time,
124+
style: context.texts.labelSmall?.copyWith(
125+
color: labelColor,
126+
fontWeight: isNow ? .w700 : .w500,
127+
height: 1,
128+
),
129+
),
130+
const SizedBox(height: 10),
131+
Icon(icon, color: color, fill: 1, size: 24),
132+
if (pop > 0) ...[
133+
const SizedBox(height: 6),
134+
Row(
135+
mainAxisSize: .min,
136+
mainAxisAlignment: .center,
137+
children: [
138+
Icon(
139+
Symbols.water_drop_rounded,
140+
size: 10,
141+
color: Colors.blueAccent.withValues(alpha: 0.85),
142+
),
143+
const SizedBox(width: 2),
144+
Text(
145+
'$pop%',
146+
style: TextStyle(
147+
fontSize: 10,
148+
color: Colors.blueAccent.withValues(alpha: 0.85),
149+
fontWeight: .w600,
150+
height: 1,
151+
),
152+
),
153+
],
154+
),
155+
],
156+
const SizedBox(height: 10),
157+
Text(
158+
'${temp.round()}°',
159+
style: context.texts.titleMedium?.copyWith(
160+
fontWeight: isNow ? .w700 : .w600,
161+
color: primaryColor,
162+
height: 1,
163+
),
164+
),
165+
],
166+
),
167+
);
168+
}
169+
}

lib/app/new_home/_widgets/radar.dart

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class Radar extends StatefulWidget {
3434

3535
class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
3636
MapLibreMapController? _mapController;
37-
bool _homeListenerAdded = false;
37+
HomeModel? _homeModel;
3838

3939
/// Resolves to the list of available radar timestamps once fetched.
4040
late Future<List<String>> _radarListFuture;
@@ -109,9 +109,11 @@ class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
109109
final route = ModalRoute.of(context);
110110
if (route != null) routeObserver.subscribe(this, route);
111111

112-
if (!_homeListenerAdded) {
113-
context.home.addListener(_onHomeModelChanged);
114-
_homeListenerAdded = true;
112+
final model = context.home;
113+
if (_homeModel != model) {
114+
_homeModel?.removeListener(_onHomeModelChanged);
115+
_homeModel = model;
116+
model.addListener(_onHomeModelChanged);
115117
}
116118
}
117119

@@ -121,14 +123,22 @@ class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
121123
final targetLocation = userLocation ?? DpipMap.kTaiwanCenter;
122124
final targetZoom = userLocation != null ? DpipMap.kUserLocationZoom : DpipMap.kTaiwanZoom;
123125

124-
return Padding(
125-
padding: .symmetric(horizontal: 12, vertical: 4),
126-
child: Card(
127-
clipBehavior: .antiAlias,
126+
return Container(
127+
margin: const .symmetric(horizontal: 16, vertical: 8),
128+
decoration: BoxDecoration(
129+
color: context.colors.surfaceContainerLow,
130+
borderRadius: .circular(16),
131+
border: Border.all(
132+
color: context.colors.outlineVariant.withValues(alpha: 0.5),
133+
),
134+
),
135+
clipBehavior: .antiAlias,
136+
child: Material(
137+
color: Colors.transparent,
128138
child: InkWell(
129-
onTap: () => MapRoute(layers: 'radar').push(context),
139+
onTap: () => const MapRoute(layers: 'radar').push(context),
130140
child: Padding(
131-
padding: .all(12),
141+
padding: const .all(12),
132142
child: Column(
133143
crossAxisAlignment: .start,
134144
spacing: 12,
@@ -209,7 +219,7 @@ class _RadarState extends State<Radar> with WidgetsBindingObserver, RouteAware {
209219

210220
@override
211221
void dispose() {
212-
context.home.removeListener(_onHomeModelChanged);
222+
_homeModel?.removeListener(_onHomeModelChanged);
213223
routeObserver.unsubscribe(this);
214224
WidgetsBinding.instance.removeObserver(this);
215225
super.dispose();

0 commit comments

Comments
 (0)