Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
f0b7f33b8d |
BIN
assets/gas.jpeg
Normal file
BIN
assets/gas.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
assets/wi-fi.gif
Normal file
BIN
assets/wi-fi.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
@ -15,9 +15,12 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: SplashScreen(), // Start with SplashScreen
|
||||
theme:ThemeData(
|
||||
fontFamily: 'Poppins',
|
||||
),
|
||||
home: const SplashScreen(), // Start with SplashScreen
|
||||
);
|
||||
}
|
||||
}
|
@ -1,51 +1,207 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:login_page/data/bg_data.dart'; // Import your bgList
|
||||
import 'package:login_page/screens/login_screen.dart'; // Make sure this import path is correct
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
final String email;
|
||||
|
||||
const HomePage({super.key, required this.email});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: true,
|
||||
title: Text('Hello $email'),
|
||||
centerTitle: true,
|
||||
title: Text("Dashboard"),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
// Logout Icon Button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: 'Logout',
|
||||
icon: Icon(Icons.logout, color: Colors.black),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Go back to login screen
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => LoginScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ListView.builder(
|
||||
itemCount: bgList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
height: 180,
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Welcome Section Container with Greeting Inside
|
||||
Container(
|
||||
padding: EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 4),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Greeting + Waving Hand Icon
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"Good Morning, Thereza",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Icon(Icons.waving_hand, size: 24, color: Colors.blue),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
|
||||
// Prepaid Info
|
||||
Text("Prepaid: 0794606921"),
|
||||
SizedBox(height: 5),
|
||||
Text("Credit (Ksh): 0"),
|
||||
Text("Net Points: 0"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Full Width Wholesome Image - No Background
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.asset(
|
||||
'assets/gas.jpeg',
|
||||
width: double.infinity,
|
||||
height: 250, // Fallback height for most devices
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Active Subscription Title
|
||||
Text(
|
||||
"Your active subscriptions",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
SizedBox(height: 10),
|
||||
|
||||
// Active Subscription Card
|
||||
Card(
|
||||
elevation: 3,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"KKWZBZVZ",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text("Sh850= 30Days UnlimiNET - 30 Days"),
|
||||
SizedBox(height: 5),
|
||||
Text("Used: 3.96 GB"),
|
||||
SizedBox(height: 5),
|
||||
Text("Expires: 14/06/2025 17:55"),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text("RECONNECT"),
|
||||
),
|
||||
),
|
||||
],
|
||||
image: DecorationImage(
|
||||
image: AssetImage(bgList[index]),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Enter Voucher Code
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Enter Voucher Code",
|
||||
suffixIcon: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text("CONNECT"),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Offers Title
|
||||
Text(
|
||||
"Offers",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
SizedBox(height: 10),
|
||||
|
||||
// Offers List
|
||||
_buildOfferCard("Daily FREE 20 Minutes UnlimiNET", "FREE"),
|
||||
_buildOfferCard("Sh5= 30Minutes UnlimiNET", "Sh5"),
|
||||
_buildOfferCard("Sh9= 1Hour UnlimiNET", "Sh9"),
|
||||
_buildOfferCard("Sh13= 2Hours UnlimiNET", "Sh13"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Reusable widget for each offer card
|
||||
Widget _buildOfferCard(String name, String price) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(vertical: 5),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text("1 Device"),
|
||||
],
|
||||
),
|
||||
price == "FREE"
|
||||
? OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
side: BorderSide(color: Colors.red),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text("FREE"),
|
||||
)
|
||||
: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
side: BorderSide(color: Colors.red),
|
||||
),
|
||||
onPressed: () {},
|
||||
child: Text("BUY"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../utils/text_utils.dart';
|
||||
import 'home.dart';
|
||||
import 'verify.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
@ -9,431 +11,99 @@ class LoginScreen extends StatefulWidget {
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
// State class for LoginScreen, with animation support for flipping forms
|
||||
class _LoginScreenState extends State<LoginScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool isDarkMode = false; // Tracks dark mode state
|
||||
bool isRemembered = false; // Tracks "Remember Me" checkbox
|
||||
bool isLogin = true; // Tracks if login or register form is shown
|
||||
bool isDarkMode = false;
|
||||
bool _phoneHasFocus = false;
|
||||
bool _phoneContainsText = false;
|
||||
|
||||
// Controllers for email and password input fields
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
|
||||
// Hardcoded credentials, updated on registration or password reset
|
||||
String _correctEmail = 'Amazons@tech.com';
|
||||
String _correctPassword = '12345';
|
||||
// Auto-generated verification code
|
||||
String? _generatedCode;
|
||||
|
||||
// Animation controller and animation for flipping between forms
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
// Timer variables
|
||||
late Timer _timer;
|
||||
int _countdown = 60;
|
||||
bool _isCounting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize animation controller for flip effect
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 600),
|
||||
);
|
||||
// Tween for flip animation
|
||||
_animation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose controllers to free resources
|
||||
_animationController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneController.dispose();
|
||||
if (_isCounting) _timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Login logic: checks credentials and navigates to HomePage if correct
|
||||
void _login() {
|
||||
if (_emailController.text == _correctEmail &&
|
||||
_passwordController.text == _correctPassword) {
|
||||
/// Validates phone number input
|
||||
String? _validatePhone(String value) {
|
||||
if (value.isEmpty) return 'Phone number cannot be empty';
|
||||
if (!RegExp(r'^\d{10}$').hasMatch(value)) {
|
||||
return 'Enter a valid 10-digit phone number';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Generates a random 4-digit code
|
||||
String _generateVerificationCode() {
|
||||
final random = DateTime.now().millisecondsSinceEpoch % 9000 + 1000;
|
||||
return random.toString();
|
||||
}
|
||||
|
||||
/// Starts a countdown timer for code expiration
|
||||
void _startCountdown() {
|
||||
if (_isCounting) return;
|
||||
|
||||
_isCounting = true;
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_countdown == 0) {
|
||||
_isCounting = false;
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() {
|
||||
_countdown--;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends user to Verification Screen with generated code
|
||||
void _connect() {
|
||||
final validation = _validatePhone(_phoneController.text);
|
||||
if (validation != null) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(validation)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate new code
|
||||
_generatedCode = _generateVerificationCode();
|
||||
|
||||
// Show snackbar and auto-fill in verification screen
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Auto-filled code: $_generatedCode")),
|
||||
);
|
||||
});
|
||||
|
||||
// Start countdown
|
||||
_countdown = 60;
|
||||
_startCountdown();
|
||||
|
||||
// Navigate to verification screen
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => HomePage(email: _emailController.text), // Go to HomePage
|
||||
builder:
|
||||
(context) => VerificationScreen(
|
||||
correctCode: _generatedCode!,
|
||||
autoFillCode: _generatedCode, // Pass generated code for auto-fill
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Show error if credentials are wrong
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Invalid credentials')));
|
||||
}
|
||||
}
|
||||
|
||||
// Registration logic: updates hardcoded credentials and flips back to login
|
||||
void _register() {
|
||||
if (_emailController.text.isNotEmpty &&
|
||||
_passwordController.text.isNotEmpty) {
|
||||
setState(() {
|
||||
_correctEmail = _emailController.text; // Update email
|
||||
_correctPassword = _passwordController.text; // Update password
|
||||
isLogin = true; // Switch to login form
|
||||
});
|
||||
_animationController.reverse(); // Animate back to login
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Registration successful! Please login')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Password reset logic: updates the hardcoded password
|
||||
void _resetPassword(String newPassword) {
|
||||
setState(() {
|
||||
_correctPassword = newPassword; // Update password
|
||||
});
|
||||
Navigator.pop(context); // Close dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Password updated successfully!')),
|
||||
);
|
||||
}
|
||||
|
||||
// Shows a dialog to enter a new password for reset
|
||||
void _showResetPasswordDialog() {
|
||||
final TextEditingController newPasswordController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
'Reset Password',
|
||||
style: TextStyle(color: isDarkMode ? Colors.white : Colors.black),
|
||||
),
|
||||
content: SizedBox(
|
||||
height: 100,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: newPasswordController, // Controller for new password
|
||||
obscureText: true, // Hide input
|
||||
decoration: InputDecoration(
|
||||
labelText: 'New Password',
|
||||
labelStyle: TextStyle(
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
border: OutlineInputBorder(),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
|
||||
actions: [
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
color: isDarkMode ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Update button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (newPasswordController.text.isNotEmpty) {
|
||||
_resetPassword(newPasswordController.text); // Update password
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Update',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFC76723),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Login form widget
|
||||
Widget _buildLoginForm() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 20), // Spacer
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
width: 200,
|
||||
child: Image.asset('assets/logo.png', fit: BoxFit.contain), // Logo
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Center(
|
||||
child: TextUtil(
|
||||
text: "Login",
|
||||
weight: true,
|
||||
size: 30,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextUtil(
|
||||
text: "Email",
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
Container(
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _emailController, // Email input
|
||||
style: TextStyle(color: isDarkMode ? Colors.white : Colors.black),
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: Icon(
|
||||
Icons.mail,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextUtil(
|
||||
text: "Password",
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
Container(
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _passwordController, // Password input
|
||||
obscureText: true, // Hide input
|
||||
style: TextStyle(color: isDarkMode ? Colors.white : Colors.black),
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: Icon(
|
||||
Icons.lock,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
// Remember Me checkbox
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isRemembered = !isRemembered;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
isRemembered ? Icons.check_box : Icons.check_box_outline_blank,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
size: 18,
|
||||
),
|
||||
label: TextUtil(
|
||||
text: "Remember Me",
|
||||
size: 12,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||
),
|
||||
const Spacer(),
|
||||
// Forget password button
|
||||
TextButton(
|
||||
onPressed: _showResetPasswordDialog, // Show reset dialog
|
||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||
child: TextUtil(
|
||||
text: "FORGET PASSWORD",
|
||||
size: 12,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Login button
|
||||
GestureDetector(
|
||||
onTap: _login, // Call login logic
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: TextUtil(
|
||||
text: "Log In",
|
||||
color: isDarkMode ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Switch to register form
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isLogin = false; // Show register form
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
});
|
||||
_animationController.forward(); // Animate flip
|
||||
},
|
||||
child: Center(
|
||||
child: TextUtil(
|
||||
text: "Don't have an account? REGISTER",
|
||||
size: 14,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Register form widget (flipped)
|
||||
Widget _buildRegisterForm() {
|
||||
return Transform(
|
||||
transform: Matrix4.identity()..rotateY(3.14159), // Flip effect
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
width: 200,
|
||||
child: Image.asset('assets/logo.png', fit: BoxFit.contain), // Logo
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: TextUtil(
|
||||
text: "Register",
|
||||
weight: true,
|
||||
size: 30,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextUtil(
|
||||
text: "Email",
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
Container(
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _emailController, // Email input
|
||||
style: TextStyle(color: isDarkMode ? Colors.white : Colors.black),
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: Icon(
|
||||
Icons.mail,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextUtil(
|
||||
text: "Password",
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
Container(
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _passwordController, // Password input
|
||||
obscureText: true, // Hide input
|
||||
style: TextStyle(color: isDarkMode ? Colors.white : Colors.black),
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: Icon(
|
||||
Icons.lock,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
// Register button
|
||||
GestureDetector(
|
||||
onTap: _register, // Call register logic
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: TextUtil(
|
||||
text: "Register Me",
|
||||
color: isDarkMode ? Colors.black : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Switch to login form
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isLogin = true; // Show login form
|
||||
_emailController.clear();
|
||||
_passwordController.clear();
|
||||
});
|
||||
_animationController.reverse(); // Animate flip
|
||||
},
|
||||
child: Center(
|
||||
child: TextUtil(
|
||||
text: "Already have an account? LOGIN",
|
||||
size: 14,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -441,20 +111,18 @@ class _LoginScreenState extends State<LoginScreen>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: isDarkMode ? ThemeData.dark() : ThemeData.light(), // Set theme
|
||||
data: isDarkMode ? ThemeData.dark() : ThemeData.light(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFFC76723), // Custom orange color
|
||||
backgroundColor: Color(0xFFC76723),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isDarkMode ? Icons.light_mode : Icons.dark_mode,
|
||||
icon: Icon(isDarkMode ? Icons.light_mode : Icons.dark_mode),
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isDarkMode = !isDarkMode; // Toggle theme
|
||||
isDarkMode = !isDarkMode;
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -463,40 +131,220 @@ class _LoginScreenState extends State<LoginScreen>
|
||||
body: Container(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
color: isDarkMode ? const Color.fromARGB(255, 26, 24, 24) : Colors.white, // Background color
|
||||
color: isDarkMode ? Color.fromARGB(255, 26, 24, 24) : Colors.white,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||
margin: EdgeInsets.symmetric(horizontal: 30),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkMode
|
||||
? const Color(0xFF23272F) // Use a dark blue/grey for dark mode
|
||||
: const Color.fromARGB(136, 71, 69, 69),
|
||||
color:
|
||||
isDarkMode
|
||||
? Color.fromARGB(255, 35, 38, 46)
|
||||
: Color.fromARGB(136, 71, 69, 69),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
color: isDarkMode
|
||||
? const Color.fromARGB(137, 132, 129, 129) // Use a dark blue/grey for dark mode
|
||||
color:
|
||||
isDarkMode
|
||||
? Color.fromARGB(133, 93, 75, 75)
|
||||
: Colors.white70,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
// Flip between login and register forms
|
||||
return Transform(
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateY(3.14159 * _animation.value),
|
||||
alignment: Alignment.center,
|
||||
child: isLogin ? _buildLoginForm() : _buildRegisterForm(),
|
||||
);
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo with tagline directly underneath
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Logo
|
||||
SizedBox(
|
||||
height: 100,
|
||||
width: 160,
|
||||
child: Image.asset(
|
||||
'assets/logo.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4), // Reduced space between logo and tagline
|
||||
// Tagline
|
||||
Text(
|
||||
'Fast . Reliable . Affordable',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.0,
|
||||
color:
|
||||
isDarkMode ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
// Removed the blue line and adjusted spacing
|
||||
const SizedBox(height: 24), // Added more space after tagline
|
||||
// "Sign in here" text
|
||||
TextUtil(
|
||||
text: "Sign in here",
|
||||
weight: true,
|
||||
size: 20,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextUtil(text: "Phone Number"),
|
||||
Container(
|
||||
height: 50,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkMode ? Colors.grey[800] : Colors.white,
|
||||
border: Border.all(
|
||||
color:
|
||||
_phoneHasFocus || _phoneContainsText
|
||||
? (isDarkMode
|
||||
? Colors.blueAccent
|
||||
: Colors.blue)
|
||||
: (isDarkMode ? Colors.white : Colors.black)
|
||||
.withOpacity(0.4),
|
||||
width: 1.5,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone,
|
||||
color: isDarkMode ? Colors.white : Colors.black,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
style: TextStyle(
|
||||
color:
|
||||
isDarkMode ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: "Enter phone number",
|
||||
counterText: "",
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
color:
|
||||
isDarkMode
|
||||
? Colors.white70
|
||||
: Colors.black45,
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_phoneContainsText = value.isNotEmpty;
|
||||
});
|
||||
},
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_phoneHasFocus = true;
|
||||
});
|
||||
},
|
||||
onEditingComplete: () {
|
||||
setState(() {
|
||||
_phoneHasFocus = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
GestureDetector(
|
||||
onTap: _connect,
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: TextUtil(text: "Connect", color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkMode ? Colors.white24 : Colors.black26,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color:
|
||||
isDarkMode ? Colors.grey[850] : Colors.grey[200],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.headset_mic,
|
||||
color:
|
||||
isDarkMode ? Colors.blueAccent : Colors.blue,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Need help? Contact support',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
isDarkMode
|
||||
? Colors.white70
|
||||
: Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDarkMode ? Colors.white24 : Colors.black26,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color:
|
||||
isDarkMode ? Colors.grey[850] : Colors.grey[200],
|
||||
),
|
||||
child: Text(
|
||||
'© Lence Amazons Ltd • All rights reserved • 2025',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
isDarkMode ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'login_screen.dart';
|
||||
import 'package:flutter/services.dart'; // For hiding system UI
|
||||
import 'login_screen.dart'; // Make sure this import path is correct
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
@ -12,41 +13,98 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Wait for 3 seconds, then navigate to LoginScreen
|
||||
// Hide system UI (status bar and bottom navigation bar)
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
// Navigate to Login Screen after 3 seconds
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
Navigator.pushReplacement(
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const LoginScreen(),
|
||||
),
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const LoginScreen()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white, // Transparent or match background
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 1.0, end: 1.2),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Main content in the middle (with Expanded to take available space)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Animated Logo
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 1.0, end: 1.2),
|
||||
duration: const Duration(milliseconds: 700),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
onEnd: () {
|
||||
setState(() {});
|
||||
return Transform.scale(scale: scale, child: child);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/splash.png',
|
||||
width: 150,
|
||||
'assets/logo.png',
|
||||
width: 200,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
// Loading Text
|
||||
const Text(
|
||||
"Just a moment...",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Animated GIF
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.8, end: 1.0),
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, scale, child) =>
|
||||
Transform.scale(scale: scale, child: child),
|
||||
child: Image.asset(
|
||||
'assets/wi-fi.gif',
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Info Text
|
||||
const Text(
|
||||
"Hang on as we redirect you to the WiFi Portal...",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer at the bottom
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
"© Lence Amazons Ltd All rights reserved.",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
186
lib/screens/verify.dart
Normal file
186
lib/screens/verify.dart
Normal file
@ -0,0 +1,186 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:login_page/screens/home.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class VerificationScreen extends StatefulWidget {
|
||||
final String correctCode;
|
||||
final String? autoFillCode;
|
||||
|
||||
const VerificationScreen({
|
||||
super.key,
|
||||
required this.correctCode,
|
||||
this.autoFillCode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VerificationScreen> createState() => _VerificationScreenState();
|
||||
}
|
||||
|
||||
class _VerificationScreenState extends State<VerificationScreen> {
|
||||
late List<TextEditingController> _controllers;
|
||||
late List<FocusNode> _focusNodes;
|
||||
|
||||
int _resendTimer = 60;
|
||||
late Timer _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize controllers and focus nodes
|
||||
_controllers = List.generate(4, (_) => TextEditingController());
|
||||
_focusNodes = List.generate(4, (_) => FocusNode());
|
||||
|
||||
// Auto-fill if provided
|
||||
if (widget.autoFillCode != null && widget.autoFillCode!.length == 4) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
_controllers[i].text = widget.autoFillCode![i];
|
||||
}
|
||||
}
|
||||
|
||||
// Start resend timer
|
||||
_startResendTimer();
|
||||
}
|
||||
|
||||
void _startResendTimer() {
|
||||
const oneSecond = Duration(seconds: 1);
|
||||
_timer = Timer.periodic(oneSecond, (timer) {
|
||||
if (_resendTimer <= 0) {
|
||||
setState(() {
|
||||
_resendTimer = 60;
|
||||
});
|
||||
timer.cancel();
|
||||
} else {
|
||||
setState(() {
|
||||
_resendTimer--;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Cancel the timer before disposing
|
||||
_timer.cancel();
|
||||
|
||||
// Dispose all controllers and focus nodes
|
||||
for (var c in _controllers) {
|
||||
c.dispose();
|
||||
}
|
||||
for (var f in _focusNodes) {
|
||||
f.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildDigitField(int index) {
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
child: TextField(
|
||||
controller: _controllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[index + 1]);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _verifyCode() {
|
||||
String enteredCode = _controllers.map((c) => c.text).join();
|
||||
|
||||
if (enteredCode == widget.correctCode) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => HomePage(email: "Verified")),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Invalid code")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildResendText() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"Resend code in: $_resendTimer seconds",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("Enter Code")),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Logo Section
|
||||
Image.asset('assets/logo.png', width: 200),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
"Fast. Reliable. Affordable",
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
// Instruction
|
||||
Text(
|
||||
"Enter verification code sent via SMS",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
// 4 Digit Input Fields
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) => _buildDigitField(index)),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
|
||||
// Verify Button
|
||||
ElevatedButton(
|
||||
onPressed: _verifyCode,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: Size(double.infinity, 50),
|
||||
),
|
||||
child: Text("Verify"),
|
||||
),
|
||||
|
||||
SizedBox(height: 20),
|
||||
_buildResendText(),
|
||||
|
||||
SizedBox(height: 30),
|
||||
Text(
|
||||
"Customer Service",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
"0702026554 | 0790882866",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user