diff --git a/docs/Architecture.md b/docs/Architecture.md index e9a36a4f..a9dab835 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -36,7 +36,7 @@ OnTime follows **Clean Architecture** principles with a clear separation of conc │ DATA LAYER │ ├─────────────────────────────────────────────────────────────┤ │ • Repository Implementations │ -│ • Data Sources (Remote API, Local Database) │ +│ • Data Sources (Remote API, Local Database, Local Storage)│ │ • Data Models (JSON Serialization) │ │ • Database Tables & DAOs │ └─────────────────────────────────────────────────────────────┘ @@ -289,6 +289,12 @@ Database ← ScheduleDao ← ScheduleRepository ← ScheduleEntity - **Synchronization strategy** for online/offline data consistency - **Caching mechanisms** for improved performance +### 5. **Local Storage for Timed Preparation** + +- `PreparationWithTimeLocalDataSource` persists `PreparationWithTimeEntity` per schedule using SharedPreferences. +- Intended for lightweight, per-schedule timer state (elapsed time, completion) that should survive app restarts. +- Repository reads canonical preparation from remote/DB; BLoC can merge it with locally persisted timing state when needed. + ## 🧪 Testing Strategy ### Structure diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a580e400..3b0409ad 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -208,7 +208,7 @@ SPEC CHECKSUMS: FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 914057fda669db5073d3ca9d94ea932e7df3c964 flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 diff --git a/lib/data/data_sources/preparation_with_time_local_data_source.dart b/lib/data/data_sources/preparation_with_time_local_data_source.dart new file mode 100644 index 00000000..48b7b09f --- /dev/null +++ b/lib/data/data_sources/preparation_with_time_local_data_source.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; +import 'package:on_time_front/domain/entities/preparation_step_with_time_entity.dart'; + +abstract interface class PreparationWithTimeLocalDataSource { + Future savePreparation( + String scheduleId, PreparationWithTimeEntity preparation); + Future loadPreparation(String scheduleId); + Future clearPreparation(String scheduleId); +} + +@Injectable(as: PreparationWithTimeLocalDataSource) +class PreparationWithTimeLocalDataSourceImpl + implements PreparationWithTimeLocalDataSource { + static const String _prefsKeyPrefix = 'preparation_with_time_'; + + @override + Future savePreparation( + String scheduleId, PreparationWithTimeEntity preparation) async { + final prefs = await SharedPreferences.getInstance(); + final key = '$_prefsKeyPrefix$scheduleId'; + + final jsonMap = { + 'steps': preparation.preparationStepList + .map((s) => { + 'id': s.id, + 'name': s.preparationName, + 'time': s.preparationTime.inMilliseconds, + 'nextId': s.nextPreparationId, + 'elapsed': s.elapsedTime.inMilliseconds, + 'isDone': s.isDone, + }) + .toList() + }; + + await prefs.setString(key, jsonEncode(jsonMap)); + } + + @override + Future loadPreparation(String scheduleId) async { + final prefs = await SharedPreferences.getInstance(); + final key = '$_prefsKeyPrefix$scheduleId'; + final jsonString = prefs.getString(key); + if (jsonString == null) return null; + + final Map map = jsonDecode(jsonString); + final List steps = map['steps'] as List; + + final stepEntities = steps.map((raw) { + final m = raw as Map; + return PreparationStepWithTimeEntity( + id: m['id'] as String, + preparationName: m['name'] as String, + preparationTime: Duration(milliseconds: (m['time'] as num).toInt()), + nextPreparationId: m['nextId'] as String?, + elapsedTime: Duration(milliseconds: (m['elapsed'] as num).toInt()), + isDone: m['isDone'] as bool? ?? false, + ); + }).toList(); + + return PreparationWithTimeEntity(preparationStepList: stepEntities); + } + + @override + Future clearPreparation(String scheduleId) async { + final prefs = await SharedPreferences.getInstance(); + final key = '$_prefsKeyPrefix$scheduleId'; + await prefs.remove(key); + } +} diff --git a/lib/data/repositories/timed_preparation_repository_impl.dart b/lib/data/repositories/timed_preparation_repository_impl.dart new file mode 100644 index 00000000..87b6577e --- /dev/null +++ b/lib/data/repositories/timed_preparation_repository_impl.dart @@ -0,0 +1,27 @@ +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/data/data_sources/preparation_with_time_local_data_source.dart'; +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; +import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart'; + +@Singleton(as: TimedPreparationRepository) +class TimedPreparationRepositoryImpl implements TimedPreparationRepository { + final PreparationWithTimeLocalDataSource localDataSource; + + TimedPreparationRepositoryImpl({required this.localDataSource}); + + @override + Future clearTimedPreparation(String scheduleId) { + return localDataSource.clearPreparation(scheduleId); + } + + @override + Future getTimedPreparation(String scheduleId) { + return localDataSource.loadPreparation(scheduleId); + } + + @override + Future saveTimedPreparation( + String scheduleId, PreparationWithTimeEntity preparation) { + return localDataSource.savePreparation(scheduleId, preparation); + } +} diff --git a/lib/domain/entities/preparation_step_with_time_entity.dart b/lib/domain/entities/preparation_step_with_time_entity.dart new file mode 100644 index 00000000..f41ed687 --- /dev/null +++ b/lib/domain/entities/preparation_step_with_time_entity.dart @@ -0,0 +1,58 @@ +import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; + +class PreparationStepWithTimeEntity extends PreparationStepEntity { + final Duration elapsedTime; + final bool isDone; + + const PreparationStepWithTimeEntity({ + required super.id, + required super.preparationName, + required super.preparationTime, + required super.nextPreparationId, + this.elapsedTime = Duration.zero, + this.isDone = false, + }); + + @override + PreparationStepWithTimeEntity copyWith({ + String? id, + String? preparationName, + Duration? preparationTime, + String? nextPreparationId, + Duration? elapsedTime, + bool? isDone, + }) { + return PreparationStepWithTimeEntity( + id: id ?? this.id, + preparationName: preparationName ?? this.preparationName, + preparationTime: preparationTime ?? this.preparationTime, + nextPreparationId: nextPreparationId ?? this.nextPreparationId, + elapsedTime: elapsedTime ?? this.elapsedTime, + isDone: isDone ?? this.isDone, + ); + } + + PreparationStepWithTimeEntity timeElapsed(Duration elapsed) { + final updatedElapsed = elapsedTime + elapsed; + final updatedIsDone = updatedElapsed >= preparationTime; + return copyWith( + elapsedTime: updatedElapsed, + isDone: updatedIsDone, + ); + } + + @override + String toString() { + return 'PreparationStepWithTimeEntity(id: $id, preparationName: $preparationName, preparationTime: $preparationTime, nextPreparationId: $nextPreparationId, elapsedTime: $elapsedTime, isDone: $isDone)'; + } + + @override + List get props => [ + id, + preparationName, + preparationTime, + nextPreparationId, + elapsedTime, + isDone + ]; +} diff --git a/lib/domain/entities/preparation_with_time_entity.dart b/lib/domain/entities/preparation_with_time_entity.dart new file mode 100644 index 00000000..3ca4e80b --- /dev/null +++ b/lib/domain/entities/preparation_with_time_entity.dart @@ -0,0 +1,192 @@ +import 'package:equatable/equatable.dart'; +import 'package:on_time_front/domain/entities/preparation_entity.dart'; +import 'package:on_time_front/domain/entities/preparation_step_with_time_entity.dart'; +import 'package:on_time_front/presentation/shared/constants/constants.dart'; + +class PreparationWithTimeEntity extends PreparationEntity implements Equatable { + const PreparationWithTimeEntity({ + required List preparationStepList, + }) : super(preparationStepList: preparationStepList); + + factory PreparationWithTimeEntity.fromPreparation( + PreparationEntity preparation) { + return PreparationWithTimeEntity( + preparationStepList: preparation.preparationStepList + .map( + (step) => PreparationStepWithTimeEntity( + id: step.id, + preparationName: step.preparationName, + preparationTime: step.preparationTime, + nextPreparationId: step.nextPreparationId, + ), + ) + .toList(), + ); + } + + PreparationWithTimeEntity copyWith({ + List? preparationStepList, + }) { + return PreparationWithTimeEntity( + preparationStepList: preparationStepList ?? this.preparationStepList, + ); + } + + @override + List get preparationStepList => + super.preparationStepList.cast(); + + PreparationStepWithTimeEntity? get currentStep { + for (final step in preparationStepList) { + if (!step.isDone) { + return step; + } + } + return null; // All steps are done + } + + /// Returns true if all preparation steps are completed + bool get isAllStepsDone { + return currentStep == null; + } + + Duration get elapsedTime => preparationStepList.fold( + Duration.zero, (sum, s) => sum + s.elapsedTime); + + /// Returns the progress as a value between 0.0 and 1.0 + double get progress { + final totalSeconds = totalDuration.inSeconds; + final elapsed = elapsedTime.inSeconds; + return totalSeconds == 0 ? 0.0 : (elapsed / totalSeconds).clamp(0.0, 1.0); + } + + /// Returns the current step index, or -1 if all steps are done + int get currentStepIndex { + final current = currentStep; + if (current == null) return -1; + return preparationStepList.indexWhere((step) => step.id == current.id); + } + + /// Returns the resolved current step index for display purposes + int get resolvedCurrentStepIndex { + final index = currentStepIndex; + return index == -1 ? preparationStepList.length - 1 : index; + } + + /// Returns the current step's remaining time + Duration get currentStepRemainingTime { + final current = currentStep; + if (current == null) return Duration.zero; + final remaining = current.preparationTime - current.elapsedTime; + return remaining.isNegative ? Duration.zero : remaining; + } + + /// Returns the current step's name for display + String get currentStepName { + final current = currentStep; + if (current != null) return current.preparationName; + return preparationStepList.isNotEmpty + ? preparationStepList.last.preparationName + : ''; + } + + /// Returns elapsed times for each step in seconds + List get stepElapsedTimesInSeconds { + return preparationStepList + .map((step) => step.elapsedTime.inSeconds) + .toList(); + } + + /// Returns the preparation state for each step + List get preparationStepStates { + final resolvedIndex = resolvedCurrentStepIndex; + + return List.generate( + preparationStepList.length, + (index) { + if (isAllStepsDone) { + // All steps are done + return PreparationStateEnum.done; + } + if (index < resolvedIndex) { + return PreparationStateEnum.done; + } + if (index == resolvedIndex && !isAllStepsDone) { + return PreparationStateEnum.now; + } + return PreparationStateEnum.yet; + }, + ); + } + + PreparationWithTimeEntity timeElapsed(Duration elapsed) { + final current = currentStep; + if (current == null) { + return this; // All steps are done, no changes needed + } + + Duration remainingElapsed = elapsed; + List updatedSteps = + List.from(preparationStepList); + + // Find the current step index + int currentIndex = updatedSteps.indexWhere((step) => step.id == current.id); + + // Apply elapsed time to current and subsequent steps if needed + while (remainingElapsed > Duration.zero && + currentIndex < updatedSteps.length) { + final step = updatedSteps[currentIndex]; + if (step.isDone) { + currentIndex++; + continue; + } + + final stepRemainingTime = step.preparationTime - step.elapsedTime; + + if (remainingElapsed >= stepRemainingTime) { + // Complete this step and move to next + updatedSteps[currentIndex] = step.copyWith( + elapsedTime: step.preparationTime, + isDone: true, + ); + remainingElapsed -= stepRemainingTime; + currentIndex++; + } else { + // Partially complete this step + updatedSteps[currentIndex] = step.copyWith( + elapsedTime: step.elapsedTime + remainingElapsed, + isDone: step.elapsedTime + remainingElapsed >= step.preparationTime, + ); + remainingElapsed = Duration.zero; + } + } + + return copyWith(preparationStepList: updatedSteps); + } + + PreparationWithTimeEntity skipCurrentStep() { + final current = currentStep; + if (current == null) { + return this; // All steps are done, no changes needed + } + + final updatedCurrentStep = current.copyWith( + elapsedTime: current.preparationTime, + isDone: true, + ); + return copyWith( + preparationStepList: preparationStepList + .map((step) => + step.id == updatedCurrentStep.id ? updatedCurrentStep : step) + .toList(), + ); + } + + @override + String toString() { + return 'PreparationWithTimeEntity(preparationStepList: ${preparationStepList.toString()})'; + } + + @override + List get props => [preparationStepList]; +} diff --git a/lib/domain/entities/schedule_with_preparation_entity.dart b/lib/domain/entities/schedule_with_preparation_entity.dart index 4c4c72a3..5d676aed 100644 --- a/lib/domain/entities/schedule_with_preparation_entity.dart +++ b/lib/domain/entities/schedule_with_preparation_entity.dart @@ -1,8 +1,8 @@ -import 'package:on_time_front/domain/entities/preparation_entity.dart'; +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; import 'package:on_time_front/domain/entities/schedule_entity.dart'; class ScheduleWithPreparationEntity extends ScheduleEntity { - final PreparationEntity preparation; + final PreparationWithTimeEntity preparation; const ScheduleWithPreparationEntity({ required super.id, @@ -19,13 +19,28 @@ class ScheduleWithPreparationEntity extends ScheduleEntity { ///Returns the total duration of the schedule including the moving time and the preparation time. Duration get totalDuration => - moveTime + preparation.totalDuration + scheduleSpareTime!; + moveTime + + preparation.totalDuration + + (scheduleSpareTime ?? Duration.zero); ///Returns the time when the preparation starts. DateTime get preparationStartTime => scheduleTime.subtract(totalDuration); + /// Returns the time remaining before needing to leave + Duration get timeRemainingBeforeLeaving { + final now = DateTime.now(); + final spareTime = scheduleSpareTime ?? Duration.zero; + final remaining = scheduleTime.difference(now) - moveTime - spareTime; + return remaining; + } + + /// Returns whether the schedule is running late + bool get isLate { + return timeRemainingBeforeLeaving.isNegative; + } + static ScheduleWithPreparationEntity fromScheduleAndPreparationEntity( - ScheduleEntity schedule, PreparationEntity preparation) { + ScheduleEntity schedule, PreparationWithTimeEntity preparation) { return ScheduleWithPreparationEntity( id: schedule.id, place: schedule.place, @@ -39,4 +54,18 @@ class ScheduleWithPreparationEntity extends ScheduleEntity { preparation: preparation, ); } + + @override + List get props => [ + id, + place, + scheduleName, + scheduleTime, + moveTime, + isChanged, + isStarted, + scheduleSpareTime, + scheduleNote, + preparation + ]; } diff --git a/lib/domain/repositories/timed_preparation_repository.dart b/lib/domain/repositories/timed_preparation_repository.dart new file mode 100644 index 00000000..1649a36f --- /dev/null +++ b/lib/domain/repositories/timed_preparation_repository.dart @@ -0,0 +1,10 @@ +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; + +abstract interface class TimedPreparationRepository { + Future saveTimedPreparation( + String scheduleId, PreparationWithTimeEntity preparation); + + Future getTimedPreparation(String scheduleId); + + Future clearTimedPreparation(String scheduleId); +} diff --git a/lib/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart b/lib/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart index d246d2a3..cc2f1450 100644 --- a/lib/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart +++ b/lib/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:injectable/injectable.dart'; import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; +import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart'; import 'package:on_time_front/domain/use-cases/get_preparation_by_schedule_id_use_case.dart'; import 'package:on_time_front/domain/use-cases/get_schedules_by_date_use_case.dart'; import 'package:on_time_front/domain/use-cases/load_schedules_for_week_use_case.dart'; @@ -11,11 +13,13 @@ class GetNearestUpcomingScheduleUseCase { final GetSchedulesByDateUseCase _getScheduleByDateUseCase; final GetPreparationByScheduleIdUseCase _getPreparationByScheduleIdUseCase; final LoadSchedulesForWeekUseCase _loadSchedulesForWeekUseCase; + final TimedPreparationRepository _timedPreparationRepository; GetNearestUpcomingScheduleUseCase( this._getScheduleByDateUseCase, this._getPreparationByScheduleIdUseCase, - this._loadSchedulesForWeekUseCase); + this._loadSchedulesForWeekUseCase, + this._timedPreparationRepository); Stream call() async* { final DateTime now = DateTime.now(); @@ -26,11 +30,24 @@ class GetNearestUpcomingScheduleUseCase { _getScheduleByDateUseCase(now, now.add(const Duration(days: 2))); await for (final upcomingSchedule in upcomingScheduleStream) { if (upcomingSchedule.isNotEmpty) { + final schedule = upcomingSchedule.first; + + // First try to load locally stored timed preparation + final localTimed = + await _timedPreparationRepository.getTimedPreparation(schedule.id); + if (localTimed != null) { + yield ScheduleWithPreparationEntity.fromScheduleAndPreparationEntity( + schedule, localTimed); + continue; + } + + // Fallback to fetching canonical preparation from source final preparation = - await _getPreparationByScheduleIdUseCase(upcomingSchedule.first.id); + await _getPreparationByScheduleIdUseCase(schedule.id); final scheduleWithPreparation = ScheduleWithPreparationEntity.fromScheduleAndPreparationEntity( - upcomingSchedule.first, preparation); + schedule, + PreparationWithTimeEntity.fromPreparation(preparation)); yield scheduleWithPreparation; } else { yield null; diff --git a/lib/domain/use-cases/save_timed_preparation_use_case.dart b/lib/domain/use-cases/save_timed_preparation_use_case.dart new file mode 100644 index 00000000..a8c56ae1 --- /dev/null +++ b/lib/domain/use-cases/save_timed_preparation_use_case.dart @@ -0,0 +1,15 @@ +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; +import 'package:on_time_front/domain/repositories/timed_preparation_repository.dart'; + +@Injectable() +class SaveTimedPreparationUseCase { + final TimedPreparationRepository _timedPreparationRepository; + + SaveTimedPreparationUseCase(this._timedPreparationRepository); + + Future call(String scheduleId, PreparationWithTimeEntity preparation) { + return _timedPreparationRepository.saveTimedPreparation( + scheduleId, preparation); + } +} diff --git a/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_bloc.dart b/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_bloc.dart deleted file mode 100644 index ebf8867b..00000000 --- a/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_bloc.dart +++ /dev/null @@ -1,45 +0,0 @@ -library; - -import 'dart:async'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:on_time_front/domain/entities/preparation_entity.dart'; -import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; -import 'package:injectable/injectable.dart'; -import 'package:on_time_front/domain/use-cases/get_preparation_by_schedule_id_use_case.dart'; - -part 'alarm_screen_preparation_info_event.dart'; -part 'alarm_screen_preparation_info_state.dart'; - -@Injectable() -class AlarmScreenPreparationInfoBloc extends Bloc< - AlarmScreenPreparationInfoEvent, AlarmScreenPreparationInfoState> { - final GetPreparationByScheduleIdUseCase _getPreparationByScheduleIdUseCase; - - AlarmScreenPreparationInfoBloc( - this._getPreparationByScheduleIdUseCase, - ) : super(AlarmScreenPreparationInitial()) { - on(_onSubscriptionRequested); - } - - Future _onSubscriptionRequested( - AlarmScreenPreparationSubscriptionRequested event, - Emitter emit) async { - emit(AlarmScreenPreparationInfoLoadInProgress()); - - try { - final PreparationEntity prepEntity = - await _getPreparationByScheduleIdUseCase(event.scheduleId); - - final List steps = prepEntity.preparationStepList; - - emit(AlarmScreenPreparationLoadSuccess( - preparationSteps: steps, - schedule: event.schedule, - )); - } catch (e) { - emit(AlarmScreenPreparationLoadFailure(e.toString())); - } - } -} diff --git a/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_event.dart b/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_event.dart deleted file mode 100644 index 08768cc8..00000000 --- a/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_event.dart +++ /dev/null @@ -1,21 +0,0 @@ -part of 'alarm_screen_preparation_info_bloc.dart'; - -abstract class AlarmScreenPreparationInfoEvent extends Equatable { - const AlarmScreenPreparationInfoEvent(); - - @override - List get props => []; -} - -class AlarmScreenPreparationSubscriptionRequested - extends AlarmScreenPreparationInfoEvent { - final String scheduleId; - final ScheduleEntity schedule; - - const AlarmScreenPreparationSubscriptionRequested({ - required this.scheduleId, - required this.schedule, - }); - @override - List get props => [scheduleId, schedule]; -} diff --git a/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_state.dart b/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_state.dart deleted file mode 100644 index 3e2feee2..00000000 --- a/lib/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_state.dart +++ /dev/null @@ -1,63 +0,0 @@ -part of 'alarm_screen_preparation_info_bloc.dart'; - -sealed class AlarmScreenPreparationInfoState extends Equatable { - const AlarmScreenPreparationInfoState(); - - @override - List get props => []; -} - -class AlarmScreenPreparationInitial extends AlarmScreenPreparationInfoState {} - -class AlarmScreenPreparationInfoLoadInProgress - extends AlarmScreenPreparationInfoState {} - -class AlarmScreenPreparationLoadSuccess - extends AlarmScreenPreparationInfoState { - final List preparationSteps; - final ScheduleEntity schedule; - - const AlarmScreenPreparationLoadSuccess({ - required this.preparationSteps, - required this.schedule, - }); - - /// 지금부터 몇분 뒤에 나가야하는지에 대한 시간. alarm screen 최상단에서 표시. - int get beforeOutTime { - final DateTime now = DateTime.now(); - final Duration spareTime = schedule.scheduleSpareTime!; - final DateTime scheduleTime = schedule.scheduleTime; - final Duration moveTime = schedule.moveTime; - final Duration remainingDuration = - scheduleTime.difference(now) - moveTime - spareTime; - return remainingDuration.inSeconds; - } - - /// 지각 여부 - bool get isLate => beforeOutTime < 0; - - AlarmScreenPreparationLoadSuccess copyWith({ - List? preparationSteps, - int? currentIndex, - ScheduleEntity? schedule, - }) { - return AlarmScreenPreparationLoadSuccess( - preparationSteps: preparationSteps ?? this.preparationSteps, - schedule: schedule ?? this.schedule, - ); - } - - @override - List get props => [ - preparationSteps, - ]; -} - -class AlarmScreenPreparationLoadFailure - extends AlarmScreenPreparationInfoState { - final String errorMessage; - const AlarmScreenPreparationLoadFailure(this.errorMessage); - - @override - List get props => [errorMessage]; -} diff --git a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart b/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart deleted file mode 100644 index 864fd448..00000000 --- a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart +++ /dev/null @@ -1,282 +0,0 @@ -library; - -import 'dart:async'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:injectable/injectable.dart'; -import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; -import 'package:on_time_front/presentation/shared/constants/constants.dart'; - -part 'alarm_timer_event.dart'; -part 'alarm_timer_state.dart'; - -@injectable -class AlarmTimerBloc extends Bloc { - StreamSubscription? _tickerSubscription; - - // 타이머 시작 기준 시각 - DateTime? _stepStartTime; - DateTime? _beforeOutStartTime; - - final int _initialBeforeOutTime; - - AlarmTimerBloc({ - required List preparationSteps, - required int beforeOutTime, - required bool isLate, - }) : _initialBeforeOutTime = beforeOutTime, - super(AlarmTimerInitial( - preparationSteps: preparationSteps, - currentStepIndex: 0, - stepElapsedTimes: List.generate(preparationSteps.length, (_) => 0), - preparationStepStates: List.generate( - preparationSteps.length, (_) => PreparationStateEnum.yet), - preparationRemainingTime: - preparationSteps.first.preparationTime.inSeconds, - totalRemainingTime: preparationSteps.fold( - 0, (sum, step) => sum + step.preparationTime.inSeconds), - totalPreparationTime: preparationSteps.fold( - 0, (sum, step) => sum + step.preparationTime.inSeconds), - progress: 0.0, - beforeOutTime: beforeOutTime, - isLate: isLate, - )) { - on(_onStepStarted); - on(_onStepTicked); - on(_onStepSkipped); - on(_onStepNext); - on(_onStepFinalized); - on(_onStepUpdated); - on(_onPreparationsTimeOvered); - } - - void _onStepUpdated( - AlarmTimerStepsUpdated event, Emitter emit) { - emit(state.copyWith( - preparationSteps: event.preparationSteps, - )); - - if (event.preparationSteps.isNotEmpty) { - add(AlarmTimerStepStarted( - event.preparationSteps.first.preparationTime.inSeconds)); - } - } - - void _onStepStarted( - AlarmTimerStepStarted event, Emitter emit) { - // 실제 시작 시간 저장 - _stepStartTime = DateTime.now(); - // 최초 기록 - _beforeOutStartTime ??= DateTime.now(); - - final updatedStates = - List.from(state.preparationStepStates); - updatedStates[state.currentStepIndex] = PreparationStateEnum.now; - - final updatedBeforeOutTime = state.beforeOutTime; - final updatedIsLate = updatedBeforeOutTime <= 0; - - emit(state.copyWith( - preparationStepStates: updatedStates, - preparationRemainingTime: event.duration, - stepElapsedTimes: List.from(state.stepElapsedTimes), - beforeOutTime: updatedBeforeOutTime, - isLate: updatedIsLate, - )); - - _startTicker(event.duration, emit); - } - - void _onStepTicked( - AlarmTimerStepTicked event, Emitter emit) { - final updatedStepElapsedTimes = List.from(state.stepElapsedTimes); - updatedStepElapsedTimes[state.currentStepIndex] = - event.preparationElapsedTime; - - final updatedTotalRemaining = state.totalRemainingTime - 1; - - final double updatedProgress = - 1.0 - (updatedTotalRemaining / state.totalPreparationTime); - - if (event.preparationRemainingTime > 0) { - emit(state.copyWith( - preparationRemainingTime: event.preparationRemainingTime, - stepElapsedTimes: updatedStepElapsedTimes, - totalRemainingTime: updatedTotalRemaining, - progress: updatedProgress, - beforeOutTime: event.beforeOutTime, - isLate: event.isLate, - )); - } else { - add(const AlarmTimerStepNextShifted()); - } - } - - // 타이머 시작 메서드 - void _startTicker(int duration, Emitter emit) { - _tickerSubscription?.cancel(); - - if (_stepStartTime == null || _beforeOutStartTime == null) { - debugPrint("_stepStartTime or _beforeOutStartTime is null"); - return; - } - - _tickerSubscription = - Stream.periodic(const Duration(seconds: 1), (tick) => tick) - .listen((_) { - if (isClosed) return; - - final now = DateTime.now(); - final elapsed = now.difference(_stepStartTime!).inSeconds; - final newRemaining = duration - elapsed; - final totalRemaining = state.totalPreparationTime - elapsed; - - final beforeOutElapsed = now.difference(_beforeOutStartTime!).inSeconds; - final updatedBeforeOutTime = _initialBeforeOutTime - beforeOutElapsed; - final updatedIsLate = updatedBeforeOutTime <= 0; - - if (newRemaining >= 0) { - debugPrint("타이머 tick: $newRemaining초 남음"); - add(AlarmTimerStepTicked( - preparationRemainingTime: newRemaining, - preparationElapsedTime: elapsed, - totalRemainingTime: totalRemaining, - beforeOutTime: updatedBeforeOutTime, - isLate: updatedIsLate, - )); - } else { - debugPrint("타이머 완료됨"); - add(const AlarmTimerStepNextShifted()); - } - }); - } - - void _onStepSkipped( - AlarmTimerStepSkipped event, Emitter emit) { - final updatedStates = - List.from(state.preparationStepStates); - updatedStates[state.currentStepIndex] = PreparationStateEnum.done; - - final updatedRemainingTime = - state.totalRemainingTime - state.preparationRemainingTime; - - final updatedProgress = - 1.0 - (updatedRemainingTime / state.totalPreparationTime); - - _tickerSubscription?.cancel(); - emit(state.copyWith( - preparationStepStates: updatedStates, - totalRemainingTime: updatedRemainingTime, - progress: updatedProgress, - )); - - add(const AlarmTimerStepNextShifted()); - } - - void _onStepNext( - AlarmTimerStepNextShifted event, Emitter emit) { - _tickerSubscription?.cancel(); - - final isLastStep = - state.currentStepIndex + 1 >= state.preparationSteps.length; - - if (!isLastStep) { - final nextStepIndex = state.currentStepIndex + 1; - final nextStepDuration = - state.preparationSteps[nextStepIndex].preparationTime.inSeconds; - - final updatedStepStates = - List.from(state.preparationStepStates); - - updatedStepStates[state.currentStepIndex] = PreparationStateEnum.done; - updatedStepStates[nextStepIndex] = PreparationStateEnum.now; - - emit(state.copyWith( - currentStepIndex: nextStepIndex, - preparationStepStates: updatedStepStates, - preparationRemainingTime: nextStepDuration, - )); - - add(AlarmTimerStepStarted(nextStepDuration)); - } else { - final wasSkipped = state.preparationStepStates[state.currentStepIndex] == - PreparationStateEnum.done; - if (wasSkipped) { - add(const AlarmTimerStepFinalized()); - } else { - add(const AlarmTimerPreparationsTimeOvered()); - } - } - } - - Future _onStepFinalized( - AlarmTimerStepFinalized event, - Emitter emit, - ) async { - await _tickerSubscription?.cancel(); - - if (!emit.isDone) { - emit(state.copyWith(progress: 1.0)); - } - - await Future.delayed(const Duration(milliseconds: 500)); - - if (!emit.isDone) { - emit(AlarmTimerPreparationCompletion( - preparationSteps: state.preparationSteps, - currentStepIndex: state.currentStepIndex, - stepElapsedTimes: state.stepElapsedTimes, - preparationStepStates: state.preparationStepStates, - preparationRemainingTime: 0, - totalRemainingTime: 0, - totalPreparationTime: state.totalPreparationTime, - progress: 1.0, - beforeOutTime: state.beforeOutTime, - isLate: state.isLate, - )); - } - } - - @override - Future close() { - _tickerSubscription?.cancel(); - return super.close(); - } - - void _onPreparationsTimeOvered( - AlarmTimerPreparationsTimeOvered event, - Emitter emit, - ) { - final updatedStates = - List.from(state.preparationStepStates); - updatedStates[state.currentStepIndex] = PreparationStateEnum.done; - - emit(AlarmTimerPreparationsTimeOver( - preparationSteps: state.preparationSteps, - currentStepIndex: state.currentStepIndex, - stepElapsedTimes: state.stepElapsedTimes, - preparationStepStates: updatedStates, - preparationRemainingTime: 0, - totalRemainingTime: 0, - totalPreparationTime: state.totalPreparationTime, - progress: 1.0, - beforeOutTime: state.beforeOutTime, - isLate: state.isLate, - )); - - _tickerSubscription?.cancel(); - _tickerSubscription = - Stream.periodic(const Duration(seconds: 1), (x) => x).listen((tick) { - final updatedBeforeOutTime = state.beforeOutTime - 1; - final updatedIsLate = updatedBeforeOutTime <= 0; - - emit( - (state as AlarmTimerPreparationsTimeOver).copyWith( - beforeOutTime: updatedBeforeOutTime, - isLate: updatedIsLate, - ), - ); - }); - } -} diff --git a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_event.dart b/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_event.dart deleted file mode 100644 index 3d2a9f6a..00000000 --- a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_event.dart +++ /dev/null @@ -1,66 +0,0 @@ -part of 'alarm_timer_bloc.dart'; - -abstract class AlarmTimerEvent extends Equatable { - const AlarmTimerEvent(); - - @override - List get props => []; -} - -class AlarmTimerStepStarted extends AlarmTimerEvent { - final int duration; - const AlarmTimerStepStarted(this.duration); - - @override - List get props => [duration]; -} - -class AlarmTimerStepTicked extends AlarmTimerEvent { - final int preparationRemainingTime; // 남은 시간 - final int preparationElapsedTime; // 경과 시간 - final int totalRemainingTime; - final int beforeOutTime; // 나가기 전까지 남은 시간 - final bool isLate; // 지각 여부 - - const AlarmTimerStepTicked({ - required this.preparationRemainingTime, - required this.preparationElapsedTime, - required this.totalRemainingTime, - required this.beforeOutTime, - required this.isLate, - }); - - @override - List get props => [ - preparationRemainingTime, - preparationElapsedTime, - totalRemainingTime, - beforeOutTime, - isLate, - ]; -} - -class AlarmTimerStepSkipped extends AlarmTimerEvent { - const AlarmTimerStepSkipped(); -} - -class AlarmTimerStepNextShifted extends AlarmTimerEvent { - const AlarmTimerStepNextShifted(); -} - -class AlarmTimerStepFinalized extends AlarmTimerEvent { - const AlarmTimerStepFinalized(); -} - -class AlarmTimerStepsUpdated extends AlarmTimerEvent { - final List preparationSteps; - - const AlarmTimerStepsUpdated(this.preparationSteps); - - @override - List get props => [preparationSteps]; -} - -class AlarmTimerPreparationsTimeOvered extends AlarmTimerEvent { - const AlarmTimerPreparationsTimeOvered(); -} diff --git a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_state.dart b/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_state.dart deleted file mode 100644 index df770021..00000000 --- a/lib/presentation/alarm/bloc/alarm_timer/alarm_timer_state.dart +++ /dev/null @@ -1,285 +0,0 @@ -part of 'alarm_timer_bloc.dart'; - -sealed class AlarmTimerState extends Equatable { - final List preparationSteps; - final int currentStepIndex; - final List stepElapsedTimes; - final List preparationStepStates; - final int preparationRemainingTime; - final int totalRemainingTime; - final int totalPreparationTime; - - final double progress; - final int beforeOutTime; - final bool isLate; - - const AlarmTimerState({ - required this.preparationSteps, - required this.currentStepIndex, - required this.stepElapsedTimes, - required this.preparationStepStates, - required this.preparationRemainingTime, - required this.totalRemainingTime, - required this.totalPreparationTime, - required this.progress, - required this.beforeOutTime, - required this.isLate, - }); - - @override - List get props => [ - preparationSteps, - currentStepIndex, - stepElapsedTimes, - preparationStepStates, - preparationRemainingTime, - totalRemainingTime, - totalPreparationTime, - progress, - beforeOutTime, - isLate, - ]; - - AlarmTimerState copyWith({ - List? preparationSteps, - int? currentStepIndex, - List? stepElapsedTimes, - List? preparationStepStates, - int? preparationRemainingTime, - int? totalRemainingTime, - int? totalPreparationTime, - double? progress, - int beforeOutTime, - bool isLate, - }); -} - -/// 초기 상태 -class AlarmTimerInitial extends AlarmTimerState { - const AlarmTimerInitial({ - required super.preparationSteps, - required super.currentStepIndex, - required super.stepElapsedTimes, - required super.preparationStepStates, - required super.preparationRemainingTime, - required super.totalRemainingTime, - required super.totalPreparationTime, - required super.progress, - required super.beforeOutTime, - required super.isLate, - }); - - @override - AlarmTimerInitial copyWith({ - List? preparationSteps, - int? currentStepIndex, - List? stepElapsedTimes, - List? preparationStepStates, - int? preparationRemainingTime, - int? totalRemainingTime, - int? totalPreparationTime, - double? progress, - int? beforeOutTime, - bool? isLate, - }) { - return AlarmTimerInitial( - preparationSteps: preparationSteps ?? this.preparationSteps, - currentStepIndex: currentStepIndex ?? this.currentStepIndex, - stepElapsedTimes: stepElapsedTimes ?? this.stepElapsedTimes, - preparationStepStates: - preparationStepStates ?? this.preparationStepStates, - preparationRemainingTime: - preparationRemainingTime ?? this.preparationRemainingTime, - totalRemainingTime: totalRemainingTime ?? this.totalRemainingTime, - totalPreparationTime: totalPreparationTime ?? this.totalPreparationTime, - progress: progress ?? this.progress, - beforeOutTime: beforeOutTime ?? this.beforeOutTime, - isLate: isLate ?? this.isLate, - ); - } -} - -/// 진행 중 상태 -class AlarmTimerRunInProgress extends AlarmTimerState { - const AlarmTimerRunInProgress({ - required super.preparationSteps, - required super.currentStepIndex, - required super.stepElapsedTimes, - required super.preparationStepStates, - required super.preparationRemainingTime, - required super.totalRemainingTime, - required super.totalPreparationTime, - required super.progress, - required super.beforeOutTime, - required super.isLate, - }); - - @override - AlarmTimerRunInProgress copyWith({ - List? preparationSteps, - int? currentStepIndex, - List? stepElapsedTimes, - List? preparationStepStates, - int? preparationRemainingTime, - int? totalRemainingTime, - int? totalPreparationTime, - double? progress, - int? beforeOutTime, - bool? isLate, - }) { - return AlarmTimerRunInProgress( - preparationSteps: preparationSteps ?? this.preparationSteps, - currentStepIndex: currentStepIndex ?? this.currentStepIndex, - stepElapsedTimes: stepElapsedTimes ?? this.stepElapsedTimes, - preparationStepStates: - preparationStepStates ?? this.preparationStepStates, - preparationRemainingTime: - preparationRemainingTime ?? this.preparationRemainingTime, - totalRemainingTime: totalRemainingTime ?? this.totalRemainingTime, - totalPreparationTime: totalPreparationTime ?? this.totalPreparationTime, - progress: progress ?? this.progress, - beforeOutTime: beforeOutTime ?? this.beforeOutTime, - isLate: isLate ?? this.isLate, - ); - } -} - -/// 준비 단계 완료 상태 -class AlarmTimerPreparationStepCompletion extends AlarmTimerState { - final int completedStepIndex; - - const AlarmTimerPreparationStepCompletion({ - required this.completedStepIndex, - required super.preparationSteps, - required super.currentStepIndex, - required super.stepElapsedTimes, - required super.preparationStepStates, - required super.preparationRemainingTime, - required super.totalRemainingTime, - required super.totalPreparationTime, - required super.progress, - required super.beforeOutTime, - required super.isLate, - }); - - @override - AlarmTimerPreparationStepCompletion copyWith({ - List? preparationSteps, - int? currentStepIndex, - List? stepElapsedTimes, - List? preparationStepStates, - int? preparationRemainingTime, - int? totalRemainingTime, - int? totalPreparationTime, - double? progress, - int? beforeOutTime, - bool? isLate, - }) { - return AlarmTimerPreparationStepCompletion( - completedStepIndex: completedStepIndex, - preparationSteps: preparationSteps ?? this.preparationSteps, - currentStepIndex: currentStepIndex ?? this.currentStepIndex, - stepElapsedTimes: stepElapsedTimes ?? this.stepElapsedTimes, - preparationStepStates: - preparationStepStates ?? this.preparationStepStates, - preparationRemainingTime: - preparationRemainingTime ?? this.preparationRemainingTime, - totalRemainingTime: totalRemainingTime ?? this.totalRemainingTime, - totalPreparationTime: totalPreparationTime ?? this.totalPreparationTime, - progress: progress ?? this.progress, - beforeOutTime: beforeOutTime ?? this.beforeOutTime, - isLate: isLate ?? this.isLate, - ); - } -} - -/// 준비 종료 상태 -class AlarmTimerPreparationCompletion extends AlarmTimerState { - const AlarmTimerPreparationCompletion({ - required super.preparationSteps, - required super.currentStepIndex, - required super.stepElapsedTimes, - required super.preparationStepStates, - required super.preparationRemainingTime, - required super.totalRemainingTime, - required super.totalPreparationTime, - required super.progress, - required super.beforeOutTime, - required super.isLate, - }); - -// 준비 완료 상태 - @override - AlarmTimerPreparationCompletion copyWith({ - List? preparationSteps, - int? currentStepIndex, - List? stepElapsedTimes, - List? preparationStepStates, - int? preparationRemainingTime, - int? totalRemainingTime, - int? totalPreparationTime, - double? progress, - int? beforeOutTime, - bool? isLate, - }) { - return AlarmTimerPreparationCompletion( - preparationSteps: preparationSteps ?? this.preparationSteps, - currentStepIndex: currentStepIndex ?? this.currentStepIndex, - stepElapsedTimes: stepElapsedTimes ?? this.stepElapsedTimes, - preparationStepStates: - preparationStepStates ?? this.preparationStepStates, - preparationRemainingTime: - preparationRemainingTime ?? this.preparationRemainingTime, - totalRemainingTime: totalRemainingTime ?? this.totalRemainingTime, - totalPreparationTime: totalPreparationTime ?? this.totalPreparationTime, - progress: progress ?? this.progress, - beforeOutTime: beforeOutTime ?? this.beforeOutTime, - isLate: isLate ?? this.isLate, - ); - } -} - -// 준비 시간 만료 상태 -class AlarmTimerPreparationsTimeOver extends AlarmTimerState { - const AlarmTimerPreparationsTimeOver({ - required super.preparationSteps, - required super.currentStepIndex, - required super.stepElapsedTimes, - required super.preparationStepStates, - required super.preparationRemainingTime, - required super.totalRemainingTime, - required super.totalPreparationTime, - required super.progress, - required super.beforeOutTime, - required super.isLate, - }); - - @override - AlarmTimerPreparationsTimeOver copyWith({ - List? preparationSteps, - int? currentStepIndex, - List? stepElapsedTimes, - List? preparationStepStates, - int? preparationRemainingTime, - int? totalRemainingTime, - int? totalPreparationTime, - double? progress, - int? beforeOutTime, - bool? isLate, - }) { - return AlarmTimerPreparationsTimeOver( - preparationSteps: preparationSteps ?? this.preparationSteps, - currentStepIndex: currentStepIndex ?? this.currentStepIndex, - stepElapsedTimes: stepElapsedTimes ?? this.stepElapsedTimes, - preparationStepStates: - preparationStepStates ?? this.preparationStepStates, - preparationRemainingTime: - preparationRemainingTime ?? this.preparationRemainingTime, - totalRemainingTime: totalRemainingTime ?? this.totalRemainingTime, - totalPreparationTime: totalPreparationTime ?? this.totalPreparationTime, - progress: progress ?? this.progress, - beforeOutTime: beforeOutTime ?? this.beforeOutTime, - isLate: isLate ?? this.isLate, - ); - } -} diff --git a/lib/presentation/alarm/components/alarm_screen_bottom_section.dart b/lib/presentation/alarm/components/alarm_screen_bottom_section.dart index dca36aa7..2909762e 100644 --- a/lib/presentation/alarm/components/alarm_screen_bottom_section.dart +++ b/lib/presentation/alarm/components/alarm_screen_bottom_section.dart @@ -1,39 +1,35 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; -import 'package:on_time_front/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart'; +import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; import 'package:on_time_front/presentation/alarm/components/preparation_step_list_widget.dart'; import 'package:on_time_front/presentation/shared/constants/constants.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; class AlarmScreenBottomSection extends StatelessWidget { - final List preparationSteps; - final int currentStepIndex; - final List stepElapsedTimes; - final List preparationStepStates; + final PreparationWithTimeEntity preparation; final VoidCallback onSkip; final VoidCallback onEndPreparation; const AlarmScreenBottomSection({ super.key, - required this.preparationSteps, - required this.currentStepIndex, - required this.stepElapsedTimes, - required this.preparationStepStates, + required this.preparation, required this.onSkip, required this.onEndPreparation, }); @override Widget build(BuildContext context) { + final steps = + List.from(preparation.preparationStepList); + return Column( children: [ Expanded( child: _PreparationStepListSection( - preparationSteps: preparationSteps, - currentStepIndex: currentStepIndex, - stepElapsedTimes: stepElapsedTimes, - preparationStepStates: preparationStepStates, + preparationSteps: steps, + currentStepIndex: preparation.resolvedCurrentStepIndex, + stepElapsedTimes: preparation.stepElapsedTimesInSeconds, + preparationStepStates: preparation.preparationStepStates, onSkip: onSkip, )), _EndPreparationButtonSection( @@ -60,35 +56,31 @@ class _PreparationStepListSection extends StatelessWidget { }); @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, timerState) { - return Stack( - children: [ - Container( - decoration: const BoxDecoration( - color: Color(0xffF6F6F6), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), - ), - ), - ), - Positioned( - top: 15, - bottom: 0, - left: MediaQuery.of(context).size.width * 0.06, - right: MediaQuery.of(context).size.width * 0.06, - child: PreparationStepListWidget( - preparationSteps: preparationSteps, - currentStepIndex: currentStepIndex, - stepElapsedTimes: stepElapsedTimes, - preparationStepStates: preparationStepStates, - onSkip: onSkip, - ), + return Stack( + children: [ + Container( + decoration: const BoxDecoration( + color: Color(0xffF6F6F6), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), ), - ], - ); - }, + ), + ), + Positioned( + top: 15, + bottom: 0, + left: MediaQuery.of(context).size.width * 0.06, + right: MediaQuery.of(context).size.width * 0.06, + child: PreparationStepListWidget( + preparationSteps: preparationSteps, + currentStepIndex: currentStepIndex, + stepElapsedTimes: stepElapsedTimes, + preparationStepStates: preparationStepStates, + onSkip: onSkip, + ), + ), + ], ); } } diff --git a/lib/presentation/alarm/components/preparation_completion_dialog.dart b/lib/presentation/alarm/components/preparation_completion_dialog.dart new file mode 100644 index 00000000..632395a9 --- /dev/null +++ b/lib/presentation/alarm/components/preparation_completion_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:on_time_front/presentation/shared/components/custom_alert_dialog.dart'; +import 'package:on_time_front/presentation/shared/components/modal_button.dart'; +import 'package:on_time_front/l10n/app_localizations.dart'; + +Future showPreparationCompletionDialog({ + required BuildContext context, + required VoidCallback onFinish, +}) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return CustomAlertDialog( + title: Text( + AppLocalizations.of(dialogContext)!.areYouRunningLate, + ), + content: Text( + AppLocalizations.of(dialogContext)!.runningLateDescription, + ), + actionsAlignment: MainAxisAlignment.spaceBetween, + actions: [ + ModalButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + }, + text: AppLocalizations.of(dialogContext)!.continuePreparing, + color: Theme.of(dialogContext).colorScheme.primaryContainer, + textColor: Theme.of(dialogContext).colorScheme.primary, + ), + ModalButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + onFinish(); + }, + text: AppLocalizations.of(dialogContext)!.finishPreparation, + color: Theme.of(dialogContext).colorScheme.primary, + textColor: Theme.of(dialogContext).colorScheme.surface, + ), + ], + ); + }, + ); +} diff --git a/lib/presentation/alarm/screens/alarm_screen.dart b/lib/presentation/alarm/screens/alarm_screen.dart index 40dfaced..7daa04b0 100644 --- a/lib/presentation/alarm/screens/alarm_screen.dart +++ b/lib/presentation/alarm/screens/alarm_screen.dart @@ -1,194 +1,117 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:on_time_front/core/di/di_setup.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; -import 'package:on_time_front/presentation/alarm/bloc/alarm_screen_preparation_info/alarm_screen_preparation_info_bloc.dart'; -import 'package:on_time_front/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart'; import 'package:on_time_front/presentation/alarm/components/alarm_screen_bottom_section.dart'; import 'package:on_time_front/presentation/alarm/components/alarm_screen_top_section.dart'; -import 'package:on_time_front/presentation/shared/components/custom_alert_dialog.dart'; -import 'package:on_time_front/presentation/shared/components/modal_button.dart'; -import 'package:on_time_front/l10n/app_localizations.dart'; +import 'package:on_time_front/presentation/alarm/components/preparation_completion_dialog.dart'; class AlarmScreen extends StatefulWidget { - final ScheduleEntity schedule; - - const AlarmScreen({super.key, required this.schedule}); + const AlarmScreen({super.key}); @override State createState() => _AlarmScreenState(); } class _AlarmScreenState extends State { - bool _isModalVisible = false; - - void _showModal() => setState(() => _isModalVisible = true); - void _hideModal() => setState(() => _isModalVisible = false); - - void _navigateToEarlyLate(BuildContext context, AlarmTimerState timerState) { + bool _hasShownCompletionDialog = false; + void _onPreparationFinished( + BuildContext context, Duration timeRemainingBeforeLeaving, bool isLate) { context.go( '/earlyLate', extra: { - 'earlyLateTime': timerState.beforeOutTime, - 'isLate': timerState.isLate, + 'earlyLateTime': timeRemainingBeforeLeaving.inSeconds, + 'isLate': isLate, }, ); } + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + context.read().add(const ScheduleSubscriptionRequested()); + } + }); + } + @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt.get() - ..add( - AlarmScreenPreparationSubscriptionRequested( - scheduleId: widget.schedule.id, - schedule: widget.schedule, - ), - ), - child: BlocBuilder( - builder: (context, infoState) { - if (infoState is AlarmScreenPreparationLoadSuccess) { - return BlocProvider( - create: (context) => AlarmTimerBloc( - preparationSteps: infoState.preparationSteps, - beforeOutTime: infoState.beforeOutTime, - isLate: infoState.isLate, - )..add( - AlarmTimerStepStarted( - infoState.preparationSteps.first.preparationTime.inSeconds, - ), - ), - child: _buildAlarmScreen(infoState), - ); + return BlocBuilder( + builder: (context, scheduleState) { + if (scheduleState.status == ScheduleStatus.ongoing || + scheduleState.status == ScheduleStatus.started) { + final schedule = scheduleState.schedule!; + final preparation = schedule.preparation; + + if (preparation.isAllStepsDone && !_hasShownCompletionDialog) { + _hasShownCompletionDialog = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + showPreparationCompletionDialog( + context: context, + onFinish: () { + _onPreparationFinished( + context, + schedule.timeRemainingBeforeLeaving, + schedule.isLate, + ); + }, + ); + }); } + + return _buildAlarmScreen( + schedule: schedule, + ); + } else { return const Scaffold( backgroundColor: Color(0xff5C79FB), body: Center(child: CircularProgressIndicator()), ); - }, - ), - ); - } - - Widget _buildAlarmScreen(AlarmScreenPreparationLoadSuccess infoState) { - return BlocListener( - listener: (context, timerState) { - if (timerState is AlarmTimerPreparationCompletion) { - // 수동 종료 or 건너뛰기로 준비 완료 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - _navigateToEarlyLate(context, timerState); - } - }); - } else if (timerState is AlarmTimerPreparationsTimeOver) { - // 시간이 다 되어서 종료 → 모달 띄우기 - _showModal(); } }, - child: Scaffold( - backgroundColor: const Color(0xff5C79FB), - body: BlocBuilder( - builder: (context, timerState) { - final isLate = timerState.isLate; - final beforeOutTime = timerState.beforeOutTime; - final preparationName = timerState - .preparationSteps[timerState.currentStepIndex].preparationName; - final preparationRemainingTime = - timerState.preparationRemainingTime; - final progress = timerState.progress; - - return Stack( - children: [ - Column( - children: [ - AlarmScreenTopSection( - isLate: isLate, - beforeOutTime: beforeOutTime, - preparationName: preparationName, - preparationRemainingTime: preparationRemainingTime, - progress: progress, - ), - const SizedBox(height: 110), - Expanded( - child: AlarmScreenBottomSection( - preparationSteps: timerState.preparationSteps, - currentStepIndex: timerState.currentStepIndex, - stepElapsedTimes: timerState.stepElapsedTimes, - preparationStepStates: timerState.preparationStepStates, - onSkip: () => context - .read() - .add(const AlarmTimerStepSkipped()), - onEndPreparation: () => context - .read() - .add(const AlarmTimerStepFinalized()), - ), - ), - ], - ), - if (_isModalVisible) - // 모달 표시 - TimeoutModalSection( - onContinue: _hideModal, - onFinish: () => - _navigateToEarlyLate(context, timerState)), - ], - ); - }, - ), - ), ); } -} - -class TimeoutModalSection extends StatelessWidget { - final VoidCallback onContinue; - final VoidCallback onFinish; - const TimeoutModalSection({ - super.key, - required this.onContinue, - required this.onFinish, - }); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Positioned.fill( - // 배경 딤 처리 - child: Container( - color: Colors.black.withValues(alpha: 0.4), // 딤 처리 - ), - ), - Center( - child: CustomAlertDialog( - title: Text( - AppLocalizations.of(context)!.areYouRunningLate, - ), - content: Text( - AppLocalizations.of(context)!.runningLateDescription, - ), - actionsAlignment: MainAxisAlignment.spaceBetween, - actions: [ - ModalButton( - onPressed: onContinue, - text: AppLocalizations.of(context)!.continuePreparing, - color: Theme.of(context).colorScheme.primaryContainer, - textColor: Theme.of(context).colorScheme.primary, + Widget _buildAlarmScreen({ + required ScheduleWithPreparationEntity schedule, + }) { + final preparation = schedule.preparation; + return Scaffold( + backgroundColor: const Color(0xff5C79FB), + body: Stack( + children: [ + Column( + children: [ + AlarmScreenTopSection( + isLate: schedule.isLate, + beforeOutTime: schedule.timeRemainingBeforeLeaving.inSeconds, + preparationName: preparation.currentStepName, + preparationRemainingTime: + preparation.currentStepRemainingTime.inSeconds, + progress: preparation.progress, ), - ModalButton( - onPressed: onFinish, - text: AppLocalizations.of(context)!.finishPreparation, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.surface, + const SizedBox(height: 110), + Expanded( + child: AlarmScreenBottomSection( + preparation: preparation, + onSkip: () { + context + .read() + .add(const ScheduleStepSkipped()); + }, + onEndPreparation: () => _onPreparationFinished(context, + schedule.timeRemainingBeforeLeaving, schedule.isLate), + ), ), ], ), - ), - ], + ], + ), ); } } diff --git a/lib/presentation/alarm/screens/schedule_start_screen.dart b/lib/presentation/alarm/screens/schedule_start_screen.dart index d1beb60f..832311f7 100644 --- a/lib/presentation/alarm/screens/schedule_start_screen.dart +++ b/lib/presentation/alarm/screens/schedule_start_screen.dart @@ -2,17 +2,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:on_time_front/domain/entities/schedule_entity.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/shared/components/custom_alert_dialog.dart'; import 'package:on_time_front/presentation/shared/components/modal_button.dart'; class ScheduleStartScreen extends StatefulWidget { - final ScheduleEntity schedule; - const ScheduleStartScreen({ super.key, - required this.schedule, }); @override @@ -45,7 +43,12 @@ class _ScheduleStartScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - widget.schedule.scheduleName, + context + .read() + .state + .schedule + ?.scheduleName ?? + '', style: const TextStyle( fontSize: 40, fontWeight: FontWeight.bold, @@ -54,7 +57,13 @@ class _ScheduleStartScreenState extends State { ), const SizedBox(height: 8), Text( - widget.schedule.place.placeName, + context + .read() + .state + .schedule + ?.place + .placeName ?? + '', style: const TextStyle( fontSize: 25, fontWeight: FontWeight.bold, @@ -87,7 +96,7 @@ class _ScheduleStartScreenState extends State { padding: const EdgeInsets.only(bottom: 30), child: ElevatedButton( onPressed: () async { - context.go('/alarmScreen', extra: widget.schedule); + context.go('/alarmScreen'); }, child: Text(AppLocalizations.of(context)!.startPreparing), ), diff --git a/lib/presentation/app/bloc/auth/auth_bloc.dart b/lib/presentation/app/bloc/auth/auth_bloc.dart index 4605f430..af8a2cdd 100644 --- a/lib/presentation/app/bloc/auth/auth_bloc.dart +++ b/lib/presentation/app/bloc/auth/auth_bloc.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; -import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; import 'package:on_time_front/domain/entities/user_entity.dart'; import 'package:on_time_front/domain/use-cases/load_user_use_case.dart'; import 'package:on_time_front/domain/use-cases/sign_out_use_case.dart'; @@ -27,8 +26,6 @@ class AuthBloc extends Bloc { final SignOutUseCase _signOutUseCase; final ScheduleBloc _scheduleBloc; Timer? _timer; - StreamSubscription? - _upcomingScheduleSubscription; Future _appUserSubscriptionRequested( AuthUserSubscriptionRequested event, @@ -68,7 +65,6 @@ class AuthBloc extends Bloc { @override Future close() { _timer?.cancel(); - _upcomingScheduleSubscription?.cancel(); return super.close(); } } diff --git a/lib/presentation/app/bloc/auth/auth_state.dart b/lib/presentation/app/bloc/auth/auth_state.dart index 98f3eebd..1a71f1ee 100644 --- a/lib/presentation/app/bloc/auth/auth_state.dart +++ b/lib/presentation/app/bloc/auth/auth_state.dart @@ -18,23 +18,21 @@ class AuthState extends Equatable { user: user, ); - const AuthState._( - {required this.status, - this.user = const UserEntity.empty(), - this.schedule}); + const AuthState._({ + required this.status, + this.user = const UserEntity.empty(), + }); final AuthStatus status; final UserEntity user; - final ScheduleWithPreparationEntity? schedule; - AuthState copyWith( - {AuthStatus? status, - UserEntity? user, - ScheduleWithPreparationEntity? schedule}) { + AuthState copyWith({ + AuthStatus? status, + UserEntity? user, + }) { return AuthState._( status: status ?? this.status, user: user ?? this.user, - schedule: schedule ?? this.schedule, ); } diff --git a/lib/presentation/app/bloc/schedule/schedule_bloc.dart b/lib/presentation/app/bloc/schedule/schedule_bloc.dart index f1ed80c1..6ad49815 100644 --- a/lib/presentation/app/bloc/schedule/schedule_bloc.dart +++ b/lib/presentation/app/bloc/schedule/schedule_bloc.dart @@ -5,41 +5,43 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:on_time_front/core/services/navigation_service.dart'; -import 'package:on_time_front/domain/entities/preparation_entity.dart'; -import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart'; import 'package:on_time_front/domain/use-cases/get_nearest_upcoming_schedule_use_case.dart'; +import 'package:on_time_front/domain/use-cases/save_timed_preparation_use_case.dart'; part 'schedule_event.dart'; part 'schedule_state.dart'; @Singleton() class ScheduleBloc extends Bloc { - ScheduleBloc(this._getNearestUpcomingScheduleUseCase, this._navigationService) + ScheduleBloc(this._getNearestUpcomingScheduleUseCase, this._navigationService, + this._saveTimedPreparationUseCase) : super(const ScheduleState.initial()) { on(_onSubscriptionRequested); on(_onUpcomingReceived); on(_onScheduleStarted); + on(_onTick); + on(_onStepSkipped); } final GetNearestUpcomingScheduleUseCase _getNearestUpcomingScheduleUseCase; final NavigationService _navigationService; + final SaveTimedPreparationUseCase _saveTimedPreparationUseCase; StreamSubscription? _upcomingScheduleSubscription; Timer? _scheduleStartTimer; String? _currentScheduleId; + Timer? _preparationTimer; Future _onSubscriptionRequested( ScheduleSubscriptionRequested event, Emitter emit) async { await _upcomingScheduleSubscription?.cancel(); + _upcomingScheduleSubscription = _getNearestUpcomingScheduleUseCase().listen((upcomingSchedule) { // ✅ Safety check: Only add events if bloc is still active if (!isClosed) { - final scheduleWithTimePreparation = upcomingSchedule != null - ? _convertToScheduleWithTimePreparation(upcomingSchedule) - : null; - add(ScheduleUpcomingReceived(scheduleWithTimePreparation)); + add(ScheduleUpcomingReceived(upcomingSchedule)); } }); } @@ -49,18 +51,16 @@ class ScheduleBloc extends Bloc { // Cancel any existing timer _scheduleStartTimer?.cancel(); _scheduleStartTimer = null; + if (event.upcomingSchedule == null || event.upcomingSchedule!.scheduleTime.isBefore(DateTime.now())) { emit(const ScheduleState.notExists()); _currentScheduleId = null; } else if (_isPreparationOnGoing(event.upcomingSchedule!)) { - final currentStep = _findCurrentPreparationStep( - event.upcomingSchedule!, - DateTime.now(), - ); - emit(ScheduleState.ongoing(event.upcomingSchedule!, currentStep)); + emit(ScheduleState.ongoing(event.upcomingSchedule!)); debugPrint( - 'ongoingSchedule: ${event.upcomingSchedule}, currentStep: $currentStep'); + 'ongoingSchedule: ${event.upcomingSchedule}, currentStep: ${event.upcomingSchedule!.preparation.currentStep}'); + _startPreparationTimer(); } else { emit(ScheduleState.upcoming(event.upcomingSchedule!)); debugPrint('upcomingSchedule: ${event.upcomingSchedule}'); @@ -76,24 +76,38 @@ class ScheduleBloc extends Bloc { // Mark the schedule as started by updating the state debugPrint('scheddle started: ${state.schedule}'); emit(ScheduleState.started(state.schedule!)); - _navigationService.push('/scheduleStart', extra: state.schedule); + _navigationService.push('/scheduleStart'); + _startPreparationTimer(); } } - void _startScheduleTimer(ScheduleWithPreparationEntity schedule) { - final now = DateTime.now(); - final preparationStartTime = schedule.preparationStartTime; + Future _onTick(ScheduleTick event, Emitter emit) async { + if (state.schedule == null) return; + final updatedPreparation = + state.schedule!.preparation.timeElapsed(event.elapsed); + debugPrint('elapsedTime: ${updatedPreparation.elapsedTime}'); - // If the target time is in the past or now, don't set a timer - if (preparationStartTime.isBefore(now) || - preparationStartTime.isAtSameMomentAs(now)) { - return; - } + final newSchedule = + ScheduleWithPreparationEntity.fromScheduleAndPreparationEntity( + state.schedule!, updatedPreparation); - final duration = preparationStartTime.difference(now); + emit(state.copyWith(schedule: newSchedule)); + } - debugPrint('duration: $duration'); + Future _onStepSkipped( + ScheduleStepSkipped event, Emitter emit) async { + if (state.schedule == null) return; + final updated = state.schedule!.preparation.skipCurrentStep(); + emit(state.copyWith( + schedule: + ScheduleWithPreparationEntity.fromScheduleAndPreparationEntity( + state.schedule!, updated))); + await _saveTimedPreparationUseCase(state.schedule!.id, updated); + } + void _startScheduleTimer(ScheduleWithPreparationEntity schedule) { + final duration = state.durationUntilPreparationStart; + if (duration == null) return; _scheduleStartTimer = Timer(duration, () { // Only add event if bloc is still active and schedule ID matches if (!isClosed && _currentScheduleId == schedule.id) { @@ -102,68 +116,33 @@ class ScheduleBloc extends Bloc { }); } + void _startPreparationTimer() { + if (state.schedule == null) return; + _preparationTimer?.cancel(); + final elapsedTimeAfterLastTick = + DateTime.now().difference(state.schedule!.preparationStartTime) - + state.schedule!.preparation.elapsedTime; + debugPrint('elapsedTimeAfterLastTick: $elapsedTimeAfterLastTick'); + add(ScheduleTick(elapsedTimeAfterLastTick)); + _preparationTimer = Timer.periodic(Duration(seconds: 1), (_) { + if (!isClosed) add(ScheduleTick(Duration(seconds: 1))); + }); + } + @override Future close() { // ✅ Proper cleanup: Cancel subscription and timer before closing _upcomingScheduleSubscription?.cancel(); _scheduleStartTimer?.cancel(); + _preparationTimer?.cancel(); return super.close(); } - bool _isPreparationOnGoing( - ScheduleWithPreparationEntity nearestUpcomingSchedule) { - return nearestUpcomingSchedule.preparationStartTime - .isBefore(DateTime.now()) && - nearestUpcomingSchedule.scheduleTime.isAfter(DateTime.now()); + bool _isPreparationOnGoing(ScheduleWithPreparationEntity schedule) { + final start = schedule.preparationStartTime; + return start.isBefore(DateTime.now()) && + schedule.scheduleTime.isAfter(DateTime.now()); } - PreparationStepEntity _findCurrentPreparationStep( - ScheduleWithPreparationEntity schedule, DateTime now) { - final List steps = schedule - .preparation.preparationStepList - .cast(); - - if (steps.isEmpty) { - throw StateError('Preparation steps are empty'); - } - - final DateTime preparationStartTime = schedule.preparationStartTime; - - // If called when not in preparation window, clamp to bounds - if (now.isBefore(preparationStartTime)) { - return steps.first; - } - - Duration elapsed = now.difference(preparationStartTime); - - for (final PreparationStepWithTime step in steps) { - if (elapsed < step.preparationTime) { - return step.copyWithElapsed(elapsed); - } - elapsed -= step.preparationTime; - } - - // If elapsed exceeds total preparation duration (e.g., during move/spare time), - // return the last preparation step as current by convention. - return steps.last.copyWithElapsed(steps.last.preparationTime); - } - - ScheduleWithPreparationEntity _convertToScheduleWithTimePreparation( - ScheduleWithPreparationEntity schedule) { - final preparationWithTime = PreparationWithTime( - preparationStepList: schedule.preparation.preparationStepList - .map((step) => PreparationStepWithTime( - id: step.id, - preparationName: step.preparationName, - preparationTime: step.preparationTime, - nextPreparationId: step.nextPreparationId, - )) - .toList(), - ); - - return ScheduleWithPreparationEntity.fromScheduleAndPreparationEntity( - schedule, - preparationWithTime, - ); - } + // Removed unused helper since we now split in the event } diff --git a/lib/presentation/app/bloc/schedule/schedule_event.dart b/lib/presentation/app/bloc/schedule/schedule_event.dart index 98544f5b..dabf9130 100644 --- a/lib/presentation/app/bloc/schedule/schedule_event.dart +++ b/lib/presentation/app/bloc/schedule/schedule_event.dart @@ -17,7 +17,9 @@ final class ScheduleSubscriptionRequested extends ScheduleEvent { final class ScheduleUpcomingReceived extends ScheduleEvent { final ScheduleWithPreparationEntity? upcomingSchedule; - const ScheduleUpcomingReceived(this.upcomingSchedule); + const ScheduleUpcomingReceived( + ScheduleWithPreparationEntity? upcomingScheduleWithPreparation) + : upcomingSchedule = upcomingScheduleWithPreparation; @override List get props => [upcomingSchedule]; @@ -36,3 +38,16 @@ final class SchedulePreparationStarted extends ScheduleEvent { @override List get props => []; } + +final class ScheduleTick extends ScheduleEvent { + final Duration elapsed; + + const ScheduleTick(this.elapsed); + + @override + List get props => [elapsed]; +} + +final class ScheduleStepSkipped extends ScheduleEvent { + const ScheduleStepSkipped(); +} diff --git a/lib/presentation/app/bloc/schedule/schedule_state.dart b/lib/presentation/app/bloc/schedule/schedule_state.dart index c9259d74..18c35c40 100644 --- a/lib/presentation/app/bloc/schedule/schedule_state.dart +++ b/lib/presentation/app/bloc/schedule/schedule_state.dart @@ -12,7 +12,6 @@ class ScheduleState extends Equatable { const ScheduleState._({ required this.status, this.schedule, - this.currentStep, }); const ScheduleState.initial() : this._(status: ScheduleStatus.initial); @@ -22,72 +21,33 @@ class ScheduleState extends Equatable { const ScheduleState.upcoming(ScheduleWithPreparationEntity schedule) : this._(status: ScheduleStatus.upcoming, schedule: schedule); - const ScheduleState.ongoing( - ScheduleWithPreparationEntity schedule, PreparationStepEntity currentStep) - : this._( - status: ScheduleStatus.ongoing, - schedule: schedule, - currentStep: currentStep); + const ScheduleState.ongoing(ScheduleWithPreparationEntity schedule) + : this._(status: ScheduleStatus.ongoing, schedule: schedule); const ScheduleState.started(ScheduleWithPreparationEntity schedule) : this._(status: ScheduleStatus.started, schedule: schedule); final ScheduleStatus status; final ScheduleWithPreparationEntity? schedule; - final PreparationStepEntity? currentStep; ScheduleState copyWith({ ScheduleStatus? status, ScheduleWithPreparationEntity? schedule, - PreparationStepEntity? currentStep, }) { return ScheduleState._( status: status ?? this.status, schedule: schedule ?? this.schedule, - currentStep: currentStep ?? this.currentStep, ); } - @override - List get props => [status, schedule]; -} - -class PreparationStepWithTime extends PreparationStepEntity - implements Equatable { - final Duration elapsedTime; - - const PreparationStepWithTime({ - required super.id, - required super.preparationName, - required super.preparationTime, - required super.nextPreparationId, - this.elapsedTime = Duration.zero, - }); - - PreparationStepWithTime copyWithElapsed(Duration elapsed) { - return PreparationStepWithTime( - id: id, - preparationName: preparationName, - preparationTime: preparationTime, - nextPreparationId: nextPreparationId, - elapsedTime: elapsed, - ); + Duration? get durationUntilPreparationStart { + if (schedule == null) return null; + final now = DateTime.now(); + final target = schedule!.preparationStartTime; + if (target.isBefore(now) || target.isAtSameMomentAs(now)) return null; + return target.difference(now); } @override - List get props => - [id, preparationName, preparationTime, nextPreparationId, elapsedTime]; -} - -class PreparationWithTime extends PreparationEntity implements Equatable { - const PreparationWithTime({ - required List preparationStepList, - }) : super(preparationStepList: preparationStepList); - - @override - List get preparationStepList => - super.preparationStepList.cast(); - - @override - List get props => [preparationStepList]; + List get props => [status, schedule, schedule?.preparation]; } diff --git a/lib/presentation/home/components/todays_schedule_tile.dart b/lib/presentation/home/components/todays_schedule_tile.dart index 47e5a237..f56445bf 100644 --- a/lib/presentation/home/components/todays_schedule_tile.dart +++ b/lib/presentation/home/components/todays_schedule_tile.dart @@ -6,9 +6,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/presentation/home/bloc/schedule_timer_bloc.dart'; class TodaysScheduleTile extends StatelessWidget { - const TodaysScheduleTile({super.key, this.schedule}); + const TodaysScheduleTile({super.key, this.schedule, this.onTap}); final ScheduleEntity? schedule; + final VoidCallback? onTap; Widget _noSchedule(BuildContext context) { final theme = Theme.of(context); @@ -59,15 +60,20 @@ class TodaysScheduleTile extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - color: schedule == null - ? theme.colorScheme.surfaceContainerLow - : theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: schedule == null ? null : onTap, + child: Container( + decoration: BoxDecoration( + color: schedule == null + ? theme.colorScheme.surfaceContainerLow + : theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + width: double.infinity, + child: + schedule == null ? _noSchedule(context) : _scheduleExists(context), ), - width: double.infinity, - child: schedule == null ? _noSchedule(context) : _scheduleExists(context), ); } } diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart index 2d8f882d..cd063837 100644 --- a/lib/presentation/home/screens/home_screen.dart +++ b/lib/presentation/home/screens/home_screen.dart @@ -99,6 +99,7 @@ class _HomeScreenState extends State { SizedBox(height: 21.0), TodaysScheduleTile( schedule: state.todaySchedule, + onTap: () => context.go('/alarmScreen'), ) ], ), diff --git a/lib/presentation/home/screens/home_screen_tmp.dart b/lib/presentation/home/screens/home_screen_tmp.dart index 5719c825..41da7b4f 100644 --- a/lib/presentation/home/screens/home_screen_tmp.dart +++ b/lib/presentation/home/screens/home_screen_tmp.dart @@ -8,8 +8,8 @@ import 'package:on_time_front/presentation/calendar/bloc/monthly_schedules_bloc. import 'package:on_time_front/presentation/home/components/todays_schedule_tile.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/presentation/shared/components/arc_indicator.dart'; -import 'package:on_time_front/presentation/shared/theme/theme.dart'; import 'package:on_time_front/presentation/home/components/month_calendar.dart'; +import 'package:on_time_front/presentation/app/bloc/schedule/schedule_bloc.dart'; /// Wrapper widget that provides the BlocProvider for HomeScreenTmp class HomeScreenTmp extends StatelessWidget { @@ -61,7 +61,7 @@ class HomeScreenContent extends StatelessWidget { child: Column( children: [ _CharacterSection(score: score), - _TodaysScheduleOverlay(state: state), + _TodaysScheduleOverlay(), ], ), ), @@ -82,69 +82,70 @@ class HomeScreenContent extends StatelessWidget { } class _TodaysScheduleOverlay extends StatelessWidget { - const _TodaysScheduleOverlay({ - required this.state, - }); - - final MonthlySchedulesState state; + const _TodaysScheduleOverlay(); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = Theme.of(context).colorScheme; - final today = DateTime.now(); - final todayKey = DateTime(today.year, today.month, today.day); - final todaySchedules = state.schedules[todayKey] ?? []; - final todaySchedule = - todaySchedules.isNotEmpty ? todaySchedules.first : null; - - return Stack( - alignment: Alignment.bottomCenter, - children: [ - Positioned.fill( - child: Padding( - padding: const EdgeInsets.only(top: 49.0), - child: Container( - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), + return BlocBuilder( + builder: (context, scheduleState) { + final todaySchedule = + scheduleState.status == ScheduleStatus.notExists || + scheduleState.status == ScheduleStatus.initial + ? null + : scheduleState.schedule; + + return Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: Padding( + padding: const EdgeInsets.only(top: 49.0), + child: Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), ), ), ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0) + - EdgeInsets.only(bottom: 20.0), - child: Material( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - color: theme.colorScheme.surface, - elevation: 6, - shadowColor: Colors.black.withValues(alpha: 0.4), - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context)!.todaysAppointments, - style: theme.textTheme.titleMedium, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0) + + EdgeInsets.only(bottom: 20.0), + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + color: theme.colorScheme.surface, + elevation: 6, + shadowColor: Colors.black.withValues(alpha: 0.4), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.todaysAppointments, + style: theme.textTheme.titleMedium, + ), + SizedBox(height: 21.0), + TodaysScheduleTile( + schedule: todaySchedule, + onTap: () => context.go('/alarmScreen'), + ) + ], ), - SizedBox(height: 21.0), - TodaysScheduleTile( - schedule: todaySchedule, - ) - ], + ), ), ), - ), - ), - ], + ], + ); + }, ); } } @@ -276,37 +277,3 @@ class _AnimatedArcIndicatorState extends State ); } } - -class _Character extends StatelessWidget { - const _Character(); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 176, - height: 130, - child: SvgPicture.asset( - 'characters/half_character.svg', - package: 'assets', - ), - ); - } -} - -class _Slogan extends StatelessWidget { - const _Slogan({ - required this.comment, - }); - - final String comment; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - return Text( - comment, - style: textTheme.titleExtraLarge.copyWith(color: colorScheme.onPrimary), - ); - } -} diff --git a/lib/presentation/shared/router/go_router.dart b/lib/presentation/shared/router/go_router.dart index 75e7d47c..72604b43 100644 --- a/lib/presentation/shared/router/go_router.dart +++ b/lib/presentation/shared/router/go_router.dart @@ -1,5 +1,6 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/core/services/navigation_service.dart'; @@ -117,14 +118,17 @@ GoRouter goRouterConfig(AuthBloc authBloc, ScheduleBloc scheduleBloc) { path: '/scheduleStart', name: 'scheduleStart', builder: (context, state) { - return ScheduleStartScreen(schedule: state.extra as ScheduleEntity); + final schedule = context.read().state.schedule; + if (schedule == null) { + return const SizedBox.shrink(); + } + return const ScheduleStartScreen(); }, ), GoRoute( path: '/alarmScreen', builder: (context, state) { - final schedule = state.extra as ScheduleEntity; - return AlarmScreen(schedule: schedule); + return AlarmScreen(); }, ), GoRoute( diff --git a/pubspec.lock b/pubspec.lock index 98b4a2bb..088ee61f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -912,26 +912,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.10" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.1" lints: dependency: transitive description: @@ -1421,10 +1421,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.4" timezone: dependency: transitive description: @@ -1549,10 +1549,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" verbose: dependency: transitive description: @@ -1682,5 +1682,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/widgetbook/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/widgetbook/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5df..e3773d42 100644 --- a/widgetbook/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/widgetbook/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/widgetbook/lib/preparatoin_step_list_widget.dart b/widgetbook/lib/preparatoin_step_list_widget.dart index e5549ec5..152afebe 100644 --- a/widgetbook/lib/preparatoin_step_list_widget.dart +++ b/widgetbook/lib/preparatoin_step_list_widget.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; -import 'package:on_time_front/presentation/alarm/bloc/alarm_timer/alarm_timer_bloc.dart'; import 'package:on_time_front/presentation/alarm/components/preparation_step_list_widget.dart'; import 'package:on_time_front/presentation/shared/constants/constants.dart'; import 'package:uuid/uuid.dart'; @@ -9,7 +7,7 @@ import 'package:widgetbook/widgetbook.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; @widgetbook.UseCase( - name: 'Default (Bloc-based)', + name: 'Default', type: PreparationStepListWidget, ) Widget preparationStepListWidgetUseCase(BuildContext context) { @@ -91,29 +89,17 @@ Widget preparationStepListWidgetUseCase(BuildContext context) { return Scaffold( backgroundColor: const Color.fromARGB(255, 243, 241, 241), - body: BlocProvider( - create: (context) => AlarmTimerBloc( - preparationSteps: preparationSteps, - beforeOutTime: 600, - isLate: false, - )..add(AlarmTimerStepsUpdated(preparationSteps)), - child: BlocBuilder( - builder: (context, state) { - return Center( - child: SizedBox( - width: width, - height: 400, - child: PreparationStepListWidget( - preparationSteps: preparationSteps, - currentStepIndex: currentStepIndex, - stepElapsedTimes: - List.generate(listLength, (_) => stepElapsedTime), - preparationStepStates: stepStates, - onSkip: () {}, - ), - ), - ); - }, + body: Center( + child: SizedBox( + width: width, + height: 400, + child: PreparationStepListWidget( + preparationSteps: preparationSteps, + currentStepIndex: currentStepIndex, + stepElapsedTimes: List.generate(listLength, (_) => stepElapsedTime), + preparationStepStates: stepStates, + onSkip: () {}, + ), ), ), ); diff --git a/widgetbook/lib/todays_schedule_tile.dart b/widgetbook/lib/todays_schedule_tile.dart index b1e8b813..5a4025c8 100644 --- a/widgetbook/lib/todays_schedule_tile.dart +++ b/widgetbook/lib/todays_schedule_tile.dart @@ -51,6 +51,9 @@ Widget todaysScheduleTileWithSchedule(BuildContext context) { return Padding( padding: const EdgeInsets.all(16.0), - child: TodaysScheduleTile(schedule: schedule), + child: TodaysScheduleTile( + schedule: schedule, + onTap: () {}, + ), ); }