Initial commit

This commit is contained in:
2025-07-13 14:01:29 +03:00
commit 0eaf91561a
188 changed files with 11616 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
import 'package:flutter/material.dart';
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
import 'package:neomovies_mobile/presentation/screens/auth/verify_screen.dart';
import 'package:provider/provider.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Account'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Login'),
Tab(text: 'Register'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_LoginForm(),
_RegisterForm(),
],
),
);
}
}
class _LoginForm extends StatefulWidget {
@override
__LoginFormState createState() => __LoginFormState();
}
class __LoginFormState extends State<_LoginForm> {
final _formKey = GlobalKey<FormState>();
String _email = '';
String _password = '';
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
Provider.of<AuthProvider>(context, listen: false).login(_email, _password);
}
}
@override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, auth, child) {
if (auth.needsVerification && auth.pendingEmail != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => VerifyScreen(email: auth.pendingEmail!),
),
);
auth.clearVerificationFlag();
});
} else if (auth.state == AuthState.authenticated) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop();
});
}
return Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) => value!.isEmpty ? 'Email is required' : null,
onSaved: (value) => _email = value!,
),
TextFormField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) => value!.isEmpty ? 'Password is required' : null,
onSaved: (value) => _password = value!,
),
const SizedBox(height: 20),
if (auth.state == AuthState.loading)
const CircularProgressIndicator()
else
ElevatedButton(
onPressed: _submit,
child: const Text('Login'),
),
if (auth.state == AuthState.error && auth.error != null)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(auth.error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
],
),
),
);
},
);
}
}
class _RegisterForm extends StatefulWidget {
@override
__RegisterFormState createState() => __RegisterFormState();
}
class __RegisterFormState extends State<_RegisterForm> {
final _formKey = GlobalKey<FormState>();
String _name = '';
String _email = '';
String _password = '';
void _submit() async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
try {
await Provider.of<AuthProvider>(context, listen: false)
.register(_name, _email, _password);
final auth = Provider.of<AuthProvider>(context, listen: false);
// Проверяем, что регистрация прошла успешно
if (auth.state != AuthState.error) {
// Переходим к экрану верификации
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => VerifyScreen(email: _email),
),
);
}
}
} catch (e) {
// Обрабатываем ошибку, если она произошла
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Registration error: ${e.toString()}'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, auth, child) {
return Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Name'),
validator: (value) => value!.isEmpty ? 'Name is required' : null,
onSaved: (value) => _name = value!,
),
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) => value!.isEmpty ? 'Email is required' : null,
onSaved: (value) => _email = value!,
),
TextFormField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) => value!.length < 6 ? 'Password must be at least 6 characters long' : null,
onSaved: (value) => _password = value!,
),
const SizedBox(height: 20),
if (auth.state == AuthState.loading)
const CircularProgressIndicator()
else
ElevatedButton(
onPressed: _submit,
child: const Text('Register'),
),
if (auth.state == AuthState.error && auth.error != null)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(auth.error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
import 'package:neomovies_mobile/presentation/screens/auth/login_screen.dart';
import 'package:provider/provider.dart';
import '../misc/licenses_screen.dart' as licenses;
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: Consumer<AuthProvider>(
builder: (context, authProvider, child) {
switch (authProvider.state) {
case AuthState.initial:
case AuthState.loading:
return const Center(child: CircularProgressIndicator());
case AuthState.unauthenticated:
return _buildUnauthenticatedView(context);
case AuthState.authenticated:
return _buildAuthenticatedView(context, authProvider);
case AuthState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: ${authProvider.error}'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => authProvider.checkAuthStatus(),
child: const Text('Try again'),
)
],
),
);
}
},
),
);
}
Widget _buildUnauthenticatedView(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Please log in to continue'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
},
child: const Text('Login or Register'),
),
const SizedBox(height: 40),
TextButton(
onPressed: () => _showLicensesScreen(context),
child: const Text('Libraries licenses'),
),
],
),
);
}
Widget _buildAuthenticatedView(BuildContext context, AuthProvider authProvider) {
final user = authProvider.user!;
final initial = user.name.isNotEmpty ? user.name[0].toUpperCase() : '?';
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: CircleAvatar(
radius: 40,
child: Text(initial, style: Theme.of(context).textTheme.headlineMedium),
),
),
const SizedBox(height: 16),
Center(
child: Text(user.name, style: Theme.of(context).textTheme.headlineSmall),
),
const SizedBox(height: 8),
Center(
child: Text(user.email, style: Theme.of(context).textTheme.bodyMedium),
),
const Spacer(),
TextButton(
onPressed: () => _showLicensesScreen(context),
child: const Text('Libraries licenses'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
authProvider.logout();
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer,
),
child: const Text('Logout'),
),
const SizedBox(height: 10),
OutlinedButton(
onPressed: () => _showDeleteConfirmationDialog(context, authProvider),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
side: BorderSide(color: Theme.of(context).colorScheme.error),
),
child: const Text('Delete account'),
),
],
),
);
}
void _showDeleteConfirmationDialog(BuildContext context, AuthProvider authProvider) {
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Delete account'),
content: const Text('Are you sure you want to delete your account? This action is irreversible.'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(dialogContext).pop();
},
),
TextButton(
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
child: const Text('Delete'),
onPressed: () {
Navigator.of(dialogContext).pop();
authProvider.deleteAccount();
},
),
],
);
},
);
}
void _showLicensesScreen(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const licenses.LicensesScreen(),
),
);
}
}

