Complete refactor to clean up project

This commit is contained in:
2026-02-04 11:36:02 +01:00
parent 4f380b5444
commit 704bd0b928
29 changed files with 2057 additions and 1464 deletions

View File

@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:logging/logging.dart';
import 'package:sheetless/core/services/api_client.dart';
import 'package:sheetless/core/services/storage_service.dart';
import '../home/home_page.dart';
/// Login page for authenticating with the Sheetless server.
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _log = Logger('LoginPage');
final _formKey = GlobalKey<FormState>();
final _storageService = StorageService();
final _urlController = TextEditingController(
text: 'https://sheetless.julian-mutter.de',
);
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
String? _errorMessage;
bool _isLoggingIn = false;
@override
void initState() {
super.initState();
_tryAutoLogin();
}
@override
void dispose() {
_urlController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
// ---------------------------------------------------------------------------
// Auto-Login
// ---------------------------------------------------------------------------
/// Attempts to auto-login using a stored JWT token.
Future<void> _tryAutoLogin() async {
final jwt = await _storageService.readSecure(SecureStorageKey.jwt);
if (jwt != null && _isTokenValid(jwt)) {
await _navigateToHome();
return;
}
// Restore saved credentials for convenience
await _restoreCredentials();
}
/// Checks if a JWT token is still valid (not expired).
bool _isTokenValid(String jwt) {
try {
return !JwtDecoder.isExpired(jwt);
} on FormatException {
return false;
}
}
/// Restores previously saved URL and username for convenience.
Future<void> _restoreCredentials() async {
final url = await _storageService.readSecure(SecureStorageKey.url);
final username = await _storageService.readSecure(SecureStorageKey.email);
if (url != null) _urlController.text = url;
if (username != null) _usernameController.text = username;
}
// ---------------------------------------------------------------------------
// Login
// ---------------------------------------------------------------------------
/// Handles the login button press.
Future<void> _handleLogin() async {
if (_isLoggingIn) return;
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoggingIn = true;
_errorMessage = null;
});
try {
final apiClient = ApiClient(baseUrl: _urlController.text);
await apiClient.login(_usernameController.text, _passwordController.text);
// Save credentials for next time
await _saveCredentials(apiClient.token!);
await _navigateToHome();
} catch (e) {
_log.warning('Login failed', e);
setState(() {
_errorMessage = 'Login failed.\n$e';
});
} finally {
if (mounted) {
setState(() => _isLoggingIn = false);
}
}
}
/// Saves credentials after successful login.
Future<void> _saveCredentials(String token) async {
await _storageService.writeSecure(
SecureStorageKey.url,
_urlController.text,
);
await _storageService.writeSecure(SecureStorageKey.jwt, token);
await _storageService.writeSecure(
SecureStorageKey.email,
_usernameController.text,
);
}
/// Navigates to the home page after successful authentication.
Future<void> _navigateToHome() async {
final config = await _storageService.readConfig();
if (!mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => HomePage(config: config)),
);
}
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
String? _validateNotEmpty(String? value) {
if (value == null || value.isEmpty) {
return 'This field is required';
}
return null;
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildUrlField(),
const SizedBox(height: 16),
_buildUsernameField(),
const SizedBox(height: 16),
_buildPasswordField(),
const SizedBox(height: 24),
_buildLoginButton(),
if (_errorMessage != null) _buildErrorMessage(),
],
),
),
),
),
);
}
Widget _buildUrlField() {
return TextFormField(
controller: _urlController,
validator: _validateNotEmpty,
decoration: const InputDecoration(
labelText: 'Server URL',
prefixIcon: Icon(Icons.dns),
),
keyboardType: TextInputType.url,
textInputAction: TextInputAction.next,
);
}
Widget _buildUsernameField() {
return TextFormField(
controller: _usernameController,
validator: _validateNotEmpty,
decoration: const InputDecoration(
labelText: 'Username',
prefixIcon: Icon(Icons.person),
),
textInputAction: TextInputAction.next,
);
}
Widget _buildPasswordField() {
return TextFormField(
controller: _passwordController,
validator: _validateNotEmpty,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleLogin(),
);
}
Widget _buildLoginButton() {
return ElevatedButton(
onPressed: _isLoggingIn ? null : _handleLogin,
child: _isLoggingIn
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Login'),
);
}
Widget _buildErrorMessage() {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
);
}
}