add language section for onboarding

This commit is contained in:
mohamadmahdi jebeli 2025-09-22 09:46:20 +03:30
parent 9a4c51dafb
commit 4bc390c3e3
6 changed files with 248 additions and 54 deletions

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../extension/screenSize.dart'; import '../../extension/screenSize.dart';
import '../../gen/assets.gen.dart'; import '../../gen/assets.gen.dart';
import '../../res/colors.dart'; import '../../res/colors.dart';
import '../../widgets/language_selection_dialog.dart';
import 'login_page.dart'; import 'login_page.dart';
class OnboardingPage extends StatefulWidget { class OnboardingPage extends StatefulWidget {
@ -16,6 +18,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
int currentIndex = 0; int currentIndex = 0;
final PageController _pageController = PageController(); final PageController _pageController = PageController();
String _currentLanguage = '🇺🇲 English';
String _currentFlag = 'assets/icons/usa circle.svg';
final GlobalKey _languageKey = GlobalKey();
final List<String> imageAssets = [ final List<String> imageAssets = [
Assets.images.ounboarding1.path, Assets.images.ounboarding1.path,
Assets.images.frame.path, Assets.images.frame.path,
@ -35,6 +41,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
} else { } else {
_markOnboardingComplete();
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => LoginPage()), MaterialPageRoute(builder: (context) => LoginPage()),
@ -42,6 +49,16 @@ class _OnboardingPageState extends State<OnboardingPage> {
} }
} }
Future<void> _markOnboardingComplete() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('hasSeenOnboarding', true);
}
Future<void> _resetOnboarding() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('hasSeenOnboarding');
}
void _back() { void _back() {
if (currentIndex > 0) { if (currentIndex > 0) {
_pageController.previousPage( _pageController.previousPage(
@ -59,6 +76,109 @@ class _OnboardingPageState extends State<OnboardingPage> {
return Scaffold( return Scaffold(
body: Column( body: Column(
children: [ children: [
Container(
padding: EdgeInsets.fromLTRB(
width / 15,
height / 20,
width / 15,
0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(width: 50),
GestureDetector(
key: _languageKey,
onTap: () {
showLanguageSelectionOverlay(context, _currentLanguage, (
language,
flag,
) {
setState(() {
_currentLanguage = language;
_currentFlag = flag;
});
}, _languageKey);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: ClipRRect(
key: ValueKey(_currentFlag),
borderRadius: BorderRadius.circular(4),
child:
_currentFlag == 'placeholder'
? Container(
width: 24,
height: 18,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.flag,
color: AppColors.primary,
size: 12,
),
)
: _currentFlag.endsWith('.svg')
? SvgPicture.asset(
_currentFlag,
width: 24,
height: 18,
)
: Image.asset(
_currentFlag,
width: 24,
height: 18,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 24,
height: 18,
decoration: BoxDecoration(
color: AppColors.greyBorder
.withOpacity(0.2),
borderRadius: BorderRadius.circular(
4,
),
),
child: Icon(
Icons.flag,
color: AppColors.textSecondary,
size: 12,
),
);
},
),
),
),
const SizedBox(width: 8),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
_currentLanguage,
key: ValueKey(_currentLanguage),
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
),
),
),
const SizedBox(width: 8),
SvgPicture.asset(
Assets.icons.arrowRight.path,
color: AppColors.textPrimary,
),
],
),
),
],
),
),
Expanded( Expanded(
child: PageView.builder( child: PageView.builder(
controller: _pageController, controller: _pageController,
@ -72,7 +192,8 @@ class _OnboardingPageState extends State<OnboardingPage> {
return Padding( return Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
width / 15, width / 15,
height / 40, height /
60,
width / 15, width / 15,
height / 30, height / 30,
), ),

View File

@ -115,6 +115,19 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
void _onCardSelected(HuntCard card) async { void _onCardSelected(HuntCard card) async {
final huntProvider = Provider.of<HuntState>(context, listen: false); final huntProvider = Provider.of<HuntState>(context, listen: false);
// If the same card is selected, deselect it
if (huntProvider.selectedCard?.id == card.id) {
// Play sound for deselection too
await GameSoundService.playCardFlipSound();
Vibration.hasVibrator().then((hasVibrator) {
if (hasVibrator == true) {
Vibration.vibrate(duration: 50); // Lighter vibration for deselect
}
});
huntProvider.deselectCard();
return;
}
await GameSoundService.playCardFlipSound(); await GameSoundService.playCardFlipSound();
Vibration.hasVibrator().then((hasVibrator) { Vibration.hasVibrator().then((hasVibrator) {
if (hasVibrator == true) { if (hasVibrator == true) {
@ -243,6 +256,7 @@ class _HuntContentState extends State<_HuntContent> with TickerProviderStateMixi
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: HuntCardWidget( child: HuntCardWidget(
key: ValueKey(card.id),
card: card, card: card,
onTap: () => _onCardSelected(card), onTap: () => _onCardSelected(card),
isSelected: huntProvider.selectedCard?.id == card.id, isSelected: huntProvider.selectedCard?.id == card.id,

View File

@ -62,6 +62,11 @@ class HuntState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void deselectCard() {
_selectedCard = null;
notifyListeners();
}
void startHunt() { void startHunt() {
if (_selectedCard != null) { if (_selectedCard != null) {
_huntStartTime = DateTime.now(); _huntStartTime = DateTime.now();

View File

@ -84,7 +84,21 @@ class _HuntCardWidgetState extends State<HuntCardWidget>
@override @override
void didUpdateWidget(HuntCardWidget oldWidget) { void didUpdateWidget(HuntCardWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.isFlipped != oldWidget.isFlipped) {
// Handle selection state changes first
if (widget.isSelected != oldWidget.isSelected) {
if (widget.isSelected) {
// Card is being selected
_flipController.forward();
} else {
// Card is being deselected - animate back to front
_flipController.reverse();
_scaleController.reverse();
}
}
// Handle flip animation based on isFlipped property if not already handled by selection
if (widget.isFlipped != oldWidget.isFlipped && widget.isSelected == oldWidget.isSelected) {
if (widget.isFlipped) { if (widget.isFlipped) {
_flipController.forward(); _flipController.forward();
} else { } else {

View File

@ -3,12 +3,42 @@ import 'package:flutter/material.dart';
import 'package:lba/screens/auth/login_page.dart'; import 'package:lba/screens/auth/login_page.dart';
import 'package:lba/screens/auth/onboarding_page.dart'; import 'package:lba/screens/auth/onboarding_page.dart';
import 'package:lba/screens/mains/navigation/navigation.dart'; import 'package:lba/screens/mains/navigation/navigation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SimpleAuthGate extends StatelessWidget { class SimpleAuthGate extends StatefulWidget {
const SimpleAuthGate({super.key}); const SimpleAuthGate({super.key});
@override
State<SimpleAuthGate> createState() => _SimpleAuthGateState();
}
class _SimpleAuthGateState extends State<SimpleAuthGate> {
bool? _isFirstTime;
@override
void initState() {
super.initState();
_checkFirstTime();
}
Future<void> _checkFirstTime() async {
final prefs = await SharedPreferences.getInstance();
final hasSeenOnboarding = prefs.getBool('hasSeenOnboarding') ?? false;
setState(() {
_isFirstTime = !hasSeenOnboarding;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isFirstTime == null) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
return StreamBuilder<User?>( return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(), stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -42,12 +72,8 @@ class SimpleAuthGate extends StatelessWidget {
return const MainScreen(); return const MainScreen();
} }
return _shouldShowOnboarding() ? const OnboardingPage() : const LoginPage(); return _isFirstTime! ? const OnboardingPage() : const LoginPage();
}, },
); );
} }
bool _shouldShowOnboarding() {
return false;
}
} }

View File

@ -105,6 +105,17 @@ class _LanguageSelectionOverlayState extends State<LanguageSelectionOverlay>
final targetPosition = _getTargetPosition(); final targetPosition = _getTargetPosition();
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final overlayWidth = screenWidth * 0.45;
double? leftPosition;
double? rightPosition;
if (targetPosition.dx > screenWidth * 0.7) {
rightPosition = 16;
} else {
rightPosition = 24;
}
return AnimatedBuilder( return AnimatedBuilder(
animation: _animationController, animation: _animationController,
builder: (context, child) { builder: (context, child) {
@ -113,56 +124,59 @@ class _LanguageSelectionOverlayState extends State<LanguageSelectionOverlay>
child: GestureDetector( child: GestureDetector(
onTap: _closeOverlay, onTap: _closeOverlay,
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Container( child: Padding(
width: double.infinity, padding: const EdgeInsets.all(8.0),
height: double.infinity, child: SizedBox(
child: Stack( width: double.infinity,
children: [ height: double.infinity,
Positioned( child: Stack(
right: targetPosition.dx + 10, children: [
top: targetPosition.dy + 40, Positioned(
child: Transform.scale( left: leftPosition,
scale: _scaleAnimation.value, right: rightPosition,
child: Opacity( top: targetPosition.dy + 40,
opacity: _opacityAnimation.value, child: Transform.scale(
child: Container( scale: _scaleAnimation.value,
width: child: Opacity(
screenWidth * 0.45, // Less than half screen width opacity: _opacityAnimation.value,
decoration: BoxDecoration( child: Container(
color: AppColors.surface, width: overlayWidth,
borderRadius: BorderRadius.only( decoration: BoxDecoration(
bottomLeft: Radius.circular(16), color: AppColors.surface,
bottomRight: Radius.circular(16), borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), bottomLeft: Radius.circular(16),
), bottomRight: Radius.circular(16),
boxShadow: [ topLeft: Radius.circular(16),
BoxShadow(
color: AppColors.shadowColor,
blurRadius: 15,
spreadRadius: 3,
offset: const Offset(0, 5),
), ),
], boxShadow: [
), BoxShadow(
child: Column( color: AppColors.shadowColor,
mainAxisSize: MainAxisSize.min, blurRadius: 15,
children: spreadRadius: 3,
languages.map((language) { offset: const Offset(0, 5),
final isSelected = ),
_selectedLanguage == language['name']; ],
return _buildLanguageOption( ),
language['name']!, child: Column(
language['code']!, mainAxisSize: MainAxisSize.min,
language['flag']!, children:
isSelected, languages.map((language) {
); final isSelected =
}).toList(), _selectedLanguage == language['name'];
return _buildLanguageOption(
language['name']!,
language['code']!,
language['flag']!,
isSelected,
);
}).toList(),
),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
), ),
@ -177,7 +191,7 @@ class _LanguageSelectionOverlayState extends State<LanguageSelectionOverlay>
String flag, String flag,
bool isSelected, bool isSelected,
) { ) {
print('Building language option: $name with flag: $flag'); // Debug log print('Building language option: $name with flag: $flag');
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(