332 lines
11 KiB
Dart
332 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:moonwell_launcher/app/launcher_version.dart';
|
||
import 'package:moonwell_launcher/app/design_system/mw_shells.dart';
|
||
import 'package:moonwell_launcher/app/theme/moonwell_design_system.dart';
|
||
|
||
enum MwAuthenticationMode { login, registration }
|
||
|
||
class MwAuthenticationForm extends StatefulWidget {
|
||
const MwAuthenticationForm({
|
||
super.key,
|
||
required this.usernameController,
|
||
required this.passwordController,
|
||
required this.onSubmit,
|
||
this.emailController,
|
||
this.passwordConfirmationController,
|
||
this.inviteCodeController,
|
||
this.mode = MwAuthenticationMode.login,
|
||
this.onModeChanged,
|
||
this.termsAccepted = false,
|
||
this.onTermsChanged,
|
||
this.loading = false,
|
||
this.error,
|
||
this.success,
|
||
});
|
||
|
||
final TextEditingController usernameController;
|
||
final TextEditingController passwordController;
|
||
final TextEditingController? emailController;
|
||
final TextEditingController? passwordConfirmationController;
|
||
final TextEditingController? inviteCodeController;
|
||
final VoidCallback onSubmit;
|
||
final MwAuthenticationMode mode;
|
||
final ValueChanged<MwAuthenticationMode>? onModeChanged;
|
||
final bool termsAccepted;
|
||
final ValueChanged<bool>? onTermsChanged;
|
||
final bool loading;
|
||
final String? error;
|
||
final String? success;
|
||
|
||
@override
|
||
State<MwAuthenticationForm> createState() => _MwAuthenticationFormState();
|
||
}
|
||
|
||
class _MwAuthenticationFormState extends State<MwAuthenticationForm> {
|
||
bool _remember = true;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final accent = MoonWellDesignTokens.of(context).accent;
|
||
final registering = widget.mode == MwAuthenticationMode.registration;
|
||
final launcherVersion = LauncherVersionScope.of(context);
|
||
return AutofillGroup(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
const MwBrandMark(width: 210, compact: true),
|
||
const SizedBox(height: 28),
|
||
Text(
|
||
registering ? 'РЕГИСТРАЦИЯ' : 'ВХОД',
|
||
style: TextStyle(color: accent, fontSize: 10, letterSpacing: 2.4),
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
registering ? 'Создать аккаунт' : 'С возвращением',
|
||
style: const TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: Color(0xFFECE4D0),
|
||
fontSize: 30,
|
||
letterSpacing: 1,
|
||
),
|
||
),
|
||
const SizedBox(height: 7),
|
||
Text(
|
||
registering
|
||
? 'Зарегистрируй игровой аккаунт MoonWell.'
|
||
: 'Войди в свой аккаунт, чтобы продолжить путь.',
|
||
style: const TextStyle(
|
||
fontFamily: 'Cormorant Garamond',
|
||
fontStyle: FontStyle.italic,
|
||
color: Color(0x80ECE4D0),
|
||
fontSize: 15,
|
||
),
|
||
),
|
||
const SizedBox(height: 22),
|
||
Row(
|
||
children: [
|
||
_Tab(
|
||
label: 'ВХОД',
|
||
selected: !registering,
|
||
accent: accent,
|
||
onTap: widget.loading
|
||
? null
|
||
: () => widget.onModeChanged?.call(
|
||
MwAuthenticationMode.login,
|
||
),
|
||
),
|
||
_Tab(
|
||
label: 'РЕГИСТРАЦИЯ',
|
||
selected: registering,
|
||
accent: accent,
|
||
onTap: widget.loading
|
||
? null
|
||
: () => widget.onModeChanged?.call(
|
||
MwAuthenticationMode.registration,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 18),
|
||
_FieldLabel(text: registering ? 'ЛОГИН' : 'ЛОГИН ИЛИ EMAIL'),
|
||
const SizedBox(height: 7),
|
||
TextField(
|
||
controller: widget.usernameController,
|
||
enabled: !widget.loading,
|
||
textInputAction: TextInputAction.next,
|
||
autofillHints: const [AutofillHints.username],
|
||
decoration: const InputDecoration(hintText: 'Aranthel'),
|
||
),
|
||
if (registering) ...[
|
||
const SizedBox(height: 14),
|
||
const _FieldLabel(text: 'EMAIL'),
|
||
const SizedBox(height: 7),
|
||
TextField(
|
||
controller: widget.emailController,
|
||
enabled: !widget.loading,
|
||
keyboardType: TextInputType.emailAddress,
|
||
textInputAction: TextInputAction.next,
|
||
autofillHints: const [AutofillHints.email],
|
||
decoration: const InputDecoration(hintText: 'player@example.com'),
|
||
),
|
||
],
|
||
const SizedBox(height: 14),
|
||
_FieldLabel(text: 'ПАРОЛЬ'),
|
||
const SizedBox(height: 7),
|
||
TextField(
|
||
controller: widget.passwordController,
|
||
enabled: !widget.loading,
|
||
obscureText: true,
|
||
textInputAction: registering
|
||
? TextInputAction.next
|
||
: TextInputAction.done,
|
||
autofillHints: [
|
||
registering ? AutofillHints.newPassword : AutofillHints.password,
|
||
],
|
||
onSubmitted: registering ? null : (_) => widget.onSubmit(),
|
||
),
|
||
if (registering) ...[
|
||
const SizedBox(height: 14),
|
||
const _FieldLabel(text: 'ПОВТОРИТЕ ПАРОЛЬ'),
|
||
const SizedBox(height: 7),
|
||
TextField(
|
||
controller: widget.passwordConfirmationController,
|
||
enabled: !widget.loading,
|
||
obscureText: true,
|
||
textInputAction: TextInputAction.next,
|
||
autofillHints: const [AutofillHints.newPassword],
|
||
),
|
||
const SizedBox(height: 14),
|
||
const _FieldLabel(text: 'ИНВАЙТ-КОД (НЕОБЯЗАТЕЛЬНО)'),
|
||
const SizedBox(height: 7),
|
||
TextField(
|
||
controller: widget.inviteCodeController,
|
||
enabled: !widget.loading,
|
||
textInputAction: TextInputAction.done,
|
||
onSubmitted: (_) => widget.onSubmit(),
|
||
),
|
||
],
|
||
if (widget.error != null) ...[
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
widget.error!,
|
||
style: const TextStyle(color: Color(0xFFD76A78), fontSize: 12),
|
||
),
|
||
],
|
||
if (widget.success != null) ...[
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
widget.success!,
|
||
style: const TextStyle(color: Color(0xFF6FC28A), fontSize: 12),
|
||
),
|
||
],
|
||
const SizedBox(height: 8),
|
||
if (registering)
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
width: 24,
|
||
child: Checkbox(
|
||
value: widget.termsAccepted,
|
||
onChanged: widget.loading
|
||
? null
|
||
: (value) =>
|
||
widget.onTermsChanged?.call(value ?? false),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Expanded(
|
||
child: Text(
|
||
'Я принимаю правила сервера и пользовательское соглашение',
|
||
style: TextStyle(color: Color(0x9AECE4D0), fontSize: 11),
|
||
),
|
||
),
|
||
],
|
||
)
|
||
else
|
||
Row(
|
||
children: [
|
||
SizedBox(
|
||
width: 24,
|
||
child: Checkbox(
|
||
value: _remember,
|
||
onChanged: widget.loading
|
||
? null
|
||
: (value) => setState(() => _remember = value ?? false),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'Запомнить меня',
|
||
style: TextStyle(color: Color(0x9AECE4D0), fontSize: 11),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
'Забыли пароль?',
|
||
style: TextStyle(color: accent, fontSize: 11),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 18),
|
||
SizedBox(
|
||
height: 54,
|
||
child: ElevatedButton(
|
||
onPressed: widget.loading ? null : widget.onSubmit,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF6D499E),
|
||
foregroundColor: const Color(0xFFECE4D0),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
),
|
||
child: widget.loading
|
||
? const SizedBox.square(
|
||
dimension: 18,
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
)
|
||
: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
registering ? 'СОЗДАТЬ АККАУНТ' : 'ВОЙТИ',
|
||
style: const TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
letterSpacing: 2.5,
|
||
),
|
||
),
|
||
const SizedBox(width: 14),
|
||
const Icon(Icons.arrow_forward, size: 18),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 28),
|
||
Center(
|
||
child: Text(
|
||
'MOONWELL · V $launcherVersion · PLAY.MOON-WELL.ONLINE',
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: Color(0x52ECE4D0),
|
||
fontSize: 8,
|
||
letterSpacing: 1.2,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _FieldLabel extends StatelessWidget {
|
||
const _FieldLabel({required this.text});
|
||
final String text;
|
||
@override
|
||
Widget build(BuildContext context) => Text(
|
||
text,
|
||
style: const TextStyle(
|
||
fontFamily: 'JetBrains Mono',
|
||
color: Color(0x80ECE4D0),
|
||
fontSize: 9,
|
||
letterSpacing: 1.5,
|
||
),
|
||
);
|
||
}
|
||
|
||
class _Tab extends StatelessWidget {
|
||
const _Tab({
|
||
required this.label,
|
||
required this.accent,
|
||
this.onTap,
|
||
this.selected = false,
|
||
});
|
||
final String label;
|
||
final Color accent;
|
||
final VoidCallback? onTap;
|
||
final bool selected;
|
||
@override
|
||
Widget build(BuildContext context) => Expanded(
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
child: Container(
|
||
height: 38,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: selected ? accent : Colors.transparent),
|
||
),
|
||
),
|
||
child: Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontFamily: 'Cinzel',
|
||
color: selected ? const Color(0xFFECE4D0) : const Color(0x80ECE4D0),
|
||
fontSize: 9,
|
||
letterSpacing: 1.5,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|