From 3aafaea76d110b294a6e3ac1d41cf93f3fc64d0f Mon Sep 17 00:00:00 2001 From: aligopalpour13 Date: Wed, 13 May 2026 14:48:58 +0330 Subject: [PATCH] feat: add LanguagePage, enforce full-screen mode on iOS, and improve intro video aspect ratio handling --- .../presentation/ui/language_page.dart | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) diff --git a/lib/features/language/presentation/ui/language_page.dart b/lib/features/language/presentation/ui/language_page.dart index e69de29..eee449e 100644 --- a/lib/features/language/presentation/ui/language_page.dart +++ b/lib/features/language/presentation/ui/language_page.dart @@ -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 createState() => _LanguagePageState(); +} + +class _LanguagePageState extends State { + final controller = FixedExtentScrollController(initialItem: 50); + bool _isEnabled = false; + LanguageEntity? _selectedLanguage; + + @override + void initState() { + super.initState(); + _selectedLanguage = + context.read().state.selectedLanguage ?? + const LanguageEntity( + code: 'en', + title: '', + displayName: 'English (English', + ); + context.read().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( + 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().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().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().add( + SelectLanguageEvent(_selectedLanguage!), + ); + final downloadBloc = context.read(); + // 1. Cancel any previous downloads. + downloadBloc.add(CancelDownloadEvent()); + + // 2. Update App Locale + context.read().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().add( + const StartDownloadEvent( + toLevel: MyConstants.firstDownloadBatchCount, + ), + ); + + context.goNamed( + Routes.downloadPage, + extra: const DownloadPageConfig( + downloadToLevel: MyConstants.firstDownloadBatchCount, + redirectTo: Routes.homePage, + ), + ); + } + } + : null, + title: context.translate.select, + ); + } +}