Skip to content

Commit f309292

Browse files
committed
fix(home): show the EEW card only in the full-screen dashboard
Fix(zh-Hant): 首頁收合時不重複顯示地震速報卡片,展開後才顯示完整內容 Fix(en-US): the home no longer duplicates the earthquake alert card while collapsed and shows it when expanded
1 parent c8fb437 commit f309292

4 files changed

Lines changed: 167 additions & 54 deletions

File tree

lib/features/home/presentation/widgets/home_content.dart

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,21 @@ class HomeContent extends StatelessWidget {
438438
// thing on the dashboard that matters in the seconds it
439439
// exists. Nothing renders when calm, so the sheet's
440440
// ordinary layout is untouched outside an earthquake.
441-
const HomeEewSection(),
442-
// The section renders nothing when calm — only reserve
443-
// the gap below it while an alert is actually showing,
444-
// or a calm dashboard gains an extra empty lg here on
445-
// top of the one already before this section.
446-
if (HomeEewSection.isActive(context))
447-
const SizedBox(height: AppSpacing.lg),
441+
//
442+
// Full-screen only: while the sheet rests half-open,
443+
// `HomeMonitorBanner` is still on screen carrying the same
444+
// alert (it only slides away as the sheet rises), and two
445+
// copies of one warning in one view is one too many. The
446+
// card takes over exactly where the banner leaves off.
447+
if (expanded) ...[
448+
const HomeEewSection(),
449+
// The section renders nothing when calm — only reserve
450+
// the gap below it while an alert is actually showing,
451+
// or a calm dashboard gains an extra empty lg here on
452+
// top of the one already before this section.
453+
if (HomeEewSection.isActive(context))
454+
const SizedBox(height: AppSpacing.lg),
455+
],
448456
// Collapsed, or nothing to anchor a hero to: active events
449457
// only. Full-screen township: the hero above, then forecast
450458
// → events reached by scrolling past it. 全國: events only

lib/features/home/presentation/widgets/home_eew_section.dart

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -80,43 +80,19 @@ class _EewSectionBody extends StatelessWidget {
8080

8181
@override
8282
Widget build(BuildContext context) {
83-
final theme = Theme.of(context);
84-
final colors = theme.colorScheme;
85-
final l10n = AppLocalizations.of(context);
86-
return Column(
87-
crossAxisAlignment: CrossAxisAlignment.stretch,
88-
children: [
89-
Row(
90-
children: [
91-
Icon(Icons.warning_amber_outlined, size: 18, color: colors.error),
92-
const SizedBox(width: AppSpacing.sm),
93-
Expanded(
94-
child: Text(
95-
l10n.eewTitle,
96-
style: theme.textTheme.titleSmall?.copyWith(
97-
color: colors.onSurface,
98-
fontWeight: FontWeight.w700,
99-
),
100-
),
101-
),
102-
Text(
103-
l10n.eewSerial(alert.serial),
104-
style: theme.textTheme.labelSmall?.copyWith(
105-
color: colors.onSurfaceVariant,
106-
),
107-
),
108-
],
109-
),
110-
const SizedBox(height: AppSpacing.sm),
111-
Material(
112-
color: Colors.transparent,
113-
child: InkWell(
114-
borderRadius: BorderRadius.circular(12),
115-
onTap: () => context.pushNamed(AppRoutes.eew),
116-
child: _EewAlertCard(alert: alert),
117-
),
118-
),
119-
],
83+
// No section header outside the card: the space between the sheet's cards
84+
// is the scroll-dimmed weather sky (`_ScrollBlurredWeather`), not a
85+
// surface, and it is dimmed precisely so the solid plates carry the
86+
// reading. Bare theme-ink text there is unreadable in the light theme —
87+
// near-black `onSurface` on a 45 %-dimmed sky. The title lives inside the
88+
// card instead, exactly like every sibling section's.
89+
return Material(
90+
color: Colors.transparent,
91+
child: InkWell(
92+
borderRadius: BorderRadius.circular(12),
93+
onTap: () => context.pushNamed(AppRoutes.eew),
94+
child: _EewAlertCard(alert: alert),
95+
),
12096
);
12197
}
12298
}
@@ -211,6 +187,32 @@ class _EewAlertCardState extends State<_EewAlertCard> with SecondTicker {
211187
child: Column(
212188
crossAxisAlignment: CrossAxisAlignment.stretch,
213189
children: [
190+
Row(
191+
children: [
192+
Icon(
193+
Icons.warning_amber_outlined,
194+
size: 18,
195+
color: colors.error,
196+
),
197+
const SizedBox(width: AppSpacing.sm),
198+
Expanded(
199+
child: Text(
200+
l10n.eewTitle,
201+
style: theme.textTheme.titleSmall?.copyWith(
202+
color: colors.onSurface,
203+
fontWeight: FontWeight.w700,
204+
),
205+
),
206+
),
207+
Text(
208+
l10n.eewSerial(widget.alert.serial),
209+
style: theme.textTheme.labelSmall?.copyWith(
210+
color: colors.onSurfaceVariant,
211+
),
212+
),
213+
],
214+
),
215+
const SizedBox(height: AppSpacing.sm),
214216
Row(
215217
children: [
216218
Expanded(

test/features/home/presentation/widgets/home_content_test.dart

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:dpip/core/error/result.dart';
2+
import 'package:dpip/core/geo/location_service.dart';
3+
import 'package:dpip/core/geo/location_status.dart';
24
import 'package:dpip/core/geo/town.dart';
35
import 'package:dpip/core/geo/town_directory.dart';
46
import 'package:dpip/core/realtime/clock.dart';
@@ -11,12 +13,14 @@ import 'package:dpip/core/realtime/ticker.dart';
1113
import 'package:dpip/core/settings/settings_store.dart';
1214
import 'package:dpip/core/settings/region_store.dart';
1315
import 'package:dpip/features/earthquake/domain/eew.dart';
16+
import 'package:dpip/features/earthquake/domain/seismic_travel_time.dart';
1417
import 'package:dpip/features/events/domain/event.dart';
1518
import 'package:dpip/features/events/domain/event_repository.dart';
1619
import 'package:dpip/features/home/presentation/home_active_events_controller.dart';
1720
import 'package:dpip/features/home/presentation/home_weather_controller.dart';
1821
import 'package:dpip/features/home/presentation/widgets/home_active_events_section.dart';
1922
import 'package:dpip/features/home/presentation/widgets/home_content.dart';
23+
import 'package:dpip/features/home/presentation/widgets/home_eew_section.dart';
2024
import 'package:dpip/features/home/presentation/widgets/home_forecast_section.dart';
2125
import 'package:dpip/features/home/presentation/widgets/home_rain_trend_section.dart';
2226
import 'package:dpip/features/home/presentation/widgets/home_sheet_header.dart';
@@ -114,11 +118,43 @@ class _StaticEewSource extends RealtimeSource<List<Eew>> {
114118
bool sameData(List<Eew>? a, List<Eew>? b) => listEquals(a, b);
115119
}
116120

121+
/// A refreshed EEW channel carrying one live alert, ready to hand to [_wrap].
122+
Future<RealtimeNotifier<List<Eew>>> _liveEew() async {
123+
final channel = RealtimeChannel<List<Eew>>(
124+
source: _StaticEewSource([
125+
Eew(
126+
agency: 'CWA',
127+
id: 'test',
128+
serial: 34,
129+
status: 0,
130+
isFinal: false,
131+
info: const EewInfo(
132+
time: 1786362600000,
133+
longitude: 121.5,
134+
latitude: 23.5,
135+
depth: 10,
136+
magnitude: 7.5,
137+
location: '臺東縣',
138+
max: 6,
139+
),
140+
),
141+
]),
142+
clock: _FakeClock(),
143+
elapsed: _FakeElapsed(),
144+
ticker: _FakeTicker(),
145+
config: RealtimeConfig.eew,
146+
label: 'test-eew',
147+
);
148+
await channel.refreshNow();
149+
return RealtimeNotifier<List<Eew>>(channel);
150+
}
151+
117152
/// Pumps [HomeContent] with everything it reads: a [RegionStore] to switch on
118153
/// and localizations for the body.
119154
Widget _wrap(
120155
RegionStore store, {
121156
bool expanded = false,
157+
RealtimeNotifier<List<Eew>>? eew,
122158
double topInset = 0,
123159
double textScale = 1,
124160
ScrollController? controller,
@@ -147,18 +183,36 @@ Widget _wrap(
147183
ChangeNotifierProvider<HomeActiveEventsController>(
148184
create: (_) => HomeActiveEventsController(events, store),
149185
),
150-
ChangeNotifierProvider<RealtimeNotifier<List<Eew>>>(
151-
create: (_) => RealtimeNotifier<List<Eew>>(
152-
RealtimeChannel<List<Eew>>(
153-
source: _StaticEewSource(const []),
154-
clock: _FakeClock(),
155-
elapsed: _FakeElapsed(),
156-
ticker: _FakeTicker(),
157-
config: RealtimeConfig.eew,
158-
label: 'test-eew',
159-
),
186+
// Read by the EEW alert card for its 所在地預估 countdown.
187+
Provider<Future<SeismicTravelTimeTable>>.value(
188+
value: Future<SeismicTravelTimeTable>.value(
189+
const SeismicTravelTimeTable({}),
160190
),
161191
),
192+
Provider<LocationService>.value(
193+
value: LocationService(
194+
directory,
195+
isAvailable: () async => false,
196+
fix: () async => null,
197+
lastKnown: () async => null,
198+
status: () async => LocationStatus.denied,
199+
),
200+
),
201+
if (eew != null)
202+
ChangeNotifierProvider<RealtimeNotifier<List<Eew>>>.value(value: eew)
203+
else
204+
ChangeNotifierProvider<RealtimeNotifier<List<Eew>>>(
205+
create: (_) => RealtimeNotifier<List<Eew>>(
206+
RealtimeChannel<List<Eew>>(
207+
source: _StaticEewSource(const []),
208+
clock: _FakeClock(),
209+
elapsed: _FakeElapsed(),
210+
ticker: _FakeTicker(),
211+
config: RealtimeConfig.eew,
212+
label: 'test-eew',
213+
),
214+
),
215+
),
162216
],
163217
child: Builder(
164218
builder: (context) => MediaQuery(
@@ -364,6 +418,33 @@ void main() {
364418
expect(find.byType(HomeActiveEventsSection), findsOneWidget);
365419
});
366420

421+
testWidgets('the EEW card is full-screen only, never at rest', (
422+
tester,
423+
) async {
424+
final store = await _store();
425+
store
426+
..select(1)
427+
..setCurrentCode('100');
428+
429+
// At rest `HomeMonitorBanner` is still on screen with the same alert, so
430+
// the in-sheet card would be a second copy of one warning.
431+
await tester.pumpWidget(_wrap(store, eew: await _liveEew()));
432+
await tester.pumpAndSettle();
433+
expect(find.byType(HomeEewSection), findsNothing);
434+
expect(find.text('臺東縣'), findsNothing);
435+
436+
// Full-screen the banner has slid away, so the card carries the alert.
437+
await tester.pumpWidget(
438+
_wrap(store, expanded: true, eew: await _liveEew()),
439+
);
440+
await tester.pumpAndSettle();
441+
expect(find.byType(HomeEewSection), findsOneWidget);
442+
expect(find.text('臺東縣'), findsOneWidget);
443+
444+
// Tear down so the card's countdown timer is cancelled.
445+
await tester.pumpWidget(const SizedBox());
446+
});
447+
367448
testWidgets('a dry hour hides the rain trend and raises a compact forecast', (
368449
tester,
369450
) async {

test/features/home/presentation/widgets/home_eew_section_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Widget _wrap(RealtimeNotifier<List<Eew>> notifier, RegionStore store) =>
106106
localizationsDelegates: AppLocalizations.localizationsDelegates,
107107
supportedLocales: AppLocalizations.supportedLocales,
108108
locale: const Locale('en'),
109+
theme: ThemeData(brightness: Brightness.light),
109110
home: MultiProvider(
110111
providers: [
111112
ChangeNotifierProvider<RealtimeNotifier<List<Eew>>>.value(
@@ -155,6 +156,27 @@ void main() {
155156
await tester.pumpWidget(const SizedBox());
156157
});
157158

159+
testWidgets('title and serial sit inside the card, not on the backdrop', (
160+
tester,
161+
) async {
162+
final setup = await _liveNotifier([_alert()]);
163+
final store = await _store();
164+
await tester.pumpWidget(_wrap(setup.notifier, store));
165+
166+
// The gap between the home sheet's cards is the scroll-dimmed weather sky,
167+
// not a surface — theme on-surface ink is unreadable there in the light
168+
// theme. Both header texts must be on the card's opaque plate.
169+
for (final label in ['Earthquake early warning', 'Report 2']) {
170+
expect(
171+
find.ancestor(of: find.text(label), matching: find.byType(Card)),
172+
findsOneWidget,
173+
reason: '"$label" must render inside the alert card',
174+
);
175+
}
176+
177+
await tester.pumpWidget(const SizedBox());
178+
});
179+
158180
testWidgets('renders nothing when the feed is calm (no alerts)', (
159181
tester,
160182
) async {

0 commit comments

Comments
 (0)