From 4cff95e57153bc2c3ec5e754adb84b2e1796caa8 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Wed, 22 Apr 2026 11:57:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E3=83=90=E3=82=B9=E9=81=8B=E8=A1=8C?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=81=AB=E9=81=85=E5=BB=B6=E8=A6=8B?= =?UTF-8?q?=E8=BE=BC=E3=81=BF=E8=A1=A8=E7=A4=BA=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BusTrip に nullable な delayMinutes を追加し、バス画面のリストタイルで `N分遅延見込み` を表示する。データソースは未接続で、null のときは非表示。 Co-Authored-By: Claude Opus 4.7 (1M context) --- Flutter/lib/domain/bus_trip.dart | 1 + Flutter/lib/feature/bus/bus_screen.dart | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Flutter/lib/domain/bus_trip.dart b/Flutter/lib/domain/bus_trip.dart index fe3edf60..bcc9cfd2 100644 --- a/Flutter/lib/domain/bus_trip.dart +++ b/Flutter/lib/domain/bus_trip.dart @@ -28,6 +28,7 @@ abstract class BusTrip with _$BusTrip { const factory BusTrip({ required String route, required List stops, + int? delayMinutes, }) = _BusTrip; factory BusTrip.fromFirebase( diff --git a/Flutter/lib/feature/bus/bus_screen.dart b/Flutter/lib/feature/bus/bus_screen.dart index 7916c2dd..c276e4e1 100644 --- a/Flutter/lib/feature/bus/bus_screen.dart +++ b/Flutter/lib/feature/bus/bus_screen.dart @@ -184,6 +184,7 @@ final class BusScreen extends HookConsumerWidget { isTo: value.isTo, isKameda: kameda, myBusStopName: value.myBusStop.name, + delayMinutes: busTrip.delayMinutes, onTap: busTrip.route == '0' ? null : () async { @@ -349,6 +350,7 @@ final class _BusTripTile extends StatelessWidget { required this.isTo, required this.isKameda, required this.myBusStopName, + this.delayMinutes, this.onTap, super.key, }); @@ -359,6 +361,7 @@ final class _BusTripTile extends StatelessWidget { final bool isTo; final bool isKameda; final String myBusStopName; + final int? delayMinutes; final VoidCallback? onTap; BusType _busType() { @@ -401,6 +404,24 @@ final class _BusTripTile extends StatelessWidget { '${formatDuration(beginTime)} → ${formatDuration(endTime)}', style: Theme.of(context).textTheme.headlineSmall, ), + if (delayMinutes != null && delayMinutes! > 0) + Row( + spacing: 4, + children: [ + Icon( + Icons.schedule, + size: 16, + color: SemanticColor.light.accentWarning, + ), + Text( + '$delayMinutes分遅延見込み', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: SemanticColor.light.accentWarning, + fontWeight: FontWeight.w600, + ), + ), + ], + ), Text( directionText.isEmpty ? route : '$route $directionText', style: Theme.of(context).textTheme.bodySmall?.copyWith( From 1acf5c85802e4bc7f75c4942444336a49571109e Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Wed, 22 Apr 2026 12:03:20 +0900 Subject: [PATCH 2/4] =?UTF-8?q?BusTrip.fromFirebase=20=E3=81=AB=200?= =?UTF-8?q?=E3=80=9C8=20=E5=88=86=E3=81=AE=E3=83=80=E3=83=9F=E3=83=BC?= =?UTF-8?q?=E9=81=85=E5=BB=B6=E3=82=92=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 遅延表示 UI の確認用に、delayMinutes のデータソースが決まるまでの 仮値を BusTrip.fromFirebase 内で生成するようにした。dart:math の Random で呼び出しごとに 0〜8 の一様乱数を返し、TODO コメントで 本実装で差し替える場所を明示している。振る舞いは遅延表示の モック化のみで、既存の便データ構築ロジックには手を入れていない。 Co-Authored-By: Claude Opus 4.7 (1M context) --- Flutter/lib/domain/bus_trip.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Flutter/lib/domain/bus_trip.dart b/Flutter/lib/domain/bus_trip.dart index bcc9cfd2..c55a9099 100644 --- a/Flutter/lib/domain/bus_trip.dart +++ b/Flutter/lib/domain/bus_trip.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:dotto/domain/bus_stop.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -23,6 +25,10 @@ abstract class BusTripStop with _$BusTripStop { } } +final Random _mockDelayRandom = Random(); + +int _mockDelayMinutes() => _mockDelayRandom.nextInt(9); + @freezed abstract class BusTrip with _$BusTrip { const factory BusTrip({ @@ -36,8 +42,9 @@ abstract class BusTrip with _$BusTrip { List allStops, ) { final stopsList = map['stops'] as List; + final route = map['route'] as String; return BusTrip( - route: map['route'] as String, + route: route, stops: stopsList.map((e) { final stopMap = Map.from(e as Map); final id = stopMap['id'] as int; @@ -46,6 +53,8 @@ abstract class BusTrip with _$BusTrip { ); return BusTripStop.fromFirebase(targetBusStop, stopMap); }).toList(), + // TODO(dummy): 遅延分数のデータソースが決まるまでの仮置き。 + delayMinutes: _mockDelayMinutes(), ); } } From 0aac8acc45773c56df00666fd32044743a9c9067 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Wed, 22 Apr 2026 15:18:53 +0900 Subject: [PATCH 3/4] =?UTF-8?q?BusTrip=20=E3=81=AB=E9=81=8B=E4=BC=91?= =?UTF-8?q?=E3=83=95=E3=83=A9=E3=82=B0=20isCancelled=20=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 遅延見込みと並んで運休状態も扱えるよう、BusTrip に `isCancelled` フィールドとモック用ヘルパーを追加した。 データソースが未確定のため、`fromFirebase` では約10本に1本を 運休扱いとするダミー生成を行い、運休時は遅延分数を null にする。 --- Flutter/lib/domain/bus_trip.dart | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Flutter/lib/domain/bus_trip.dart b/Flutter/lib/domain/bus_trip.dart index c55a9099..845ec04b 100644 --- a/Flutter/lib/domain/bus_trip.dart +++ b/Flutter/lib/domain/bus_trip.dart @@ -25,9 +25,11 @@ abstract class BusTripStop with _$BusTripStop { } } -final Random _mockDelayRandom = Random(); +final Random _mockRandom = Random(); -int _mockDelayMinutes() => _mockDelayRandom.nextInt(9); +int _mockDelayMinutes() => _mockRandom.nextInt(9); + +bool _mockIsCancelled() => _mockRandom.nextInt(10) == 0; @freezed abstract class BusTrip with _$BusTrip { @@ -35,6 +37,7 @@ abstract class BusTrip with _$BusTrip { required String route, required List stops, int? delayMinutes, + @Default(false) bool isCancelled, }) = _BusTrip; factory BusTrip.fromFirebase( @@ -43,6 +46,8 @@ abstract class BusTrip with _$BusTrip { ) { final stopsList = map['stops'] as List; final route = map['route'] as String; + // TODO(dummy): 遅延/運休のデータソースが決まるまでの仮置き。 + final cancelled = _mockIsCancelled(); return BusTrip( route: route, stops: stopsList.map((e) { @@ -53,8 +58,8 @@ abstract class BusTrip with _$BusTrip { ); return BusTripStop.fromFirebase(targetBusStop, stopMap); }).toList(), - // TODO(dummy): 遅延分数のデータソースが決まるまでの仮置き。 - delayMinutes: _mockDelayMinutes(), + delayMinutes: cancelled ? null : _mockDelayMinutes(), + isCancelled: cancelled, ); } } From 59e582269b544d13e9c4250fedb836e12cda1d96 Mon Sep 17 00:00:00 2001 From: Kanta Oikawa Date: Wed, 22 Apr 2026 15:19:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E3=83=90=E3=82=B9=E9=81=8B=E8=A1=8C?= =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=AB=E3=81=AB=E9=81=8B=E4=BC=91=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BusTrip.isCancelled に対応し、運休便のタイルでは 発着時刻を取り消し線と弱色で表示し、遅延見込みの代わりに 赤色の「運休」バッジを表示する。運休便および route == '0' の ダミー便はタップによる詳細遷移も無効化する。 --- Flutter/lib/feature/bus/bus_screen.dart | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Flutter/lib/feature/bus/bus_screen.dart b/Flutter/lib/feature/bus/bus_screen.dart index c276e4e1..0b3691e4 100644 --- a/Flutter/lib/feature/bus/bus_screen.dart +++ b/Flutter/lib/feature/bus/bus_screen.dart @@ -185,7 +185,8 @@ final class BusScreen extends HookConsumerWidget { isKameda: kameda, myBusStopName: value.myBusStop.name, delayMinutes: busTrip.delayMinutes, - onTap: busTrip.route == '0' + isCancelled: busTrip.isCancelled, + onTap: busTrip.route == '0' || busTrip.isCancelled ? null : () async { await Navigator.of(context).push( @@ -351,6 +352,7 @@ final class _BusTripTile extends StatelessWidget { required this.isKameda, required this.myBusStopName, this.delayMinutes, + this.isCancelled = false, this.onTap, super.key, }); @@ -362,6 +364,7 @@ final class _BusTripTile extends StatelessWidget { final bool isKameda; final String myBusStopName; final int? delayMinutes; + final bool isCancelled; final VoidCallback? onTap; BusType _busType() { @@ -402,9 +405,34 @@ final class _BusTripTile extends StatelessWidget { children: [ Text( '${formatDuration(beginTime)} → ${formatDuration(endTime)}', - style: Theme.of(context).textTheme.headlineSmall, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: isCancelled + ? SemanticColor.light.labelSecondary + : null, + decoration: isCancelled + ? TextDecoration.lineThrough + : null, + ), ), - if (delayMinutes != null && delayMinutes! > 0) + if (isCancelled) + Row( + spacing: 4, + children: [ + Icon( + Icons.block, + size: 16, + color: SemanticColor.light.accentError, + ), + Text( + '運休', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: SemanticColor.light.accentError, + fontWeight: FontWeight.w600, + ), + ), + ], + ) + else if (delayMinutes != null && delayMinutes! > 0) Row( spacing: 4, children: [