Browse Source
feat: add LanguagePage, enforce full-screen mode on iOS, and improve intro video aspect ratio handling
develop
feat: add LanguagePage, enforce full-screen mode on iOS, and improve intro video aspect ratio handling
develop
1 changed files with 325 additions and 0 deletions
@ -0,0 +1,325 @@ |
|||||
|
import 'package:auto_size_text/auto_size_text.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_bloc/flutter_bloc.dart'; |
||||
|
import 'package:go_router/go_router.dart'; |
||||
|
import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; |
||||
|
import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; |
||||
|
import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/routers/my_routes.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/status/base_status.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/widgets/button/my_blue_button.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/widgets/error/error_state.dart'; |
||||
|
import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_bloc.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_event.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_bloc.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_event.dart'; |
||||
|
import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_state.dart'; |
||||
|
import 'package:wheel_chooser/wheel_chooser.dart'; |
||||
|
|
||||
|
import '../../../../core/widgets/animations/rotation_anim.dart'; |
||||
|
import '../../domain/entity/language_entity.dart'; |
||||
|
|
||||
|
class LanguagePage extends StatefulWidget { |
||||
|
const LanguagePage({super.key}); |
||||
|
|
||||
|
@override |
||||
|
State<LanguagePage> createState() => _LanguagePageState(); |
||||
|
} |
||||
|
|
||||
|
class _LanguagePageState extends State<LanguagePage> { |
||||
|
final controller = FixedExtentScrollController(initialItem: 50); |
||||
|
bool _isEnabled = false; |
||||
|
LanguageEntity? _selectedLanguage; |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
_selectedLanguage = |
||||
|
context.read<LanguageBloc>().state.selectedLanguage ?? |
||||
|
const LanguageEntity( |
||||
|
code: 'en', |
||||
|
title: '', |
||||
|
displayName: 'English (English', |
||||
|
); |
||||
|
context.read<LanguageBloc>().add(const GetLanguagesEvent()); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void dispose() { |
||||
|
controller.dispose(); |
||||
|
super.dispose(); |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
body: Container( |
||||
|
height: context.heightScreen, |
||||
|
width: context.widthScreen, |
||||
|
decoration: BoxDecoration( |
||||
|
gradient: const LinearGradient( |
||||
|
begin: Alignment.topCenter, |
||||
|
end: Alignment.bottomCenter, |
||||
|
colors: [Color(0XFF00154C), Color(0XFF150532)], |
||||
|
), |
||||
|
image: DecorationImage( |
||||
|
image: const AssetImage(MyAssets.pattern), |
||||
|
scale: 3, |
||||
|
repeat: ImageRepeat.repeat, |
||||
|
colorFilter: ColorFilter.mode( |
||||
|
Colors.white.withValues(alpha: 0.2), |
||||
|
BlendMode.srcIn, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
child: BlocConsumer<LanguageBloc, LanguageState>( |
||||
|
listener: (context, state) async { |
||||
|
if (state.languages.isNotEmpty && !_isEnabled) { |
||||
|
int targetIndex = 0; |
||||
|
if (state.selectedLanguage != null) { |
||||
|
targetIndex = state.languages.indexWhere( |
||||
|
(l) => l.code == state.selectedLanguage!.code, |
||||
|
); |
||||
|
if (targetIndex == -1) targetIndex = 0; |
||||
|
} |
||||
|
|
||||
|
await Future.delayed(const Duration(milliseconds: 50)); |
||||
|
if (controller.hasClients) { |
||||
|
await controller.animateToItem( |
||||
|
targetIndex, |
||||
|
duration: const Duration(seconds: 1), |
||||
|
curve: Curves.easeOutCirc, |
||||
|
); |
||||
|
|
||||
|
if (state.selectedLanguage == null && context.mounted) { |
||||
|
context.read<LanguageBloc>().add( |
||||
|
SelectLanguageEvent(state.languages[targetIndex]), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
setState(() => _isEnabled = true); |
||||
|
} |
||||
|
}, |
||||
|
builder: (context, state) { |
||||
|
if (state.getLanguagesStatus is BaseLoading) { |
||||
|
return Center(child: _loading(context)); |
||||
|
} |
||||
|
if (state.getLanguagesStatus is BaseError) { |
||||
|
return Padding( |
||||
|
padding: EdgeInsets.symmetric( |
||||
|
vertical: |
||||
|
MediaQuery.viewPaddingOf(context).bottom + MySpaces.s16, |
||||
|
horizontal: 60, |
||||
|
), |
||||
|
child: ErrorState( |
||||
|
onTap: () { |
||||
|
context.read<LanguageBloc>().add(const GetLanguagesEvent()); |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
final double itemSize = |
||||
|
setSize(context: context, mobile: 45, tablet: 60) ?? 45; |
||||
|
final selectStyle = MYTextStyle.titr1.copyWith( |
||||
|
// Keep same size as before (WheelChooser.choices) |
||||
|
fontSize: itemSize * 0.34, |
||||
|
color: Colors.white, |
||||
|
); |
||||
|
final unSelectStyle = MYTextStyle.titr1.copyWith( |
||||
|
fontSize: itemSize * 0.34, |
||||
|
color: Colors.white.withValues(alpha: 0.5), |
||||
|
); |
||||
|
return Padding( |
||||
|
padding: EdgeInsets.only( |
||||
|
left: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, |
||||
|
right: |
||||
|
setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, |
||||
|
bottom: MySpaces.s40 + MediaQuery.paddingOf(context).bottom, |
||||
|
top: 60, |
||||
|
), |
||||
|
child: Column( |
||||
|
children: [ |
||||
|
_title(context), |
||||
|
const SizedBox(height: 72), |
||||
|
Expanded( |
||||
|
child: ShaderMask( |
||||
|
shaderCallback: (rect) { |
||||
|
return const LinearGradient( |
||||
|
begin: Alignment.topCenter, |
||||
|
end: Alignment.bottomCenter, |
||||
|
colors: [ |
||||
|
Color(0x00FFF7FA), |
||||
|
Color(0x1AFFF7FA), |
||||
|
Color(0xFFFFFFFF), |
||||
|
Color(0x1AFEFCFD), |
||||
|
Color(0x00FEFCFD), |
||||
|
], |
||||
|
).createShader(rect); |
||||
|
}, |
||||
|
child: Stack( |
||||
|
children: [ |
||||
|
Center( |
||||
|
child: Container( |
||||
|
width: double.infinity, |
||||
|
alignment: Alignment.centerLeft, |
||||
|
height: itemSize, |
||||
|
padding: const EdgeInsets.symmetric( |
||||
|
horizontal: 18, |
||||
|
), |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.white.withValues(alpha: .2), |
||||
|
borderRadius: BorderRadius.circular(12), |
||||
|
), |
||||
|
child: Transform.translate( |
||||
|
offset: |
||||
|
const Offset(-4, 0), |
||||
|
child: const MyImage(image: MyAssets.check), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
WheelChooser.custom( |
||||
|
controller: controller, |
||||
|
datas: null, |
||||
|
startPosition: null, |
||||
|
perspective: 0.00000001, |
||||
|
listWidth: double.infinity, |
||||
|
listHeight: double.infinity, |
||||
|
itemSize: itemSize, |
||||
|
isInfinite: true, |
||||
|
onValueChanged: (position) { |
||||
|
if (state.languages.isEmpty) return; |
||||
|
final pos = position is int |
||||
|
? position |
||||
|
: int.tryParse(position.toString()) ?? 0; |
||||
|
final index = |
||||
|
((pos % state.languages.length) + |
||||
|
state.languages.length) % |
||||
|
state.languages.length; |
||||
|
final lang = state.languages[index]; |
||||
|
setState(() { |
||||
|
_selectedLanguage = lang; |
||||
|
}); |
||||
|
}, |
||||
|
children: state.languages.map((lang) { |
||||
|
final isSelected = |
||||
|
_selectedLanguage?.code == lang.code; |
||||
|
return Center( |
||||
|
child: Padding( |
||||
|
padding: const EdgeInsets.symmetric( |
||||
|
horizontal: 36, |
||||
|
), |
||||
|
child: MediaQuery( |
||||
|
data: MediaQuery.of(context).copyWith( |
||||
|
textScaler: const TextScaler.linear(1.5), |
||||
|
), |
||||
|
child: AutoSizeText( |
||||
|
lang.displayName, |
||||
|
textAlign: TextAlign.center, |
||||
|
maxLines: 2, |
||||
|
minFontSize: 10, |
||||
|
stepGranularity: 0.5, |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
style: isSelected |
||||
|
? selectStyle |
||||
|
: unSelectStyle, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
}).toList(), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
const SizedBox(height: 72), |
||||
|
_btn(context, state), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _loading(BuildContext context) { |
||||
|
return RotationAnim( |
||||
|
child: MyImage( |
||||
|
image: MyAssets.loading, |
||||
|
size: setSize(context: context, mobile: 110, tablet: 145), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _title(BuildContext context) { |
||||
|
return Row( |
||||
|
spacing: MySpaces.s10, |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
children: [ |
||||
|
const MyImage(image: MyAssets.lang, size: 28), |
||||
|
AutoSizeText( |
||||
|
context.translate.select_language, |
||||
|
minFontSize: 12, |
||||
|
maxFontSize: 20, |
||||
|
maxLines: 1, |
||||
|
textAlign: TextAlign.center, |
||||
|
style: MYTextStyle.titr0.copyWith(color: const Color(0XFF847AC4)), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Widget _btn(BuildContext context, LanguageState state) { |
||||
|
return MyBlueButton( |
||||
|
onTap: (_isEnabled && _selectedLanguage != null) |
||||
|
? () async { |
||||
|
context.read<LanguageBloc>().add( |
||||
|
SelectLanguageEvent(_selectedLanguage!), |
||||
|
); |
||||
|
final downloadBloc = context.read<DownloadBloc>(); |
||||
|
// 1. Cancel any previous downloads. |
||||
|
downloadBloc.add(CancelDownloadEvent()); |
||||
|
|
||||
|
// 2. Update App Locale |
||||
|
context.read<AppBloc>().add( |
||||
|
ChangeLocaleEvent(_selectedLanguage!.locale), |
||||
|
); |
||||
|
await Future.delayed(const Duration(milliseconds: 80)); |
||||
|
final lastDownloadedLevel = await downloadBloc |
||||
|
.lastDownloadedLevel(); |
||||
|
|
||||
|
if (!context.mounted) return; |
||||
|
|
||||
|
if (lastDownloadedLevel >= MyConstants.firstDownloadBatchCount) { |
||||
|
context.goNamed(Routes.homePage); |
||||
|
} else { |
||||
|
context.read<DownloadBloc>().add( |
||||
|
const StartDownloadEvent( |
||||
|
toLevel: MyConstants.firstDownloadBatchCount, |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
context.goNamed( |
||||
|
Routes.downloadPage, |
||||
|
extra: const DownloadPageConfig( |
||||
|
downloadToLevel: MyConstants.firstDownloadBatchCount, |
||||
|
redirectTo: Routes.homePage, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
: null, |
||||
|
title: context.translate.select, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue