diff --git a/assets/appleid_button.png b/assets/appleid_button.png new file mode 100644 index 00000000..7b80164a Binary files /dev/null and b/assets/appleid_button.png differ diff --git a/lib/core/constants/endpoint.dart b/lib/core/constants/endpoint.dart index 1c43823f..8c6a9ed9 100644 --- a/lib/core/constants/endpoint.dart +++ b/lib/core/constants/endpoint.dart @@ -56,6 +56,9 @@ class Endpoint { static get updateDefaultPreparation => _defaultPreparation; + static const _updateSpareTime = '/users/me/spare-time'; + static get updateSpareTime => _updateSpareTime; + static const _fcmToken = '/firebase-token'; // 사용자 fcm 토큰 등록 static get fcmTokenRegister => _fcmToken; } diff --git a/lib/data/data_sources/preparation_remote_data_source.dart b/lib/data/data_sources/preparation_remote_data_source.dart index 6d5d0a75..33108cfc 100644 --- a/lib/data/data_sources/preparation_remote_data_source.dart +++ b/lib/data/data_sources/preparation_remote_data_source.dart @@ -8,6 +8,7 @@ import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/data/models/create_preparation_schedule_request_model.dart'; import 'package:on_time_front/data/models/create_defualt_preparation_request_model.dart'; import 'package:on_time_front/data/models/get_preparation_step_response_model.dart'; +import 'package:on_time_front/data/models/update_spare_time_request_model.dart'; abstract interface class PreparationRemoteDataSource { Future createDefaultPreparation( @@ -24,6 +25,8 @@ abstract interface class PreparationRemoteDataSource { Future getPreparationByScheduleId(String scheduleId); Future getDefualtPreparation(); + + Future updateSpareTime(Duration newSpareTime); } @Injectable(as: PreparationRemoteDataSource) @@ -121,7 +124,7 @@ class PreparationRemoteDataSourceImpl implements PreparationRemoteDataSource { PreparationUserModifyRequestModelListExtension.fromEntityList( preparationEntity.preparationStepList); - final result = await dio.post( + final result = await dio.put( Endpoint.updateDefaultPreparation, data: updateModel.map((model) => model.toJson()).toList(), ); @@ -154,4 +157,20 @@ class PreparationRemoteDataSourceImpl implements PreparationRemoteDataSource { rethrow; } } + + @override + Future updateSpareTime(Duration newSpareTime) async { + try { + final body = UpdateSpareTimeRequestModel.fromDuration(newSpareTime); + final result = await dio.put( + Endpoint.updateSpareTime, + data: body.toJson(), + ); + if (result.statusCode != 200) { + throw Exception('Error updating spare time'); + } + } catch (e) { + rethrow; + } + } } diff --git a/lib/data/models/update_spare_time_request_model.dart b/lib/data/models/update_spare_time_request_model.dart new file mode 100644 index 00000000..8b34aa69 --- /dev/null +++ b/lib/data/models/update_spare_time_request_model.dart @@ -0,0 +1,13 @@ +class UpdateSpareTimeRequestModel { + final int newSpareTime; + + UpdateSpareTimeRequestModel({required this.newSpareTime}); + + Map toJson() => { + 'newSpareTime': newSpareTime, + }; + + factory UpdateSpareTimeRequestModel.fromDuration(Duration duration) { + return UpdateSpareTimeRequestModel(newSpareTime: duration.inMinutes); + } +} diff --git a/lib/data/repositories/preparation_repository_impl.dart b/lib/data/repositories/preparation_repository_impl.dart index cc2672f6..ff42ecac 100644 --- a/lib/data/repositories/preparation_repository_impl.dart +++ b/lib/data/repositories/preparation_repository_impl.dart @@ -91,4 +91,12 @@ class PreparationRepositoryImpl implements PreparationRepository { rethrow; } } + + Future updateSpareTime(Duration newSpareTime) async { + try { + await preparationRemoteDataSource.updateSpareTime(newSpareTime); + } catch (e) { + rethrow; + } + } } diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index e9be96a5..6e0b7d15 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -168,6 +168,16 @@ class UserRepositoryImpl implements UserRepository { } } + @override + Future disconnectGoogleSignIn() async { + try { + await _googleSignIn.disconnect(); + debugPrint('Google Sign-In disconnected'); + } catch (e) { + debugPrint('Google Sign-In disconnect failed: $e'); + } + } + @override Stream get userStream => _userStreamController.asBroadcastStream(); diff --git a/lib/domain/repositories/preparation_repository.dart b/lib/domain/repositories/preparation_repository.dart index 10edde1f..d6b94af3 100644 --- a/lib/domain/repositories/preparation_repository.dart +++ b/lib/domain/repositories/preparation_repository.dart @@ -17,4 +17,6 @@ abstract interface class PreparationRepository { Future updatePreparationByScheduleId( PreparationEntity preparationEntity, String scheduleId); + + Future updateSpareTime(Duration newSpareTime); } diff --git a/lib/domain/repositories/user_repository.dart b/lib/domain/repositories/user_repository.dart index e9a8fbf0..da9f2579 100644 --- a/lib/domain/repositories/user_repository.dart +++ b/lib/domain/repositories/user_repository.dart @@ -33,4 +33,6 @@ abstract interface class UserRepository { Future postFeedback(String message); Future getUserSocialType(); + + Future disconnectGoogleSignIn(); } diff --git a/lib/domain/use-cases/delete_user_use_case.dart b/lib/domain/use-cases/delete_user_use_case.dart index b5c1fb4b..105f9490 100644 --- a/lib/domain/use-cases/delete_user_use_case.dart +++ b/lib/domain/use-cases/delete_user_use_case.dart @@ -15,7 +15,9 @@ class DeleteUserUseCase { final socialTypeString = await _userRepository.getUserSocialType(); final socialType = socialTypeFromString(socialTypeString); + if (socialType == SocialType.google) { + await _userRepository.disconnectGoogleSignIn(); await _userRepository.deleteGoogleUser(); } else if (socialType == SocialType.apple) { await _userRepository.deleteAppleUser(); diff --git a/lib/domain/use-cases/update_spare_time_use_case.dart b/lib/domain/use-cases/update_spare_time_use_case.dart new file mode 100644 index 00000000..2bf97c79 --- /dev/null +++ b/lib/domain/use-cases/update_spare_time_use_case.dart @@ -0,0 +1,13 @@ +import 'package:injectable/injectable.dart'; +import 'package:on_time_front/domain/repositories/preparation_repository.dart'; + +@Injectable() +class UpdateSpareTimeUseCase { + final PreparationRepository _preparationRepository; + + UpdateSpareTimeUseCase(this._preparationRepository); + + Future call(Duration newSpareTime) async { + await _preparationRepository.updateSpareTime(newSpareTime); + } +} diff --git a/lib/presentation/login/components/google_sign_in_button/apple_sign_in_button_mobile.dart b/lib/presentation/login/components/google_sign_in_button/apple_sign_in_button_mobile.dart index abf72a54..fdf9f18e 100644 --- a/lib/presentation/login/components/google_sign_in_button/apple_sign_in_button_mobile.dart +++ b/lib/presentation/login/components/google_sign_in_button/apple_sign_in_button_mobile.dart @@ -12,15 +12,11 @@ class AppleSignInButton extends StatelessWidget { return SizedBox( width: 358, - child: DefaultTextStyle.merge( - style: TextStyle( - fontSize: 19, - fontWeight: FontWeight.w600, - height: 24 / 19, - fontFamily: 'SF Pro', - ), - child: SignInWithAppleButton( - onPressed: () async { + height: 54, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { try { final credential = await SignInWithApple.getAppleIDCredential( scopes: [ @@ -51,11 +47,14 @@ class AppleSignInButton extends StatelessWidget { debugPrint('Apple Sign In Error: ${e.toString()}'); } }, - style: SignInWithAppleButtonStyle.black, - height: 54, borderRadius: BorderRadius.circular(14), - iconAlignment: IconAlignment.center, - text: 'Sign in with Apple', + child: Image.asset( + 'appleid_button.png', + package: 'assets', + width: 358, + height: 54, + fit: BoxFit.cover, + ), ), ), ); diff --git a/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_mobile.dart b/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_mobile.dart index a0b5d7d5..b83115ec 100644 --- a/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_mobile.dart +++ b/lib/presentation/login/components/google_sign_in_button/google_sign_in_button_mobile.dart @@ -4,50 +4,64 @@ import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/domain/repositories/user_repository.dart'; class GoogleSignInButton extends StatelessWidget { - GoogleSignInButton({super.key}); - - final Widget googleIconSvg = SvgPicture.asset( - 'google_icon.svg', - package: 'assets', - semanticsLabel: 'Google Icon', - fit: BoxFit.contain, - ); + const GoogleSignInButton({super.key}); @override Widget build(BuildContext context) { final UserRepository authenticationRepository = getIt.get(); - return GestureDetector( - onTap: () async { - try { - final googleAccout = - await authenticationRepository.googleSignIn.signIn(); - if (googleAccout == null) { - throw Exception('Google Sign In Failed, Sign In Accout is null'); + + return SizedBox( + width: 358, + height: 54, + child: ElevatedButton( + onPressed: () async { + try { + final googleAccount = + await authenticationRepository.googleSignIn.signIn(); + if (googleAccount == null) { + throw Exception('Google Sign In Failed, Sign In Account is null'); + } + await authenticationRepository.signInWithGoogle(googleAccount); + } catch (e) { + debugPrint(e.toString()); } - await authenticationRepository.signInWithGoogle(googleAccout); - } catch (e) { - debugPrint(e.toString()); - } - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all(color: Color(0xFF747775)), - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 2, - offset: Offset(0, 1), + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 1, + shadowColor: Colors.black26, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: Color(0xFFDADCE0), width: 1), + ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'google_icon.svg', + package: 'assets', + semanticsLabel: 'Google Icon', + fit: BoxFit.contain, + width: 20, + height: 20, + ), + SizedBox(width: 10), + Text( + 'Sign in with Google', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w600, + fontSize: 21, + height: 1.4, + letterSpacing: 0, + color: Colors.black87, + ), ), ], ), - child: Padding( - padding: const EdgeInsets.all(9.0), - child: googleIconSvg, - ), ), ); } diff --git a/lib/presentation/login/screens/sign_in_main_screen.dart b/lib/presentation/login/screens/sign_in_main_screen.dart index 845d89e2..5250e475 100644 --- a/lib/presentation/login/screens/sign_in_main_screen.dart +++ b/lib/presentation/login/screens/sign_in_main_screen.dart @@ -44,7 +44,7 @@ class _SignInMainScreenState extends State { SizedBox(height: 41), if (!kIsWeb && Platform.isIOS) ...[ AppleSignInButton(), - SizedBox(height: 22), + SizedBox(height: 16), ], GoogleSignInButton(), ], diff --git a/lib/presentation/my_page/preparation_spare_time_edit/bloc/default_preparation_spare_time_form_bloc.dart b/lib/presentation/my_page/preparation_spare_time_edit/bloc/default_preparation_spare_time_form_bloc.dart index a6237649..90a93cea 100644 --- a/lib/presentation/my_page/preparation_spare_time_edit/bloc/default_preparation_spare_time_form_bloc.dart +++ b/lib/presentation/my_page/preparation_spare_time_edit/bloc/default_preparation_spare_time_form_bloc.dart @@ -3,7 +3,9 @@ import 'package:equatable/equatable.dart'; import 'package:injectable/injectable.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/domain/use-cases/get_default_preparation_use_case.dart'; -import 'package:on_time_front/domain/use-cases/onboard_use_case.dart'; +import 'package:on_time_front/domain/use-cases/update_default_preparation_use_case.dart'; +import 'package:on_time_front/domain/use-cases/update_spare_time_use_case.dart'; +import 'package:on_time_front/domain/use-cases/load_user_use_case.dart'; part 'default_preparation_spare_time_form_event.dart'; part 'default_preparation_spare_time_form_state.dart'; @@ -14,7 +16,9 @@ class DefaultPreparationSpareTimeFormBloc extends Bloc< DefaultPreparationSpareTimeFormState> { DefaultPreparationSpareTimeFormBloc( this._getDefaultPreparationUseCase, - this._onboardUseCase, + this._updateDefaultPreparationUseCase, + this._updateSpareTimeUseCase, + this._loadUserUseCase, ) : super(DefaultPreparationSpareTimeFormState()) { on(_onFormEditRequested); on(_onSpareTimeIncreased); @@ -23,7 +27,9 @@ class DefaultPreparationSpareTimeFormBloc extends Bloc< } final GetDefaultPreparationUseCase _getDefaultPreparationUseCase; - final OnboardUseCase _onboardUseCase; + final UpdateDefaultPreparationUseCase _updateDefaultPreparationUseCase; + final UpdateSpareTimeUseCase _updateSpareTimeUseCase; + final LoadUserUseCase _loadUserUseCase; final Duration lowerBound = Duration(minutes: 10); final Duration stepSize = Duration(minutes: 5); @@ -78,11 +84,9 @@ class DefaultPreparationSpareTimeFormBloc extends Bloc< )); try { - await _onboardUseCase( - preparationEntity: event.preparation, - spareTime: state.spareTime!, - note: event.note, - ); + await _updateDefaultPreparationUseCase(event.preparation); + await _updateSpareTimeUseCase(state.spareTime!); + await _loadUserUseCase(); emit(state.copyWith( status: DefaultPreparationSpareTimeStatus.success, diff --git a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart index 5ab142bf..ba9a7960 100644 --- a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart +++ b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart @@ -47,6 +47,10 @@ class _PreparationSpareTimeEditView extends StatelessWidget { builder: (context, state2) { return Scaffold( appBar: AppBar( + elevation: 0, + shadowColor: Colors.transparent, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, leading: IconButton( icon: Icon( Icons.arrow_back_ios_rounded, @@ -181,27 +185,37 @@ class _PreparationSection extends StatelessWidget { width: double.infinity, child: Padding( padding: const EdgeInsets.only(bottom: 15.0), - child: Text( - AppLocalizations.of(context)!.totalTime, - textAlign: TextAlign.end, + child: Builder( + builder: (context) { + final totalDuration = preparationNameState.preparationStepList + .fold(Duration.zero, + (prev, step) => prev + step.preparationTime.value); + return Text( + '${AppLocalizations.of(context)!.totalTime}${totalDuration.inMinutes}분', + textAlign: TextAlign.end, + ); + }, ), ), ), - PreparationFormCreateList( - preparationNameState: preparationNameState, - onNameChanged: ({required int index, required String value}) { - context.read().add( - PreparationFormPreparationStepNameChanged( - index: index, - preparationStepName: value, - ), - ); - }, - onCreationRequested: () { - context.read().add( - const PreparationFormPreparationStepCreationRequested(), - ); - }, + Expanded( + child: PreparationFormCreateList( + preparationNameState: preparationNameState, + enableDismissible: true, + onNameChanged: ({required int index, required String value}) { + context.read().add( + PreparationFormPreparationStepNameChanged( + index: index, + preparationStepName: value, + ), + ); + }, + onCreationRequested: () { + context.read().add( + const PreparationFormPreparationStepCreationRequested(), + ); + }, + ), ), ], ); diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart index bc2898aa..8eae2568 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart @@ -74,12 +74,18 @@ class PreparationFormBloc PreparationFormPreparationStepRemoved event, Emitter emit, ) { + if (state.preparationStepList.length <= 1) { + return; + } + final removedList = List.from(state.preparationStepList); removedList.removeWhere((element) => element.id == event.preparationStepId); + final isValid = _validate(removedList); emit(state.copyWith( preparationStepList: removedList, + isValid: isValid, )); } @@ -133,8 +139,10 @@ class PreparationFormBloc final item = changedList.removeAt(oldIndex); changedList.insert(newIndex, item); + final isValid = _validate(changedList); emit(state.copyWith( preparationStepList: changedList, + isValid: isValid, )); } diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart index 6a7ca06a..280ac666 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart @@ -4,6 +4,7 @@ import 'package:on_time_front/presentation/onboarding/preparation_name_select/co import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_reorderable_list.dart'; +import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_reorderable_list_dissmissible.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/cubit/preparation_step_form_cubit.dart'; class PreparationFormCreateList extends StatelessWidget { @@ -11,32 +12,49 @@ class PreparationFormCreateList extends StatelessWidget { {super.key, required this.preparationNameState, required this.onNameChanged, - required this.onCreationRequested}); + required this.onCreationRequested, + this.enableDismissible = false}); final PreparationFormState preparationNameState; final void Function({required int index, required String value}) onNameChanged; final VoidCallback onCreationRequested; + final bool enableDismissible; @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [ - PreparationFormReorderableList( - preparationStepList: preparationNameState.preparationStepList, - onNameChanged: (index, value) { - onNameChanged(index: index, value: value); - }, - onTimeChanged: (index, value) => context - .read() - .add(PreparationFormPreparationStepTimeChanged( - index: index, preparationStepTime: value)), - onReorder: (oldIndex, newIndex) => context - .read() - .add(PreparationFormPreparationStepOrderChanged( - oldIndex: oldIndex, newIndex: newIndex)), - ), + enableDismissible + ? PreparationFormReorderableListDismissible( + preparationStepList: preparationNameState.preparationStepList, + onNameChanged: (index, value) { + onNameChanged(index: index, value: value); + }, + onTimeChanged: (index, value) => context + .read() + .add(PreparationFormPreparationStepTimeChanged( + index: index, preparationStepTime: value)), + onReorder: (oldIndex, newIndex) => context + .read() + .add(PreparationFormPreparationStepOrderChanged( + oldIndex: oldIndex, newIndex: newIndex)), + ) + : PreparationFormReorderableList( + preparationStepList: preparationNameState.preparationStepList, + onNameChanged: (index, value) { + onNameChanged(index: index, value: value); + }, + onTimeChanged: (index, value) => context + .read() + .add(PreparationFormPreparationStepTimeChanged( + index: index, preparationStepTime: value)), + onReorder: (oldIndex, newIndex) => context + .read() + .add(PreparationFormPreparationStepOrderChanged( + oldIndex: oldIndex, newIndex: newIndex)), + ), preparationNameState.status == PreparationFormStatus.adding ? BlocProvider( create: (context) => PreparationStepFormCubit( diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_reorderable_list_dissmissible.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_reorderable_list_dissmissible.dart new file mode 100644 index 00000000..376df148 --- /dev/null +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_reorderable_list_dissmissible.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart'; +import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart'; +import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/cubit/preparation_step_form_cubit.dart'; + +class PreparationFormReorderableListDismissible extends StatelessWidget { + const PreparationFormReorderableListDismissible({ + super.key, + required this.preparationStepList, + required this.onNameChanged, + required this.onTimeChanged, + required this.onReorder, + }); + + final List preparationStepList; + final Function(int index, String value) onNameChanged; + final Function(int index, Duration value) onTimeChanged; + final Function(int oldIndex, int newIndex) onReorder; + + @override + Widget build(BuildContext context) { + Widget proxyDecorator( + Widget child, int index, Animation animation) { + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return SizedBox( + child: child, + ); + }, + child: child, + ); + } + + return SingleChildScrollView( + child: ReorderableListView.builder( + buildDefaultDragHandles: false, + proxyDecorator: proxyDecorator, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: NeverScrollableScrollPhysics(), + itemCount: preparationStepList.length, + itemBuilder: (context, index) { + return Dismissible( + key: ValueKey( + 'dismissible_${preparationStepList[index].id}'), + direction: DismissDirection.endToStart, + background: Container( + height: double.infinity, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 20.0), + color: Colors.red, + child: const Icon( + Icons.delete, + color: Colors.white, + size: 24, + ), + ), + confirmDismiss: (direction) async { + if (preparationStepList.length <= 1) { + return false; + } + return direction == DismissDirection.endToStart; + }, + onDismissed: (direction) { + if (direction == DismissDirection.endToStart) { + context.read().add( + PreparationFormPreparationStepRemoved( + preparationStepId: preparationStepList[index].id, + ), + ); + } + }, + child: PreparationFormListField( + key: ValueKey(preparationStepList[index].id), + index: index, + preparationStep: preparationStepList[index], + onNameChanged: (value) { + onNameChanged(index, value); + }, + onPreparationTimeChanged: (value) { + onTimeChanged(index, value); + }, + ), + ); + }, + onReorder: (int oldIndex, int newIndex) { + onReorder(oldIndex, newIndex); + }, + ), + ); + } +}