diff --git a/lib/app/map/_lib/managers/monitor.dart b/lib/app/map/_lib/managers/monitor.dart index 3afea6671..fb2759ade 100644 --- a/lib/app/map/_lib/managers/monitor.dart +++ b/lib/app/map/_lib/managers/monitor.dart @@ -1389,7 +1389,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, ), ), diff --git a/lib/app/map/_lib/managers/report.dart b/lib/app/map/_lib/managers/report.dart index fd090062f..f7ae0ffef 100644 --- a/lib/app/map/_lib/managers/report.dart +++ b/lib/app/map/_lib/managers/report.dart @@ -13,6 +13,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'; @@ -44,6 +45,7 @@ class ReportMapLayerManager extends MapLayerManager { final currentReport = ValueNotifier(null); final isLoading = ValueNotifier(false); + final dataNotifier = ValueNotifier(0); DateTime? _lastFetchTime; @@ -103,6 +105,7 @@ class ReportMapLayerManager extends MapLayerManager { if (!context.mounted) return; GlobalProviders.data.setPartialReport(reportList); + dataNotifier.value++; _lastFetchTime = DateTime.now(); } @@ -328,513 +331,518 @@ class _ReportMapLayerSheetState extends State { @override Widget build(BuildContext context) { - 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 ValueListenableBuilder( + valueListenable: widget.manager.dataNotifier, + builder: (context, value, child) { + return ResponsiveContainer( + mode: ResponsiveMode.panel, + child: MorphingSheet( + key: ValueKey(value), + 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: const Text('重播'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MapMonitorPage(data: report.time.millisecondsSinceEpoch - 5000), + ); + } + + 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), + ), + ], + ); + }, ); }, - ); - }, - ), + ), + ); + } ); } } diff --git a/lib/app/map/page.dart b/lib/app/map/page.dart index 82ba60298..c46fb1861 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']; return MapPageOptions( initialLayers: layers?.map((layer) => MapLayer.values.byName(layer)).toSet(), reportId: report, + replayTimestamp: replay == null ? null : int.tryParse(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 replayTimestamp; + + const MapMonitorPage({super.key, required this.replayTimestamp}); + + @override + Widget build(BuildContext context) { + return MapPage( + options: MapPageOptions( + initialLayers: {MapLayer.monitor}, + replayTimestamp: replayTimestamp, + ), + ); + } +} diff --git a/lib/route/report/report_sheet_content.dart b/lib/route/report/report_sheet_content.dart index d2475faa8..2c4f3d380 100644 --- a/lib/route/report/report_sheet_content.dart +++ b/lib/route/report/report_sheet_content.dart @@ -1,4 +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'; @@ -62,18 +64,18 @@ class ReportSheetContent extends StatelessWidget { launchUrl(report.reportUrl); }, ), - /* ActionChip( + 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 - 2000), ), ); }, - ), */ + ), ], ), const Divider(), 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;