From 44855132c54b50f50117cdd37687c72b488ebda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BB=CF=89=E3=83=BB=EF=BC=88=E7=AB=B9=E5=AD=90?= =?UTF-8?q?=EF=BC=89?= Date: Mon, 22 Dec 2025 23:38:35 +0800 Subject: [PATCH 01/13] feat: replay --- lib/app/map/_lib/managers/report.dart | 5 ++-- lib/app/map/page.dart | 31 +++++++++++++++++++--- lib/route/report/report_sheet_content.dart | 5 ++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index e95f118ed..2167ae731 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -14,6 +14,7 @@ import 'package:dpip/api/model/location/location.dart'; import 'package:dpip/api/model/report/earthquake_report.dart'; import 'package:dpip/api/model/report/partial_earthquake_report.dart'; import 'package:dpip/app/map/_lib/manager.dart'; +import 'package:dpip/app/map/page.dart'; import 'package:dpip/app/map/_lib/utils.dart'; import 'package:dpip/core/i18n.dart'; import 'package:dpip/core/providers.dart'; @@ -616,7 +617,7 @@ class _ReportMapLayerSheetState extends State { launchUrl(report.reportUrl); }, ), - /* ActionChip( + ActionChip( avatar: const Icon(Symbols.replay), label: const Text('重播'), onPressed: () { @@ -627,7 +628,7 @@ class _ReportMapLayerSheetState extends State { ), ); }, - ), */ + ), ], ), const Divider(), diff --git a/lib/app/map/page.dart b/lib/app/map/page.dart index 82ba60298..a0b06f83c 100644 --- a/lib/app/map/page.dart +++ b/lib/app/map/page.dart @@ -22,16 +22,19 @@ import 'package:maplibre_gl/maplibre_gl.dart'; class MapPageOptions { final Set? initialLayers; final String? reportId; + final int? replayTimestamp; - MapPageOptions({this.initialLayers, this.reportId}); + MapPageOptions({this.initialLayers, this.reportId, this.replayTimestamp}); factory MapPageOptions.fromQueryParameters(Map queryParameters) { final layers = queryParameters['layers']?.split(','); final report = queryParameters['report']; + final replay = queryParameters['replay'] != null ? int.tryParse(queryParameters['replay']!) : null; return MapPageOptions( initialLayers: layers?.map((layer) => MapLayer.values.byName(layer)).toSet(), reportId: report, + replayTimestamp: replay, ); } } @@ -48,6 +51,7 @@ class MapPage extends StatefulWidget { if (options.initialLayers != null) parameters.add('layers=${options.initialLayers!.map((e) => e.name).join(',')}'); if (options.reportId != null) parameters.add('report=${options.reportId}'); + if (options.replayTimestamp != null) parameters.add('replay=${options.replayTimestamp}'); return "/map?${parameters.join('&')}"; } @@ -63,7 +67,7 @@ class _MapPageState extends State with TickerProviderStateMixin { Timer? _ticker; late BaseMapType _baseMapType = GlobalProviders.map.baseMap; - late Set _activeLayers = widget.options?.initialLayers ?? {}; + late Set _activeLayers = widget.options?.initialLayers ?? (widget.options?.replayTimestamp != null ? {MapLayer.monitor} : {}); Future? _toggleLayerOperation; void _setupTicker() { @@ -226,7 +230,12 @@ class _MapPageState extends State with TickerProviderStateMixin { manager.dispose(); } - _managers[MapLayer.monitor] = MonitorMapLayerManager(context, controller); + _managers[MapLayer.monitor] = MonitorMapLayerManager( + context, + controller, + isReplayMode: widget.options?.replayTimestamp != null, + replayTimestamp: widget.options?.replayTimestamp, + ); _managers[MapLayer.report] = ReportMapLayerManager(context, controller, initialReportId: widget.options?.reportId); _managers[MapLayer.tsunami] = TsunamiMapLayerManager(context, controller); _managers[MapLayer.radar] = RadarMapLayerManager( @@ -287,3 +296,19 @@ class _MapPageState extends State with TickerProviderStateMixin { super.dispose(); } } + +class MapMonitorPage extends StatelessWidget { + final int data; + + const MapMonitorPage({super.key, required this.data}); + + @override + Widget build(BuildContext context) { + return MapPage( + options: MapPageOptions( + initialLayers: {MapLayer.monitor}, + replayTimestamp: data, + ), + ); + } +} diff --git a/lib/route/report/report_sheet_content.dart b/lib/route/report/report_sheet_content.dart index d2475faa8..b75cfb6f9 100644 --- a/lib/route/report/report_sheet_content.dart +++ b/lib/route/report/report_sheet_content.dart @@ -1,4 +1,5 @@ import 'package:dpip/api/model/report/earthquake_report.dart'; +import 'package:dpip/app/map/page.dart'; import 'package:dpip/utils/depth_color.dart'; import 'package:dpip/utils/extensions/build_context.dart'; import 'package:dpip/utils/extensions/number.dart'; @@ -62,7 +63,7 @@ class ReportSheetContent extends StatelessWidget { launchUrl(report.reportUrl); }, ), - /* ActionChip( + ActionChip( avatar: const Icon(Symbols.replay), label: Text('重播'), onPressed: () { @@ -73,7 +74,7 @@ class ReportSheetContent extends StatelessWidget { ), ); }, - ), */ + ), ], ), const Divider(), From 4a5ae37f9e99ed3e1e5bd93e35099dc8902537b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BB=CF=89=E3=83=BB=EF=BC=88=E7=AB=B9=E5=AD=90?= =?UTF-8?q?=EF=BC=89?= Date: Mon, 22 Dec 2025 23:38:56 +0800 Subject: [PATCH 02/13] fix: report layer show latest report --- lib/app/map/_lib/managers/report.dart | 32 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index 2167ae731..9354495cc 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -44,6 +44,7 @@ class ReportMapLayerManager extends MapLayerManager { final currentReport = ValueNotifier(null); final isLoading = ValueNotifier(false); + final dataNotifier = ValueNotifier(0); DateTime? _lastFetchTime; @@ -103,6 +104,7 @@ class ReportMapLayerManager extends MapLayerManager { if (!context.mounted) return; GlobalProviders.data.setPartialReport(reportList); + dataNotifier.value++; _lastFetchTime = DateTime.now(); } @@ -328,19 +330,22 @@ class _ReportMapLayerSheetState extends State { @override Widget build(BuildContext context) { - return MorphingSheet( - controller: morphingSheetController, - title: '地震報告'.i18n, - borderRadius: BorderRadius.circular(16), - elevation: 4, - partialBuilder: (context, controller, sheetController) { - if (GlobalProviders.data.partialReport.isEmpty) { - return const SizedBox.shrink(); - } + return ValueListenableBuilder( + valueListenable: widget.manager.dataNotifier, + builder: (context, value, child) { + return MorphingSheet( + controller: morphingSheetController, + title: '地震報告'.i18n, + borderRadius: BorderRadius.circular(16), + elevation: 4, + partialBuilder: (context, controller, sheetController) { + if (GlobalProviders.data.partialReport.isEmpty) { + return const SizedBox.shrink(); + } - return ValueListenableBuilder( - valueListenable: widget.manager.currentReport, - builder: (context, currentReport, child) { + return ValueListenableBuilder( + valueListenable: widget.manager.currentReport, + builder: (context, currentReport, child) { // Show the first report from partial report list if (currentReport == null) { final report = GlobalProviders.data.partialReport.first; @@ -834,4 +839,5 @@ class _ReportMapLayerSheetState extends State { }, ); } -} +); +}} \ No newline at end of file From e4cb9e0acff9dd28a9ae5767d85423dd73e94622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BB=CF=89=E3=83=BB=EF=BC=88=E7=AB=B9=E5=AD=90?= =?UTF-8?q?=EF=BC=89?= Date: Mon, 22 Dec 2025 23:48:49 +0800 Subject: [PATCH 03/13] =?UTF-8?q?Internationalize=20'=E9=87=8D=E6=92=AD'?= =?UTF-8?q?=20label=20and=20refactor=20MapMonitorPage=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hardcoded '重播' label with its internationalized version using '.i18n'. Refactors MapMonitorPage to use a more descriptive 'replayTimestamp' parameter instead of 'data' for clarity. Updates all relevant usages and imports. --- lib/app/map/_lib/managers/report.dart | 9 +++++---- lib/app/map/page.dart | 8 ++++---- lib/route/report/report_sheet_content.dart | 5 +++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index 9354495cc..6e26478fa 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -624,12 +624,12 @@ class _ReportMapLayerSheetState extends State { ), ActionChip( avatar: const Icon(Symbols.replay), - label: const Text('重播'), + label: Text('重播'.i18n), onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => MapMonitorPage(data: report.time.millisecondsSinceEpoch - 5000), + builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 5000), ), ); }, @@ -838,6 +838,7 @@ class _ReportMapLayerSheetState extends State { ); }, ); + } + ); } -); -}} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/app/map/page.dart b/lib/app/map/page.dart index a0b06f83c..c426c90f3 100644 --- a/lib/app/map/page.dart +++ b/lib/app/map/page.dart @@ -29,7 +29,7 @@ class MapPageOptions { factory MapPageOptions.fromQueryParameters(Map queryParameters) { final layers = queryParameters['layers']?.split(','); final report = queryParameters['report']; - final replay = queryParameters['replay'] != null ? int.tryParse(queryParameters['replay']!) : null; + final replay = int.tryParse(queryParameters['replay'] ?? ''); return MapPageOptions( initialLayers: layers?.map((layer) => MapLayer.values.byName(layer)).toSet(), @@ -298,16 +298,16 @@ class _MapPageState extends State with TickerProviderStateMixin { } class MapMonitorPage extends StatelessWidget { - final int data; + final int replayTimestamp; - const MapMonitorPage({super.key, required this.data}); + const MapMonitorPage({super.key, required this.replayTimestamp}); @override Widget build(BuildContext context) { return MapPage( options: MapPageOptions( initialLayers: {MapLayer.monitor}, - replayTimestamp: data, + replayTimestamp: replayTimestamp, ), ); } diff --git a/lib/route/report/report_sheet_content.dart b/lib/route/report/report_sheet_content.dart index b75cfb6f9..c453c251e 100644 --- a/lib/route/report/report_sheet_content.dart +++ b/lib/route/report/report_sheet_content.dart @@ -14,6 +14,7 @@ import 'package:intl/intl.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:dpip/core/i18n.dart'; class ReportSheetContent extends StatelessWidget { final ScrollController controller; @@ -65,12 +66,12 @@ class ReportSheetContent extends StatelessWidget { ), ActionChip( avatar: const Icon(Symbols.replay), - label: Text('重播'), + label: Text('重播'.i18n), onPressed: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => MapMonitorPage(data: report.time.millisecondsSinceEpoch - 5000), + builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 5000), ), ); }, From 9c969e47c7924080a8ef1650ee968b305118abaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BB=CF=89=E3=83=BB=EF=BC=88=E7=AB=B9=E5=AD=90?= =?UTF-8?q?=EF=BC=89?= Date: Mon, 22 Dec 2025 23:52:48 +0800 Subject: [PATCH 04/13] fix: i18n --- lib/route/report/report_sheet_content.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/route/report/report_sheet_content.dart b/lib/route/report/report_sheet_content.dart index c453c251e..d32616eb0 100644 --- a/lib/route/report/report_sheet_content.dart +++ b/lib/route/report/report_sheet_content.dart @@ -14,7 +14,6 @@ import 'package:intl/intl.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:dpip/core/i18n.dart'; class ReportSheetContent extends StatelessWidget { final ScrollController controller; @@ -66,7 +65,7 @@ class ReportSheetContent extends StatelessWidget { ), ActionChip( avatar: const Icon(Symbols.replay), - label: Text('重播'.i18n), + label: const Text('重播'), onPressed: () { Navigator.push( context, From 6bce753854675a335b90367ba47843d7d62b599c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BB=CF=89=E3=83=BB=EF=BC=88=E7=AB=B9=E5=AD=90?= =?UTF-8?q?=EF=BC=89?= Date: Tue, 23 Dec 2025 00:13:00 +0800 Subject: [PATCH 05/13] fix: replay --- lib/app/map/page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/map/page.dart b/lib/app/map/page.dart index c426c90f3..f4e88a7cd 100644 --- a/lib/app/map/page.dart +++ b/lib/app/map/page.dart @@ -29,7 +29,7 @@ class MapPageOptions { factory MapPageOptions.fromQueryParameters(Map queryParameters) { final layers = queryParameters['layers']?.split(','); final report = queryParameters['report']; - final replay = int.tryParse(queryParameters['replay'] ?? ''); + final replay = queryParameters['replay']; return MapPageOptions( initialLayers: layers?.map((layer) => MapLayer.values.byName(layer)).toSet(), From a02050853d09fe39f7d39073911f4939beccbbaa Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 10:09:46 +0800 Subject: [PATCH 06/13] fix: int --- lib/app/map/page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/map/page.dart b/lib/app/map/page.dart index f4e88a7cd..c46fb1861 100644 --- a/lib/app/map/page.dart +++ b/lib/app/map/page.dart @@ -34,7 +34,7 @@ class MapPageOptions { return MapPageOptions( initialLayers: layers?.map((layer) => MapLayer.values.byName(layer)).toSet(), reportId: report, - replayTimestamp: replay, + replayTimestamp: replay == null ? null : int.tryParse(replay), ); } } From dc85a4a15ec32f6d0d4997ce82fc3058943ab7da Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 10:12:36 +0800 Subject: [PATCH 07/13] fix: newline at end of file --- lib/app/map/_lib/managers/report.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index 6e26478fa..f588fe09f 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -841,4 +841,4 @@ class _ReportMapLayerSheetState extends State { } ); } -} \ No newline at end of file +} From 3325465bad82eef703a3947a96d678b25428feb9 Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 10:16:16 +0800 Subject: [PATCH 08/13] fix: i18n --- lib/route/report/report_sheet_content.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/route/report/report_sheet_content.dart b/lib/route/report/report_sheet_content.dart index d32616eb0..5cab82d51 100644 --- a/lib/route/report/report_sheet_content.dart +++ b/lib/route/report/report_sheet_content.dart @@ -1,5 +1,6 @@ import 'package:dpip/api/model/report/earthquake_report.dart'; import 'package:dpip/app/map/page.dart'; +import 'package:dpip/core/i18n.dart'; import 'package:dpip/utils/depth_color.dart'; import 'package:dpip/utils/extensions/build_context.dart'; import 'package:dpip/utils/extensions/number.dart'; @@ -65,7 +66,7 @@ class ReportSheetContent extends StatelessWidget { ), ActionChip( avatar: const Icon(Symbols.replay), - label: const Text('重播'), + label: Text('重播'.i18n), onPressed: () { Navigator.push( context, From 6ced0863dd38819c2d2bd3f9c728cc527b5e8a53 Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 10:17:29 +0800 Subject: [PATCH 09/13] fix: change seconds --- lib/app/map/_lib/managers/report.dart | 2 +- lib/route/report/report_sheet_content.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index f588fe09f..400270938 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -629,7 +629,7 @@ class _ReportMapLayerSheetState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 5000), + builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 2000), ), ); }, diff --git a/lib/route/report/report_sheet_content.dart b/lib/route/report/report_sheet_content.dart index 5cab82d51..2c4f3d380 100644 --- a/lib/route/report/report_sheet_content.dart +++ b/lib/route/report/report_sheet_content.dart @@ -71,7 +71,7 @@ class ReportSheetContent extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 5000), + builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 2000), ), ); }, From eabdbd560eb7a9c1be85ebb582647cbddd08d85b Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 10:43:03 +0800 Subject: [PATCH 10/13] fix: replay colors --- lib/app/map/_lib/managers/monitor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/map/_lib/managers/monitor.dart b/lib/app/map/_lib/managers/monitor.dart index fddab9a78..b0511571c 100644 --- a/lib/app/map/_lib/managers/monitor.dart +++ b/lib/app/map/_lib/managers/monitor.dart @@ -1385,7 +1385,7 @@ class _MonitorMapLayerSheetState extends State { timeText, textAlign: TextAlign.center, style: TextStyle( - color: isStale ? Colors.red : context.colors.onSurface, + color: widget.manager.isReplayMode ? Colors.orange : (isStale ? Colors.red : context.colors.onSurface), fontSize: 16, ), ), From 233c2ab7b8f50461476c0f231aa60b187de68791 Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 20:46:29 +0800 Subject: [PATCH 11/13] style: format --- lib/app/map/_lib/managers/report.dart | 930 +++++++++++++------------- 1 file changed, 464 insertions(+), 466 deletions(-) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index b44033ad6..ea392786c 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -334,516 +334,514 @@ class _ReportMapLayerSheetState extends State { return ValueListenableBuilder( valueListenable: widget.manager.dataNotifier, builder: (context, value, child) { - return ResponsiveContainer( - mode: ResponsiveMode.panel, - child: MorphingSheet( - controller: morphingSheetController, - title: '地震報告'.i18n, - borderRadius: BorderRadius.circular(16), - elevation: 4, - partialBuilder: (context, controller, sheetController) { - if (GlobalProviders.data.partialReport.isEmpty) { - return const SizedBox.shrink(); - } - - return ValueListenableBuilder( - valueListenable: widget.manager.currentReport, - builder: (context, currentReport, child) { - // Show the first report from partial report list - if (currentReport == null) { - final report = GlobalProviders.data.partialReport.first; - - final locationString = report.extractLocation(); - final location = Location.tryParse(locationString)?.dynamicName ?? locationString; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Selector>( - selector: (context, model) => model.radar, - builder: (context, radar, child) { - return Column( - mainAxisSize: MainAxisSize.min, - spacing: 4, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - spacing: 8, - children: [ - Icon(Symbols.docs_rounded, size: 24, color: context.colors.onSurface), - Expanded( - child: Text( - '近期的地震報告'.i18n, - style: context.texts.titleMedium?.copyWith(color: context.colors.onSurface), - ), - ), - Text( - '更多'.i18n, - style: context.texts.labelSmall?.copyWith(color: context.colors.outline), - ), - Icon(Symbols.swipe_up_rounded, size: 16, color: context.colors.outline), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - spacing: 8, - children: [ - IntensityBox( - intensity: report.intensity, - size: 48, - borderRadius: 12, - border: !report.hasNumber, - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - report.hasNumber - ? '編號 {number} 顯著有感地震'.i18n.args({'number': report.number}) - : location, - style: context.texts.titleMedium, - ), - Text( - report.time.toLocaleDateTimeString(context), - style: context.texts.bodyMedium?.copyWith( - color: context.colors.onSurfaceVariant, - ), - ), - ], - ), - ), - Text('M ${report.magnitude.toStringAsFixed(1)}', style: context.texts.titleMedium), - ], - ), - ), - ], - ); - }, - ), - ); + return ResponsiveContainer( + mode: ResponsiveMode.panel, + child: MorphingSheet( + controller: morphingSheetController, + title: '地震報告'.i18n, + borderRadius: BorderRadius.circular(16), + elevation: 4, + partialBuilder: (context, controller, sheetController) { + if (GlobalProviders.data.partialReport.isEmpty) { + return const SizedBox.shrink(); } - // Show the current report with details - - final locationString = currentReport.extractLocation(); - final location = Location.tryParse(locationString)?.dynamicName ?? locationString; - - return Padding( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisSize: MainAxisSize.min, - spacing: 4, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - Expanded( - child: Column( + return ValueListenableBuilder( + valueListenable: widget.manager.currentReport, + builder: (context, currentReport, child) { + // Show the first report from partial report list + if (currentReport == null) { + final report = GlobalProviders.data.partialReport.first; + + final locationString = report.extractLocation(); + final location = Location.tryParse(locationString)?.dynamicName ?? locationString; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Selector>( + selector: (context, model) => model.radar, + builder: (context, radar, child) { + return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 2, + spacing: 4, children: [ - Text( - currentReport.hasNumber - ? '編號 {number} 顯著有感地震'.i18n.args({'number': currentReport.number}) - : '小區域有感地震'.i18n, - style: context.texts.labelMedium?.copyWith(color: context.colors.outline), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + spacing: 8, + children: [ + Icon(Symbols.docs_rounded, size: 24, color: context.colors.onSurface), + Expanded( + child: Text( + '近期的地震報告'.i18n, + style: context.texts.titleMedium?.copyWith(color: context.colors.onSurface), + ), + ), + Text( + '更多'.i18n, + style: context.texts.labelSmall?.copyWith(color: context.colors.outline), + ), + Icon(Symbols.swipe_up_rounded, size: 16, color: context.colors.outline), + ], + ), ), - Text(location, style: context.texts.titleLarge?.copyWith(fontWeight: FontWeight.w500)), - Text( - currentReport.time.toLocaleDateTimeString(context), - style: context.texts.bodyMedium?.copyWith(color: context.colors.onSurfaceVariant), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + spacing: 8, + children: [ + IntensityBox( + intensity: report.intensity, + size: 48, + borderRadius: 12, + border: !report.hasNumber, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + report.hasNumber + ? '編號 {number} 顯著有感地震'.i18n.args({'number': report.number}) + : location, + style: context.texts.titleMedium, + ), + Text( + report.time.toLocaleDateTimeString(context), + style: context.texts.bodyMedium?.copyWith( + color: context.colors.onSurfaceVariant, + ), + ), + ], + ), + ), + Text('M ${report.magnitude.toStringAsFixed(1)}', style: context.texts.titleMedium), + ], + ), ), ], - ), - ), - IntensityBox( - intensity: currentReport.intensity, - size: 56, - borderRadius: 12, - border: !currentReport.hasNumber, - ), - ], - ), - Row( - spacing: 16, + ); + }, + ), + ); + } + + // Show the current report with details + + final locationString = currentReport.extractLocation(); + final location = Location.tryParse(locationString)?.dynamicName ?? locationString; + + return Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 4, children: [ - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '地震規模'.i18n, - style: context.texts.bodyMedium?.copyWith(color: context.colors.onSurfaceVariant), - ), - Text( - 'M ${currentReport.magnitude.toStringAsFixed(1)}', - style: context.texts.bodyLarge?.copyWith(fontWeight: FontWeight.w500), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 2, + children: [ + Text( + currentReport.hasNumber + ? '編號 {number} 顯著有感地震'.i18n.args({'number': currentReport.number}) + : '小區域有感地震'.i18n, + style: context.texts.labelMedium?.copyWith(color: context.colors.outline), + ), + Text(location, style: context.texts.titleLarge?.copyWith(fontWeight: FontWeight.w500)), + Text( + currentReport.time.toLocaleDateTimeString(context), + style: context.texts.bodyMedium?.copyWith(color: context.colors.onSurfaceVariant), + ), + ], ), - ], - ), + ), + IntensityBox( + intensity: currentReport.intensity, + size: 56, + borderRadius: 12, + border: !currentReport.hasNumber, + ), + ], ), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '震源深度'.i18n, - style: context.texts.bodyMedium?.copyWith(color: context.colors.onSurfaceVariant), + Row( + spacing: 16, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '地震規模'.i18n, + style: context.texts.bodyMedium?.copyWith(color: context.colors.onSurfaceVariant), + ), + Text( + 'M ${currentReport.magnitude.toStringAsFixed(1)}', + style: context.texts.bodyLarge?.copyWith(fontWeight: FontWeight.w500), + ), + ], ), - Text( - '${currentReport.depth}km', - style: context.texts.bodyLarge?.copyWith(fontWeight: FontWeight.w500), + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '震源深度'.i18n, + style: context.texts.bodyMedium?.copyWith(color: context.colors.onSurfaceVariant), + ), + Text( + '${currentReport.depth}km', + style: context.texts.bodyLarge?.copyWith(fontWeight: FontWeight.w500), + ), + ], ), - ], - ), + ), + ], ), ], ), - ], - ), + ); + }, ); }, - ); - }, - fullBuilder: (context, controller, sheetController) { - return ValueListenableBuilder( - valueListenable: widget.manager.currentReport, - builder: (context, currentReport, child) { - if (currentReport == null) { - final grouped = GlobalProviders.data.partialReport - .groupListsBy((report) => report.time.toLocaleFullDateString(context)) - .entries - .toList(); - - return CustomScrollView( - controller: controller, - slivers: [ - SliverAppBar( - title: Text('地震報告'.i18n), - leading: BackButton( - onPressed: () { - sheetController.collapse(); - controller.animateTo(0, duration: Durations.short4, curve: Easing.emphasizedDecelerate); - }, - ), - floating: true, - snap: true, - pinned: true, - ), - SliverPadding( - padding: EdgeInsets.only(bottom: context.padding.bottom), - sliver: SliverList.builder( - itemCount: grouped.length, - itemBuilder: (context, index) { - final MapEntry(key: date, value: reports) = grouped[index]; - - return ListSection( - title: date, - children: reports.map((report) { - final locationString = report.extractLocation(); - final location = Location.tryParse(locationString)?.dynamicName ?? locationString; - - return ListSectionTile( - leading: IntensityBox( - intensity: report.intensity, - size: 36, - borderRadius: 8, - border: !report.hasNumber, - ), - title: location, - subtitle: Text( - '${report.hasNumber ? '${'編號 {number} 顯著有感地震'.i18n.args({'number': report.number})}\n' : ''}${report.time.toLocaleTimeString(context)}・${report.depth}km', - ), - trailing: Text( - 'M ${report.magnitude.toStringAsFixed(1)}', - style: context.texts.labelLarge, - ), - onTap: () { - widget.manager.setReport(report.id); - sheetController.collapse(); - }, + fullBuilder: (context, controller, sheetController) { + return ValueListenableBuilder( + valueListenable: widget.manager.currentReport, + builder: (context, currentReport, child) { + if (currentReport == null) { + final grouped = GlobalProviders.data.partialReport + .groupListsBy((report) => report.time.toLocaleFullDateString(context)) + .entries + .toList(); + + return CustomScrollView( + controller: controller, + slivers: [ + SliverAppBar( + title: Text('地震報告'.i18n), + leading: BackButton( + onPressed: () { + sheetController.collapse(); + controller.animateTo(0, duration: Durations.short4, curve: Easing.emphasizedDecelerate); + }, + ), + floating: true, + snap: true, + pinned: true, + ), + SliverPadding( + padding: EdgeInsets.only(bottom: context.padding.bottom), + sliver: SliverList.builder( + itemCount: grouped.length, + itemBuilder: (context, index) { + final MapEntry(key: date, value: reports) = grouped[index]; + + return ListSection( + title: date, + children: reports.map((report) { + final locationString = report.extractLocation(); + final location = Location.tryParse(locationString)?.dynamicName ?? locationString; + + return ListSectionTile( + leading: IntensityBox( + intensity: report.intensity, + size: 36, + borderRadius: 8, + border: !report.hasNumber, + ), + title: location, + subtitle: Text( + '${report.hasNumber ? '${'編號 {number} 顯著有感地震'.i18n.args({'number': report.number})}\n' : ''}${report.time.toLocaleTimeString(context)}・${report.depth}km', + ), + trailing: Text( + 'M ${report.magnitude.toStringAsFixed(1)}', + style: context.texts.labelLarge, + ), + onTap: () { + widget.manager.setReport(report.id); + sheetController.collapse(); + }, + ); + }).toList(), ); - }).toList(), - ); - }, - ), - ), - ], - ); - } - - final report = GlobalProviders.data.report[currentReport.id]; - - late List content; - - if (report == null) { - content = [const Center(child: CircularProgressIndicator())]; - } else { - final locationString = report.getLocation(); - final location = Location.tryParse(locationString)?.dynamicName ?? locationString; - - content = [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - IntensityBox(intensity: report.getMaxIntensity()), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - report.hasNumber - ? '編號 {number} 顯著有感地震'.i18n.args({'number': report.number}) - : '小區域有感地震'.i18n, - style: TextStyle(color: context.colors.onSurfaceVariant, fontSize: 14), - ), - Text(location, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - ], + }, ), ), ], - ), - ), - Wrap( - spacing: 8, - children: [ - ActionChip( - avatar: Icon(Symbols.open_in_new, color: context.colors.onPrimary), - label: Text('報告頁面'.i18n), - backgroundColor: context.colors.primary, - labelStyle: TextStyle(color: context.colors.onPrimary), - side: BorderSide(color: context.colors.primary), - onPressed: () { - launchUrl(report.reportUrl); - }, - ), - ActionChip( - avatar: const Icon(Symbols.replay), - label: Text('重播'.i18n), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 2000), + ); + } + + final report = GlobalProviders.data.report[currentReport.id]; + late List content; + + if (report == null) { + content = [const Center(child: CircularProgressIndicator())]; + } else { + final locationString = report.getLocation(); + final location = Location.tryParse(locationString)?.dynamicName ?? locationString; + + content = [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + IntensityBox(intensity: report.getMaxIntensity()), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + report.hasNumber + ? '編號 {number} 顯著有感地震'.i18n.args({'number': report.number}) + : '小區域有感地震'.i18n, + style: TextStyle(color: context.colors.onSurfaceVariant, fontSize: 14), + ), + Text(location, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), + ], + ), ), - ); - }, + ], + ), ), - ], - ), - ), - const Divider(), - DetailFieldTile( - label: '發震時間'.i18n, - child: Text( - DateFormat('yyyy/MM/dd HH:mm:ss').format(report.time), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - DetailFieldTile( - label: '位於'.i18n, - child: Text( - report.convertLatLon(), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), - Row( - children: [ - Expanded( - child: DetailFieldTile( - label: '地震規模'.i18n, - child: Row( - children: [ - Container( - height: 12, - width: 12, - margin: const EdgeInsets.only(right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: MagnitudeColor.magnitude(report.magnitude), + Wrap( + spacing: 8, + children: [ + ActionChip( + avatar: Icon(Symbols.open_in_new, color: context.colors.onPrimary), + label: Text('報告頁面'.i18n), + backgroundColor: context.colors.primary, + labelStyle: TextStyle(color: context.colors.onPrimary), + side: BorderSide(color: context.colors.primary), + onPressed: () { + launchUrl(report.reportUrl); + }, + ), + ActionChip( + avatar: const Icon(Symbols.replay), + label: Text('重播'.i18n), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MapMonitorPage(replayTimestamp: report.time.millisecondsSinceEpoch - 2000), ), - ), - Text( - 'M ${report.magnitude}', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ], + ); + }, ), + ], + ), + const Divider(), + DetailFieldTile( + label: '發震時間'.i18n, + child: Text( + DateFormat('yyyy/MM/dd HH:mm:ss').format(report.time), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), - Expanded( - child: DetailFieldTile( - label: '震源深度'.i18n, - child: Row( - children: [ - Container( - height: 12, - width: 12, - margin: const EdgeInsets.only(right: 6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: getDepthColor(report.depth), - ), + DetailFieldTile( + label: '位於'.i18n, + child: Text( + report.convertLatLon(), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + Row( + children: [ + Expanded( + child: DetailFieldTile( + label: '地震規模'.i18n, + child: Row( + children: [ + Container( + height: 12, + width: 12, + margin: const EdgeInsets.only(right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: MagnitudeColor.magnitude(report.magnitude), + ), + ), + Text( + 'M ${report.magnitude}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], ), - Text( - '${report.depth} km', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: DetailFieldTile( + label: '震源深度'.i18n, + child: Row( + children: [ + Container( + height: 12, + width: 12, + margin: const EdgeInsets.only(right: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: getDepthColor(report.depth), + ), + ), + Text( + '${report.depth} km', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], ), - ], + ), ), - ), + ], ), - ], - ), - const Divider(), - DetailFieldTile( - label: '各地震度'.i18n, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (final MapEntry(key: areaName, value: area) in report.list.entries) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, + const Divider(), + DetailFieldTile( + label: '各地震度'.i18n, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (final MapEntry(key: areaName, value: area) in report.list.entries) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text(areaName, style: const TextStyle(fontWeight: FontWeight.bold)), - ), - const SizedBox(width: 20), - Expanded( - child: Wrap( - spacing: 8, - runSpacing: 8, - children: [ - for (final MapEntry(key: townName, value: town) in area.town.entries) - ActionChip( - padding: const EdgeInsets.all(4), - side: BorderSide(color: IntensityColor.intensity(town.intensity)), - backgroundColor: IntensityColor.intensity( - town.intensity, - ).withValues(alpha: 0.16), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - avatar: AspectRatio( - aspectRatio: 1, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: IntensityColor.intensity(town.intensity), - ), - child: Center( - child: Text( - town.intensity.asIntensityDisplayLabel, - style: TextStyle( - height: 1, - fontSize: 15, - fontWeight: FontWeight.bold, - color: IntensityColor.onIntensity(town.intensity), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(areaName, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(width: 20), + Expanded( + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final MapEntry(key: townName, value: town) in area.town.entries) + ActionChip( + padding: const EdgeInsets.all(4), + side: BorderSide(color: IntensityColor.intensity(town.intensity)), + backgroundColor: IntensityColor.intensity( + town.intensity, + ).withValues(alpha: 0.16), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + avatar: AspectRatio( + aspectRatio: 1, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: IntensityColor.intensity(town.intensity), + ), + child: Center( + child: Text( + town.intensity.asIntensityDisplayLabel, + style: TextStyle( + height: 1, + fontSize: 15, + fontWeight: FontWeight.bold, + color: IntensityColor.onIntensity(town.intensity), + ), + ), ), ), ), + label: Text(townName), + onPressed: () { + sheetController.collapse(); + widget.manager.controller.animateCamera( + CameraUpdate.newLatLng(LatLng(town.lat, town.lon)), + ); + }, ), - ), - label: Text(townName), - onPressed: () { - sheetController.collapse(); - widget.manager.controller.animateCamera( - CameraUpdate.newLatLng(LatLng(town.lat, town.lon)), - ); - }, - ), - ], - ), + ], + ), + ), + ], ), ], ), - ], - ), - ), - ], - ), - ), - const Divider(), - DetailFieldTile( - label: '地震報告圖'.i18n, - child: EnlargeableImage( - aspectRatio: 4 / 3, - heroTag: 'report-image-${report.id}', - imageUrl: report.reportImageUrl, - imageName: report.reportImageName, - ), - ), - if (report.hasNumber) - DetailFieldTile( - label: '震度圖'.i18n, - child: EnlargeableImage( - aspectRatio: 2334 / 2977, - heroTag: 'intensity-image-${report.id}', - imageUrl: report.intensityMapImageUrl!, - imageName: report.intensityMapImageName!, + ), + ], + ), ), - ), - if (report.hasNumber) - DetailFieldTile( - label: '最大地動加速度圖'.i18n, - child: EnlargeableImage( - aspectRatio: 2334 / 2977, - heroTag: 'pga-image-${report.id}', - imageUrl: report.pgaMapImageUrl!, - imageName: report.pgaMapImageName!, + const Divider(), + DetailFieldTile( + label: '地震報告圖'.i18n, + child: EnlargeableImage( + aspectRatio: 4 / 3, + heroTag: 'report-image-${report.id}', + imageUrl: report.reportImageUrl, + imageName: report.reportImageName, + ), ), - ), - if (report.hasNumber) - DetailFieldTile( - label: '最大地動速度圖'.i18n, - child: EnlargeableImage( - aspectRatio: 2334 / 2977, - heroTag: 'pgv-image-${report.id}', - imageUrl: report.pgvMapImageUrl!, - imageName: report.pgvMapImageName!, + if (report.hasNumber) + DetailFieldTile( + label: '震度圖'.i18n, + child: EnlargeableImage( + aspectRatio: 2334 / 2977, + heroTag: 'intensity-image-${report.id}', + imageUrl: report.intensityMapImageUrl!, + imageName: report.intensityMapImageName!, + ), + ), + if (report.hasNumber) + DetailFieldTile( + label: '最大地動加速度圖'.i18n, + child: EnlargeableImage( + aspectRatio: 2334 / 2977, + heroTag: 'pga-image-${report.id}', + imageUrl: report.pgaMapImageUrl!, + imageName: report.pgaMapImageName!, + ), + ), + if (report.hasNumber) + DetailFieldTile( + label: '最大地動速度圖'.i18n, + child: EnlargeableImage( + aspectRatio: 2334 / 2977, + heroTag: 'pgv-image-${report.id}', + imageUrl: report.pgvMapImageUrl!, + imageName: report.pgvMapImageName!, + ), + ), + ]; + } + + return CustomScrollView( + controller: controller, + slivers: [ + SliverAppBar( + title: Text('地震報告'.i18n), + leading: BackButton( + onPressed: () { + widget.manager.setReport(null); + controller.animateTo(0, duration: Durations.short4, curve: Easing.emphasizedDecelerate); + }, + ), + floating: true, + snap: true, + pinned: true, ), - ), - ]; - } - - return CustomScrollView( - controller: controller, - slivers: [ - SliverAppBar( - title: Text('地震報告'.i18n), - leading: BackButton( - onPressed: () { - widget.manager.setReport(null); - controller.animateTo(0, duration: Durations.short4, curve: Easing.emphasizedDecelerate); - }, - ), - floating: true, - snap: true, - pinned: true, - ), - SliverPadding( - padding: EdgeInsets.fromLTRB(16, 0, 16, context.padding.bottom), - sliver: SliverList.list(children: content), - ), - ], + SliverPadding( + padding: EdgeInsets.fromLTRB(16, 0, 16, context.padding.bottom), + sliver: SliverList.list(children: content), + ), + ], + ); + }, ); }, - ); - }, - ), + ), + ); + } ); - } - ); } } From 3537360a9a565dc06ad8af6264dbf13d674fdc32 Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 20:46:42 +0800 Subject: [PATCH 12/13] fix: width --- lib/widgets/responsive/responsive_container.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/widgets/responsive/responsive_container.dart b/lib/widgets/responsive/responsive_container.dart index 0b69cb77f..e1a6e668e 100644 --- a/lib/widgets/responsive/responsive_container.dart +++ b/lib/widgets/responsive/responsive_container.dart @@ -31,7 +31,11 @@ class ResponsiveContainer extends StatelessWidget { switch (mode) { case ResponsiveMode.panel: - contentMaxWidth = (maxWidth ?? constraints.maxWidth * 0.45); + if (isLargeTablet) { + contentMaxWidth = maxWidth ?? width * 0.45; + } else { + contentMaxWidth = width; + } alignment = isLargeTablet ? Alignment.centerRight : Alignment.center; From df823373859fd93677d2a6781991abe946c465ee Mon Sep 17 00:00:00 2001 From: lowrt Date: Tue, 23 Dec 2025 21:52:46 +0800 Subject: [PATCH 13/13] fix: Report Information Box --- lib/app/map/_lib/managers/report.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index ea392786c..f7ae0ffef 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -337,6 +337,7 @@ class _ReportMapLayerSheetState extends State { return ResponsiveContainer( mode: ResponsiveMode.panel, child: MorphingSheet( + key: ValueKey(value), controller: morphingSheetController, title: '地震報告'.i18n, borderRadius: BorderRadius.circular(16),