Browse Source
feat: implement portrait lock, pause media on navigation, update assets, and refactor AppDelegate for implicit engine support
develop
feat: implement portrait lock, pause media on navigation, update assets, and refactor AppDelegate for implicit engine support
develop
12 changed files with 40 additions and 352 deletions
-
1android/app/src/main/AndroidManifest.xml
-
BINassets/audios/home.mp3
-
BINassets/audios/home3.mp3
-
BINassets/audios/question.mp3
-
BINassets/audios/question3.mp3
-
2ios/Flutter/AppFrameworkInfo.plist
-
9ios/Podfile.lock
-
7ios/Runner/AppDelegate.swift
-
44ios/Runner/Info.plist
-
2lib/features/intro/presentation/bloc/intro_bloc.dart
-
325lib/features/language/presentation/ui/language_page.dart
-
2pubspec.yaml
@ -1,325 +0,0 @@ |
|||||
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