Compare commits

...

2 commits

Author SHA1 Message Date
827fcbbae6
clean up login page 2026-06-05 17:06:33 -04:00
7001796343
clean up verify page 2026-06-05 17:05:25 -04:00
3 changed files with 36 additions and 167 deletions

View file

@ -2,58 +2,43 @@ import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/models/requests/login_request.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
class LoginPage extends HookConsumerWidget { class LoginPage extends HookConsumerWidget {
final Uri homeserver; final Uri homeserver;
const LoginPage({super.key, required this.homeserver}); const LoginPage({super.key, required this.homeserver});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(ClientController.provider.notifier); final client = ref.watch(ClientController.provider.notifier);
final isLoggingIn = useState(false);
final hasError = useState(false);
final passwordFocusNode = useFocusNode();
final theme = Theme.of(context);
final isLoading = useState(false);
final username = useTextEditingController(); final username = useTextEditingController();
final password = useTextEditingController(); final password = useTextEditingController();
Future<void> tryLogin() async { final inputError = useState<String?>(null);
if (isLoggingIn.value) return;
isLoggingIn.value = true;
hasError.value = false;
Future<void> tryLogin() async {
isLoading.value = true;
try {
final error = await client.login( final error = await client.login(
LoginRequest( .new(
username: username.text, username: username.text,
password: password.text, password: password.text,
homeserverUrl: homeserver.origin, homeserverUrl: homeserver.origin,
), ),
); );
if (!context.mounted) return;
if (error != null) { if (error != null) {
hasError.value = true; inputError.value = error;
ScaffoldMessenger.of(context).showSnackBar( isLoading.value = false;
SnackBar(
content: Text(
"Login failed. Is your password right?\nError: $error",
style: TextStyle(color: theme.colorScheme.onErrorContainer),
),
backgroundColor: theme.colorScheme.errorContainer,
),
);
isLoggingIn.value = false;
} else { } else {
Navigator.of(context).pop(); if (context.mounted) Navigator.of(context).pop();
}
} finally {
isLoading.value = false;
} }
passwordFocusNode.requestFocus();
isLoggingIn.value = false;
} }
return Scaffold( return Scaffold(
@ -66,57 +51,24 @@ class LoginPage extends HookConsumerWidget {
body: AlertDialog( body: AlertDialog(
title: Text("Login to ${homeserver.host}"), title: Text("Login to ${homeserver.host}"),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
Text("Enter your login credentials:"),
SizedBox(height: 12),
TextField( TextField(
autofocus: true, autofocus: true,
textInputAction: TextInputAction.next, textInputAction: .next,
onChanged: (newVal) { decoration: .new(label: Text("Username")),
if (hasError.value) {
hasError.value = false;
}
},
decoration: InputDecoration(
label: Text("Username"),
focusedBorder: hasError.value
? OutlineInputBorder(
borderSide: BorderSide(color: theme.colorScheme.error),
)
: null,
enabledBorder: hasError.value
? OutlineInputBorder(
borderSide: BorderSide(color: theme.colorScheme.error),
)
: null,
),
controller: username, controller: username,
), ),
SizedBox(height: 12), SizedBox(height: 12),
TextField( TextField(
focusNode: passwordFocusNode, textInputAction: .done,
textInputAction: TextInputAction.done,
onSubmitted: (_) => tryLogin(), onSubmitted: (_) => tryLogin(),
onChanged: (newVal) {
if (hasError.value) {
hasError.value = false;
}
},
selectAllOnFocus: true, selectAllOnFocus: true,
decoration: InputDecoration( decoration: .new(
label: Text("Password"), label: Text("Password"),
focusedBorder: hasError.value errorText: inputError.value,
? OutlineInputBorder( errorMaxLines: 5,
borderSide: BorderSide(color: theme.colorScheme.error),
)
: null,
enabledBorder: hasError.value
? OutlineInputBorder(
borderSide: BorderSide(color: theme.colorScheme.error),
)
: null,
), ),
controller: password, controller: password,
obscureText: true, obscureText: true,
@ -125,7 +77,7 @@ class LoginPage extends HookConsumerWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: hasError.value ? null : tryLogin, onPressed: isLoading.value ? null : tryLogin,
child: Text("Sign In"), child: Text("Sign In"),
), ),
], ],

View file

@ -3,7 +3,6 @@ import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/form_text_input.dart";
class VerifyPage extends HookConsumerWidget { class VerifyPage extends HookConsumerWidget {
const VerifyPage({super.key}); const VerifyPage({super.key});
@ -11,7 +10,8 @@ class VerifyPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final passphraseController = useTextEditingController(); final passphraseController = useTextEditingController();
final isVerifying = useState(false); final isLoading = useState(false);
return Scaffold( return Scaffold(
appBar: Appbar(), appBar: Appbar(),
body: AlertDialog( body: AlertDialog(
@ -24,19 +24,17 @@ class VerifyPage extends HookConsumerWidget {
"Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.", "Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.",
), ),
SizedBox(height: 12), SizedBox(height: 12),
FormTextInput( TextField(
required: false,
autofocus: true, autofocus: true,
capitalize: true,
controller: passphraseController, controller: passphraseController,
obscure: true, obscureText: true,
title: "Recovery Key or Passphrase", decoration: .new(label: Text("Recovery Key or Passphrase")),
), ),
], ],
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: isVerifying.value onPressed: isLoading.value
? null ? null
: () async { : () async {
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
@ -49,7 +47,7 @@ class VerifyPage extends HookConsumerWidget {
), ),
); );
isVerifying.value = true; isLoading.value = true;
final error = await ref final error = await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
@ -57,7 +55,7 @@ class VerifyPage extends HookConsumerWidget {
snackbar.close(); snackbar.close();
if (error != null) { if (error != null) {
isVerifying.value = false; isLoading.value = false;
if (context.mounted) { if (context.mounted) {
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
.new( .new(

View file

@ -1,81 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class FormTextInput extends StatelessWidget {
final List<String? Function(String value)> extraValidators;
final TextEditingController? controller;
final TextInputType keyboardType;
final String? initialValue;
final bool readOnly;
final bool obscure;
final String? title;
final int? minLines;
final int? maxLength;
final bool outlined;
final int? maxLines;
final bool capitalize;
final bool required;
final bool autocorrect;
final void Function()? onTap;
final Widget? trailing;
final InputBorder? border;
final List<TextInputFormatter>? formatters;
final bool autofocus;
const FormTextInput({
super.key,
this.border,
this.controller,
this.autofocus = false,
this.title,
this.obscure = false,
this.readOnly = false,
this.extraValidators = const [],
this.keyboardType = TextInputType.text,
this.initialValue,
this.minLines,
this.capitalize = false,
this.maxLength,
this.formatters,
this.maxLines = 1,
this.outlined = true,
this.trailing,
this.onTap,
this.autocorrect = true,
this.required = true,
});
@override
Widget build(BuildContext context) => TextFormField(
autofocus: autofocus,
controller: controller,
keyboardType: keyboardType,
readOnly: readOnly,
minLines: minLines,
maxLines: maxLines,
maxLength: maxLength,
inputFormatters: formatters,
textCapitalization: capitalize ? .sentences : .none,
initialValue: initialValue,
autocorrect: autocorrect,
obscureText: obscure,
onTap: onTap,
decoration: .new(
labelText: title,
border: border ?? (outlined ? null : const UnderlineInputBorder()),
suffixIcon: trailing,
),
validator: (value) {
if ((value?.isEmpty ?? true) && required) {
return "This field is required";
}
for (final validator in extraValidators) {
final reason = validator(value!);
if (reason != null) return reason;
}
return null;
},
);
}