View File

@@ -0,0 +1,129 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:neomovies_mobile/presentation/providers/auth_provider.dart';
import 'package:provider/provider.dart';
class VerifyScreen extends StatefulWidget {
final String email;
const VerifyScreen({super.key, required this.email});
@override
State<VerifyScreen> createState() => _VerifyScreenState();
}
class _VerifyScreenState extends State<VerifyScreen> {
final _formKey = GlobalKey<FormState>();
String _code = '';
Timer? _timer;
int _resendCooldown = 60;
bool _canResend = false;
@override
void initState() {
super.initState();
_startCooldown();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startCooldown() {
_canResend = false;
_resendCooldown = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_resendCooldown > 0) {
setState(() {
_resendCooldown--;
});
} else {
setState(() {
_canResend = true;
});
timer.cancel();
}
});
}
void _resendCode() {
if (_canResend) {
// Here you would call the provider to resend the code
// For now, just restart the timer
_startCooldown();
}
}
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
Provider.of<AuthProvider>(context, listen: false)
.verifyEmail(widget.email, _code)
.then((_) {
final auth = Provider.of<AuthProvider>(context, listen: false);
if (auth.state != AuthState.error) {
Navigator.of(context).pop(); // Go back to LoginScreen
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Email verified. You can now login.')),
);
}
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Verify Email'),
),
body: Consumer<AuthProvider>(
builder: (context, auth, child) {
return Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Text('We sent a verification code to ${widget.email}. Enter it below.'),
const SizedBox(height: 20),
TextFormField(
decoration: const InputDecoration(labelText: 'Verification code'),
keyboardType: TextInputType.number,
validator: (value) => value!.isEmpty ? 'Enter code' : null,
onSaved: (value) => _code = value!,
),
const SizedBox(height: 20),
if (auth.state == AuthState.loading)
const CircularProgressIndicator()
else
ElevatedButton(
onPressed: _submit,
child: const Text('Verify'),
),
const SizedBox(height: 20),
TextButton(
onPressed: _canResend ? _resendCode : null,
child: Text(
_canResend
? 'Resend code'
: 'Resend code in $_resendCooldown seconds',
),
),
if (auth.state == AuthState.error && auth.error != null)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(auth.error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
),
],
),
),
);
},
),
);
}
}