You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
decky/lib/screens/login_screen.dart

254 lines
8.1 KiB

import 'package:flutter/material.dart';
import '../env.dart';
import '../services/api_auth_service.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _serverUrlController = TextEditingController();
bool _loading = false;
String? _errorMessage;
bool _isRegister = false;
final _displayNameController = TextEditingController();
@override
void initState() {
super.initState();
_serverUrlController.text = apiBaseUrl;
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_serverUrlController.dispose();
_displayNameController.dispose();
super.dispose();
}
Future<void> _saveServerUrl() async {
final raw = _serverUrlController.text.trim().replaceAll(' ', '');
if (raw.isEmpty) {
await setApiBaseUrlOverride(null);
if (mounted) {
setState(() {
_serverUrlController.text = apiBaseUrlDefault;
_errorMessage = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Using default server URL')),
);
}
return;
}
final withoutSlash = raw.endsWith('/') ? raw.substring(0, raw.length - 1) : raw;
final uri = Uri.tryParse(withoutSlash);
if (uri == null ||
(!uri.scheme.startsWith('http') || uri.host.isEmpty)) {
if (mounted) {
setState(() {
_errorMessage = 'Enter a valid URL (e.g. https://example.com)';
});
}
return;
}
await setApiBaseUrlOverride(withoutSlash);
if (mounted) {
setState(() => _errorMessage = null);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Server URL saved')),
);
}
}
Future<void> _useDefaultServerUrl() async {
await setApiBaseUrlOverride(null);
if (mounted) {
setState(() {
_serverUrlController.text = apiBaseUrlDefault;
_errorMessage = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Using default server URL')),
);
}
}
Future<void> _submit() async {
setState(() {
_errorMessage = null;
_loading = true;
});
String? err;
if (_isRegister) {
err = await ApiAuthService.instance.register(
_emailController.text,
_passwordController.text,
_displayNameController.text,
);
} else {
err = await ApiAuthService.instance.login(
_emailController.text,
_passwordController.text,
);
}
if (!mounted) return;
setState(() {
_loading = false;
_errorMessage = err;
});
if (err == null) {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isRegister ? 'Create account' : 'Log in'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
if (_isRegister) ...[
TextFormField(
controller: _displayNameController,
decoration: const InputDecoration(
labelText: 'Display name (optional)',
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
],
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: _isRegister ? 'Email' : 'Email or username',
border: const OutlineInputBorder(),
),
keyboardType: _isRegister ? TextInputType.emailAddress : TextInputType.text,
textInputAction: TextInputAction.next,
validator: (v) {
if (v == null || v.trim().isEmpty) {
return _isRegister ? 'Enter your email.' : 'Enter your email or username.';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submit(),
validator: (v) {
if (v == null || v.isEmpty) return 'Enter your password.';
return null;
},
),
const SizedBox(height: 20),
Text(
'Server URL',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: _serverUrlController,
decoration: const InputDecoration(
hintText: 'Leave empty for app default',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.url,
autocorrect: false,
textInputAction: TextInputAction.done,
),
const SizedBox(height: 8),
Row(
children: [
FilledButton.tonal(
onPressed: _loading ? null : _saveServerUrl,
child: const Text('Save server URL'),
),
const SizedBox(width: 12),
TextButton(
onPressed: _loading ? null : _useDefaultServerUrl,
child: const Text('Use default'),
),
],
),
if (_errorMessage != null) ...[
const SizedBox(height: 16),
Text(
_errorMessage!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
],
const SizedBox(height: 24),
FilledButton(
onPressed: _loading
? null
: () {
if (_formKey.currentState?.validate() ?? false) {
_submit();
}
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _loading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(_isRegister ? 'Create account' : 'Log in'),
),
const SizedBox(height: 16),
TextButton(
onPressed: _loading
? null
: () {
setState(() {
_isRegister = !_isRegister;
_errorMessage = null;
});
},
child: Text(
_isRegister
? 'Already have an account? Log in'
: 'No account? Create one',
),
),
],
),
),
),
),
);
}
}

Powered by TurnKey Linux.