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
-
42ios/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