Skip to content

Commit 2536d4d

Browse files
authored
Merge pull request #352 from DevKor-github/feat/show-prep-time-between-move
Match swipe action height to expanded schedule tiles
2 parents a9d05a3 + a745174 commit 2536d4d

13 files changed

Lines changed: 808 additions & 52 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:injectable/injectable.dart';
2+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
3+
import 'package:on_time_front/domain/repositories/preparation_repository.dart';
4+
5+
@Injectable()
6+
class StreamPreparationsUseCase {
7+
final PreparationRepository _preparationRepository;
8+
9+
StreamPreparationsUseCase(this._preparationRepository);
10+
11+
Stream<Map<String, PreparationEntity>> call() {
12+
return _preparationRepository.preparationStream;
13+
}
14+
}

lib/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@
143143
"@travelTime": {
144144
"description": "Label for the travel time text field"
145145
},
146+
"preparationTime": "Preparation Time",
147+
"@preparationTime": {
148+
"description": "Label for preparation time"
149+
},
146150
"hours": "hours",
147151
"@hours": {
148152
"description": "Unit of time"

lib/l10n/app_ko.arb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"appointmentNameHint": "예) 영화 보기",
3636
"appointmentPlace": "약속 장소",
3737
"travelTime": "이동시간",
38+
"preparationTime": "준비시간",
3839
"hours": "시간",
3940
"minutes": "분",
4041
"selectTime": "시간을 선택해 주세요",

lib/l10n/app_localizations.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ abstract class AppLocalizations {
314314
/// **'Travel Time'**
315315
String get travelTime;
316316

317+
/// Label for preparation time
318+
///
319+
/// In en, this message translates to:
320+
/// **'Preparation Time'**
321+
String get preparationTime;
322+
317323
/// Unit of time
318324
///
319325
/// In en, this message translates to:

lib/l10n/app_localizations_en.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ class AppLocalizationsEn extends AppLocalizations {
124124
@override
125125
String get travelTime => 'Travel Time';
126126

127+
@override
128+
String get preparationTime => 'Preparation Time';
129+
127130
@override
128131
String get hours => 'hours';
129132

lib/l10n/app_localizations_ko.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ class AppLocalizationsKo extends AppLocalizations {
119119
@override
120120
String get travelTime => '이동시간';
121121

122+
@override
123+
String get preparationTime => '준비시간';
124+
122125
@override
123126
String get hours => '시간';
124127

lib/presentation/calendar/bloc/monthly_schedules_bloc.dart

Lines changed: 207 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import 'dart:async';
2+
13
import 'package:equatable/equatable.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_bloc/flutter_bloc.dart';
46
import 'package:injectable/injectable.dart';
7+
import 'package:on_time_front/domain/entities/preparation_entity.dart';
58
import 'package:on_time_front/domain/entities/schedule_entity.dart';
69
import 'package:on_time_front/domain/use-cases/delete_schedule_use_case.dart';
10+
import 'package:on_time_front/domain/use-cases/get_preparation_by_schedule_id_use_case.dart';
711
import 'package:on_time_front/domain/use-cases/get_schedules_by_date_use_case.dart';
12+
import 'package:on_time_front/domain/use-cases/load_preparation_by_schedule_id_use_case.dart';
813
import 'package:on_time_front/domain/use-cases/load_schedules_for_month_use_case.dart';
14+
import 'package:on_time_front/domain/use-cases/stream_preparations_use_case.dart';
915

1016
part 'monthly_schedules_event.dart';
1117
part 'monthly_schedules_state.dart';
@@ -17,15 +23,40 @@ class MonthlySchedulesBloc
1723
this._loadSchedulesForMonthUseCase,
1824
this._getSchedulesByDateUseCase,
1925
this._deleteScheduleUseCase,
26+
this._loadPreparationByScheduleIdUseCase,
27+
this._getPreparationByScheduleIdUseCase,
28+
this._streamPreparationsUseCase,
2029
) : super(MonthlySchedulesState()) {
2130
on<MonthlySchedulesSubscriptionRequested>(_onSubscriptionRequested);
2231
on<MonthlySchedulesMonthAdded>(_onMonthAdded);
2332
on<MonthlySchedulesScheduleDeleted>(_onScheduleDeleted);
33+
on<MonthlySchedulesVisibleDateChanged>(_onVisibleDateChanged);
34+
on<MonthlySchedulesPreparationsPrefetchRequested>(
35+
_onPreparationsPrefetchRequested,
36+
);
37+
on<MonthlySchedulesPreparationsStreamChanged>(
38+
_onPreparationsStreamChanged,
39+
);
40+
41+
_preparationSubscription = _streamPreparationsUseCase().listen(
42+
(preparations) {
43+
add(
44+
MonthlySchedulesPreparationsStreamChanged(
45+
preparations: preparations,
46+
),
47+
);
48+
},
49+
);
2450
}
2551

2652
final LoadSchedulesForMonthUseCase _loadSchedulesForMonthUseCase;
2753
final GetSchedulesByDateUseCase _getSchedulesByDateUseCase;
2854
final DeleteScheduleUseCase _deleteScheduleUseCase;
55+
final LoadPreparationByScheduleIdUseCase _loadPreparationByScheduleIdUseCase;
56+
final GetPreparationByScheduleIdUseCase _getPreparationByScheduleIdUseCase;
57+
final StreamPreparationsUseCase _streamPreparationsUseCase;
58+
59+
StreamSubscription<Map<String, PreparationEntity>>? _preparationSubscription;
2960

3061
Future<void> _onSubscriptionRequested(
3162
MonthlySchedulesSubscriptionRequested event,
@@ -37,27 +68,17 @@ class MonthlySchedulesBloc
3768

3869
await emit.forEach(
3970
_getSchedulesByDateUseCase(event.startDate, event.endDate),
40-
onData: (schedules) => state.copyWith(
41-
status: () => MonthlySchedulesStatus.success,
42-
schedules: () => schedules.fold<Map<DateTime, List<ScheduleEntity>>>(
43-
{},
44-
(previousValue, element) {
45-
final scheduleTime = DateTime(
46-
element.scheduleTime.year,
47-
element.scheduleTime.month,
48-
element.scheduleTime.day,
49-
);
50-
if (previousValue.containsKey(scheduleTime)) {
51-
previousValue[scheduleTime]!.add(element);
52-
} else {
53-
previousValue[scheduleTime] = [element];
54-
}
55-
return previousValue;
56-
},
57-
),
58-
startDate: () => event.startDate,
59-
endDate: () => event.endDate,
60-
),
71+
onData: (schedules) {
72+
final groupedSchedules = _groupSchedulesByDate(schedules);
73+
final nextState = state.copyWith(
74+
status: () => MonthlySchedulesStatus.success,
75+
schedules: () => groupedSchedules,
76+
startDate: () => event.startDate,
77+
endDate: () => event.endDate,
78+
);
79+
_requestVisibleDatePreparationPrefetch(nextState);
80+
return nextState;
81+
},
6182
onError: (error, stackTrace) => state.copyWith(
6283
status: () => MonthlySchedulesStatus.error,
6384
),
@@ -106,27 +127,15 @@ class MonthlySchedulesBloc
106127
await emit.forEach(
107128
_getSchedulesByDateUseCase(startDate, endDate),
108129
onData: (schedules) {
109-
return state.copyWith(
130+
final groupedSchedules = _groupSchedulesByDate(schedules);
131+
final nextState = state.copyWith(
110132
status: () => MonthlySchedulesStatus.success,
111-
schedules: () => schedules.fold<Map<DateTime, List<ScheduleEntity>>>(
112-
{},
113-
(previousValue, element) {
114-
final scheduleTime = DateTime(
115-
element.scheduleTime.year,
116-
element.scheduleTime.month,
117-
element.scheduleTime.day,
118-
);
119-
if (previousValue.containsKey(scheduleTime)) {
120-
previousValue[scheduleTime]!.add(element);
121-
} else {
122-
previousValue[scheduleTime] = [element];
123-
}
124-
return previousValue;
125-
},
126-
),
133+
schedules: () => groupedSchedules,
127134
startDate: () => startDate,
128135
endDate: () => endDate,
129136
);
137+
_requestVisibleDatePreparationPrefetch(nextState);
138+
return nextState;
130139
},
131140
onError: (error, stackTrace) {
132141
return state.copyWith(
@@ -141,8 +150,12 @@ class MonthlySchedulesBloc
141150
Emitter<MonthlySchedulesState> emit,
142151
) async {
143152
try {
153+
final updatedPreparationMap =
154+
Map<String, Duration>.from(state.preparationDurationByScheduleId)
155+
..remove(event.schedule.id);
144156
emit(state.copyWith(
145157
lastDeletedSchedule: () => event.schedule,
158+
preparationDurationByScheduleId: () => updatedPreparationMap,
146159
));
147160
await _deleteScheduleUseCase(event.schedule);
148161
} catch (e) {
@@ -151,4 +164,160 @@ class MonthlySchedulesBloc
151164
));
152165
}
153166
}
167+
168+
void _onVisibleDateChanged(
169+
MonthlySchedulesVisibleDateChanged event,
170+
Emitter<MonthlySchedulesState> emit,
171+
) {
172+
final normalizedDate =
173+
DateTime(event.date.year, event.date.month, event.date.day);
174+
final nextState = state.copyWith(visibleDate: () => normalizedDate);
175+
emit(nextState);
176+
_requestVisibleDatePreparationPrefetch(nextState);
177+
}
178+
179+
Future<void> _onPreparationsPrefetchRequested(
180+
MonthlySchedulesPreparationsPrefetchRequested event,
181+
Emitter<MonthlySchedulesState> emit,
182+
) async {
183+
final missingIds = event.scheduleIds
184+
.where((id) => !state.preparationDurationByScheduleId.containsKey(id))
185+
.toSet();
186+
if (missingIds.isEmpty) {
187+
return;
188+
}
189+
190+
final fetchedDurations = <String, Duration>{};
191+
var hasUpdates = false;
192+
193+
for (final scheduleId in missingIds) {
194+
try {
195+
await _loadPreparationByScheduleIdUseCase(scheduleId);
196+
if (state.preparationDurationByScheduleId.containsKey(scheduleId)) {
197+
// Stream update already has fresher data.
198+
continue;
199+
}
200+
final preparation =
201+
await _getPreparationByScheduleIdUseCase(scheduleId);
202+
fetchedDurations[scheduleId] = preparation.totalDuration;
203+
hasUpdates = true;
204+
} catch (_) {
205+
// Keep fallback UI when loading fails.
206+
}
207+
}
208+
209+
if (hasUpdates) {
210+
final updatedMap = Map<String, Duration>.from(
211+
state.preparationDurationByScheduleId,
212+
)..addAll(fetchedDurations);
213+
emit(
214+
state.copyWith(
215+
preparationDurationByScheduleId: () => updatedMap,
216+
),
217+
);
218+
}
219+
}
220+
221+
void _onPreparationsStreamChanged(
222+
MonthlySchedulesPreparationsStreamChanged event,
223+
Emitter<MonthlySchedulesState> emit,
224+
) {
225+
final cachedScheduleIds = _getCachedScheduleIds(state.schedules);
226+
if (cachedScheduleIds.isEmpty) {
227+
return;
228+
}
229+
230+
final nextDurations = Map<String, Duration>.from(
231+
state.preparationDurationByScheduleId,
232+
);
233+
var hasChange = false;
234+
235+
for (final entry in event.preparations.entries) {
236+
if (!cachedScheduleIds.contains(entry.key)) {
237+
continue;
238+
}
239+
240+
final nextDuration = entry.value.totalDuration;
241+
if (nextDurations[entry.key] != nextDuration) {
242+
nextDurations[entry.key] = nextDuration;
243+
hasChange = true;
244+
}
245+
}
246+
247+
if (!hasChange) {
248+
return;
249+
}
250+
251+
emit(
252+
state.copyWith(
253+
preparationDurationByScheduleId: () => nextDurations,
254+
),
255+
);
256+
}
257+
258+
void _requestVisibleDatePreparationPrefetch(
259+
MonthlySchedulesState sourceState) {
260+
final visibleDate = sourceState.visibleDate;
261+
if (visibleDate == null) {
262+
return;
263+
}
264+
final scheduleIds = _getScheduleIdsForDate(
265+
sourceState.schedules,
266+
visibleDate,
267+
);
268+
if (scheduleIds.isEmpty) {
269+
return;
270+
}
271+
add(MonthlySchedulesPreparationsPrefetchRequested(
272+
scheduleIds: scheduleIds));
273+
}
274+
275+
Map<DateTime, List<ScheduleEntity>> _groupSchedulesByDate(
276+
List<ScheduleEntity> schedules,
277+
) {
278+
return schedules.fold<Map<DateTime, List<ScheduleEntity>>>(
279+
{},
280+
(previousValue, element) {
281+
final scheduleTime = DateTime(
282+
element.scheduleTime.year,
283+
element.scheduleTime.month,
284+
element.scheduleTime.day,
285+
);
286+
if (previousValue.containsKey(scheduleTime)) {
287+
previousValue[scheduleTime]!.add(element);
288+
} else {
289+
previousValue[scheduleTime] = [element];
290+
}
291+
return previousValue;
292+
},
293+
);
294+
}
295+
296+
List<String> _getScheduleIdsForDate(
297+
Map<DateTime, List<ScheduleEntity>> schedules,
298+
DateTime date,
299+
) {
300+
final normalizedDate = DateTime(date.year, date.month, date.day);
301+
return schedules[normalizedDate]
302+
?.map((schedule) => schedule.id)
303+
.toList(growable: false) ??
304+
const <String>[];
305+
}
306+
307+
Set<String> _getCachedScheduleIds(
308+
Map<DateTime, List<ScheduleEntity>> schedules) {
309+
final ids = <String>{};
310+
for (final scheduleList in schedules.values) {
311+
for (final schedule in scheduleList) {
312+
ids.add(schedule.id);
313+
}
314+
}
315+
return ids;
316+
}
317+
318+
@override
319+
Future<void> close() async {
320+
await _preparationSubscription?.cancel();
321+
return super.close();
322+
}
154323
}

0 commit comments

Comments
 (0)