import 'dart:async'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/account_model.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/login/lock_screen.dart'; import 'package:fladder/screens/login/login_edit_user.dart'; import 'package:fladder/screens/login/login_user_grid.dart'; import 'package:fladder/screens/login/widgets/discover_servers_widget.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/fladder_logo.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/screens/shared/passcode_input.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/auth_service.dart'; import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; @RoutePage() class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState { List users = const []; bool loading = false; String? invalidUrl = ""; bool startCheckingForErrors = false; bool addingNewUser = false; bool editingUsers = false; late final TextEditingController serverTextController = TextEditingController(text: ''); final usernameController = TextEditingController(); final passwordController = TextEditingController(); final FocusNode focusNode = FocusNode(); void startAddingNewUser() { setState(() { addingNewUser = true; editingUsers = false; }); if (FladderConfig.baseUrl != null) { serverTextController.text = FladderConfig.baseUrl!; _parseUrl(FladderConfig.baseUrl!); retrieveListOfUsers(); } } @override void initState() { super.initState(); Future.microtask(() { ref.read(userProvider.notifier).clear(); final currentAccounts = ref.read(authProvider.notifier).getSavedAccounts(); addingNewUser = currentAccounts.isEmpty; ref.read(lockScreenActiveProvider.notifier).update((state) => true); if (FladderConfig.baseUrl != null) { serverTextController.text = FladderConfig.baseUrl!; _parseUrl(FladderConfig.baseUrl!); retrieveListOfUsers(); } }); } @override Widget build(BuildContext context) { final loggedInUsers = ref.watch(authProvider.select((value) => value.accounts)); final authLoading = ref.watch(authProvider.select((value) => value.loading)); return Scaffold( appBar: const FladderAppBar(), floatingActionButton: !addingNewUser ? Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (!AdaptiveLayout.of(context).isDesktop) FloatingActionButton( key: const Key("edit_button"), child: const Icon(IconsaxPlusLinear.edit_2), onPressed: () => setState(() => editingUsers = !editingUsers), ), FloatingActionButton( key: const Key("new_button"), child: const Icon(IconsaxPlusLinear.add_square), onPressed: startAddingNewUser, ), ].addInBetween(const SizedBox(width: 16)), ) : null, body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 900), child: ListView( shrinkWrap: true, padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 32), children: [ const Center( child: FladderLogo(), ), AnimatedFadeSize( child: addingNewUser ? addUserFields(loggedInUsers, authLoading) : Column( key: UniqueKey(), mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ LoginUserGrid( users: loggedInUsers, editMode: editingUsers, onPressed: (user) async => tapLoggedInAccount(user), onLongPress: (user) => openUserEditDialogue(context, user), ), ], ), ), ].addPadding(const EdgeInsets.symmetric(vertical: 16)), ), ), ), ); } void _parseUrl(String url) { setState(() { ref.read(authProvider.notifier).setServer(""); users = []; if (url.isEmpty) { invalidUrl = ""; return; } if (!Uri.parse(url).isAbsolute) { invalidUrl = context.localized.invalidUrl; return; } if (!url.startsWith('https://') && !url.startsWith('http://')) { invalidUrl = context.localized.invalidUrlDesc; return; } invalidUrl = null; if (invalidUrl == null) { ref.read(authProvider.notifier).setServer(url.rtrim('/')); } }); } void openUserEditDialogue(BuildContext context, AccountModel user) { showDialog( context: context, builder: (context) => LoginEditUser( user: user, onTapServer: (value) { setState(() { _parseUrl(value); serverTextController.text = value; startAddingNewUser(); }); Navigator.of(context).pop(); }, ), ); } void tapLoggedInAccount(AccountModel user) async { switch (user.authMethod) { case Authentication.autoLogin: handleLogin(user); break; case Authentication.biometrics: final authenticated = await AuthService.authenticateUser(context, user); if (authenticated) { handleLogin(user); } break; case Authentication.passcode: if (context.mounted) { showPassCodeDialog(context, (newPin) { if (newPin == user.localPin) { handleLogin(user); } else { fladderSnackbar(context, title: context.localized.incorrectPinTryAgain); } }); } break; case Authentication.none: handleLogin(user); break; } } Future handleLogin(AccountModel user) async { await ref.read(authProvider.notifier).switchUser(); await ref.read(sharedUtilityProvider).updateAccountInfo(user.copyWith( lastUsed: DateTime.now(), )); ref.read(userProvider.notifier).updateUser(user.copyWith(lastUsed: DateTime.now())); loggedInGoToHome(); } void loggedInGoToHome() { ref.read(lockScreenActiveProvider.notifier).update((state) => false); if (context.mounted) { context.router.replaceAll([const DashboardRoute()]); } } Future Function()? get enterCredentialsTryLogin => emptyFields() ? null : () async { serverTextController.text = serverTextController.text.rtrim('/'); ref.read(authProvider.notifier).setServer(serverTextController.text.rtrim('/')); final response = await ref.read(authProvider.notifier).authenticateByName( usernameController.text, passwordController.text, ); if (response?.isSuccessful == false) { fladderSnackbar(context, title: "(${response?.base.statusCode}) ${response?.base.reasonPhrase ?? context.localized.somethingWentWrongPasswordCheck}"); } else if (response?.body != null) { loggedInGoToHome(); } }; bool emptyFields() => usernameController.text.isEmpty; void retrieveListOfUsers() async { serverTextController.text = serverTextController.text.rtrim('/'); ref.read(authProvider.notifier).setServer(serverTextController.text); setState(() => loading = true); final response = await ref.read(authProvider.notifier).getPublicUsers(); if ((response == null || response.isSuccessful == false) && context.mounted) { fladderSnackbar(context, title: response?.base.reasonPhrase ?? context.localized.unableToConnectHost); setState(() => startCheckingForErrors = true); } if (response?.body?.isEmpty == true) { await Future.delayed(const Duration(seconds: 1)); } setState(() { users = response?.body ?? []; loading = false; }); } Widget addUserFields(List accounts, bool authLoading) { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (accounts.isNotEmpty) Padding( padding: const EdgeInsets.all(8.0), child: IconButton.filledTonal( onPressed: () { setState(() { addingNewUser = false; loading = false; startCheckingForErrors = false; serverTextController.text = ""; usernameController.text = ""; passwordController.text = ""; invalidUrl = ""; }); ref.read(authProvider.notifier).setServer(""); }, icon: const Icon( IconsaxPlusLinear.arrow_left_2, ), ), ), if (FladderConfig.baseUrl == null) ...[ Flexible( child: OutlinedTextField( controller: serverTextController, onChanged: _parseUrl, onSubmitted: (value) => retrieveListOfUsers(), autoFillHints: const [AutofillHints.url], keyboardType: TextInputType.url, autocorrect: false, textInputAction: TextInputAction.go, label: context.localized.server, errorText: (invalidUrl == null || serverTextController.text.isEmpty || !startCheckingForErrors) ? null : invalidUrl, ), ), Padding( padding: const EdgeInsets.all(8.0), child: Tooltip( message: context.localized.retrievePublicListOfUsers, waitDuration: const Duration(seconds: 1), child: IconButton.filled( onPressed: () => retrieveListOfUsers(), icon: const Icon( IconsaxPlusLinear.refresh, ), ), ), ), ], ], ), AnimatedFadeSize( child: invalidUrl == null ? Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (loading || users.isNotEmpty) AnimatedFadeSize( duration: const Duration(milliseconds: 250), child: loading ? CircularProgressIndicator(key: UniqueKey(), strokeCap: StrokeCap.round) : LoginUserGrid( users: users, onPressed: (value) { usernameController.text = value.name; passwordController.text = ""; focusNode.requestFocus(); }, ), ), AutofillGroup( child: Column( children: [ OutlinedTextField( controller: usernameController, autoFillHints: const [AutofillHints.username], textInputAction: TextInputAction.next, autocorrect: false, onChanged: (value) => setState(() {}), label: context.localized.userName, ), OutlinedTextField( controller: passwordController, autoFillHints: const [AutofillHints.password], keyboardType: TextInputType.visiblePassword, focusNode: focusNode, autocorrect: false, textInputAction: TextInputAction.send, onSubmitted: (value) => enterCredentialsTryLogin?.call(), onChanged: (value) => setState(() {}), label: context.localized.password, ), FilledButton( onPressed: enterCredentialsTryLogin, child: authLoading ? SizedBox( width: 18, height: 18, child: CircularProgressIndicator( color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round), ) : Row( mainAxisSize: MainAxisSize.min, children: [ Text(context.localized.login), const SizedBox(width: 8), const Icon(IconsaxPlusBold.send_1), ], ), ), ].addPadding(const EdgeInsets.symmetric(vertical: 4)), ), ), ].addPadding(const EdgeInsets.symmetric(vertical: 10)), ) : DiscoverServersWidget( serverCredentials: accounts.map((e) => e.credentials).toList(), onPressed: (server) { serverTextController.text = server.address; _parseUrl(server.address); retrieveListOfUsers(); }, ), ) ].addPadding(const EdgeInsets.symmetric(vertical: 8)), ); } }