mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
405 lines
15 KiB
Dart
405 lines
15 KiB
Dart
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<ConsumerStatefulWidget> createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends ConsumerState<LoginScreen> {
|
|
List<AccountModel> 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<void> 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<Null> 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<AccountModel> 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)),
|
|
);
|
|
}
|
|
}
|