mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
145
lib/screens/login/lock_screen.dart
Normal file
145
lib/screens/login/lock_screen.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/routes/build_routes/route_builder.dart';
|
||||
import 'package:fladder/screens/login/widgets/login_icon.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/screens/shared/passcode_input.dart';
|
||||
import 'package:fladder/util/auth_service.dart';
|
||||
|
||||
final lockScreenActiveProvider = StateProvider<bool>((ref) => false);
|
||||
|
||||
class LockScreen extends ConsumerStatefulWidget {
|
||||
final bool selfLock;
|
||||
const LockScreen({this.selfLock = false, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _LockScreenState();
|
||||
}
|
||||
|
||||
class _LockScreenState extends ConsumerState<LockScreen> with WidgetsBindingObserver {
|
||||
bool poppingLockScreen = false;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
hackyFixForBlackNavbar();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void hackyFixForBlackNavbar() {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarDividerColor: Colors.transparent,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
Future.microtask(() {
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => true);
|
||||
final user = ref.read(userProvider);
|
||||
if (user != null && !widget.selfLock) {
|
||||
tapLoggedInAccount(user);
|
||||
}
|
||||
});
|
||||
hackyFixForBlackNavbar();
|
||||
}
|
||||
|
||||
void handleLogin(AccountModel user) {
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => false);
|
||||
poppingLockScreen = true;
|
||||
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 && context.mounted) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = ref.watch(userProvider);
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) {
|
||||
if (!poppingLockScreen) {
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxOutline.lock_1,
|
||||
size: 38,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 400,
|
||||
maxWidth: 400,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(64.0),
|
||||
child: LoginIcon(
|
||||
user: user!,
|
||||
onPressed: () => tapLoggedInAccount(user),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ref.read(lockScreenActiveProvider.notifier).update((state) => false);
|
||||
context.routeGo(LoginRoute());
|
||||
},
|
||||
icon: const Icon(Icons.login_rounded),
|
||||
label: Text(context.localized.login),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
lib/screens/login/login_edit_user.dart
Normal file
99
lib/screens/login/login_edit_user.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:ficonsax/ficonsax.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/util/list_padding.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class LoginEditUser extends ConsumerWidget {
|
||||
final AccountModel user;
|
||||
final ValueChanged<String>? onTapServer;
|
||||
const LoginEditUser({required this.user, this.onTapServer, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AlertDialog.adaptive(
|
||||
title: Center(child: Text(user.name)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(),
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.dns_rounded),
|
||||
const SizedBox(width: 8),
|
||||
Text(user.credentials.serverName),
|
||||
],
|
||||
),
|
||||
if (user.credentials.server.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.http_rounded),
|
||||
const SizedBox(width: 8),
|
||||
Text(user.credentials.server),
|
||||
if (onTapServer != null) ...{
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filledTonal(
|
||||
onPressed: () {
|
||||
onTapServer?.call(user.credentials.server);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.send_rounded,
|
||||
),
|
||||
)
|
||||
}
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(user.authMethod.icon),
|
||||
const SizedBox(width: 8),
|
||||
Text(user.authMethod.name(context)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(IconsaxBold.clock),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(DateFormat.yMMMEd().format(user.lastUsed)),
|
||||
Text(DateFormat.Hms().format(user.lastUsed)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
Tooltip(
|
||||
message: "Removes the user and forces a logout",
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
await ref.read(sharedUtilityProvider).removeAccount(user);
|
||||
ref.read(authProvider.notifier).getSavedAccounts();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.remove_rounded),
|
||||
label: const Text("Remove user"),
|
||||
),
|
||||
),
|
||||
),
|
||||
].addPadding(const EdgeInsets.symmetric(vertical: 8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
393
lib/screens/login/login_screen.dart
Normal file
393
lib/screens/login/login_screen.dart
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/screens/login/lock_screen.dart';
|
||||
import 'package:fladder/screens/login/widgets/discover_servers_widget.dart';
|
||||
import 'package:fladder/screens/shared/fladder_logo.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.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/build_routes/home_routes.dart';
|
||||
import 'package:fladder/routes/build_routes/route_builder.dart';
|
||||
import 'package:fladder/screens/login/login_edit_user.dart';
|
||||
import 'package:fladder/screens/login/login_user_grid.dart';
|
||||
import 'package:fladder/screens/shared/animated_fade_size.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/auth_service.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_appbar.dart';
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
});
|
||||
}
|
||||
|
||||
@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: Key("edit_button"),
|
||||
child: Icon(IconsaxOutline.edit_2),
|
||||
onPressed: () => setState(() => editingUsers = !editingUsers),
|
||||
),
|
||||
FloatingActionButton(
|
||||
key: Key("new_button"),
|
||||
child: Icon(IconsaxOutline.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: [
|
||||
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();
|
||||
});
|
||||
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.routeGo(DashboardRoute());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Null> Function()? get enterCredentialsTryLogin => emptyFields()
|
||||
? null
|
||||
: () async {
|
||||
log('try login');
|
||||
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() {
|
||||
return usernameController.text.isEmpty || passwordController.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.end,
|
||||
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(
|
||||
IconsaxOutline.arrow_left_2,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
controller: serverTextController,
|
||||
onChanged: _parseUrl,
|
||||
onSubmitted: (value) => retrieveListOfUsers(),
|
||||
autoFillHints: const [AutofillHints.url],
|
||||
keyboardType: TextInputType.url,
|
||||
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(
|
||||
IconsaxOutline.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,
|
||||
onChanged: (value) => setState(() {}),
|
||||
label: context.localized.userName,
|
||||
),
|
||||
OutlinedTextField(
|
||||
controller: passwordController,
|
||||
autoFillHints: const [AutofillHints.password],
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: focusNode,
|
||||
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),
|
||||
Icon(IconsaxBold.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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
lib/screens/login/login_user_grid.dart
Normal file
149
lib/screens/login/login_user_grid.dart
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:reorderable_grid/reorderable_grid.dart';
|
||||
|
||||
class LoginUserGrid extends ConsumerWidget {
|
||||
final List<AccountModel> users;
|
||||
final bool editMode;
|
||||
final ValueChanged<AccountModel>? onPressed;
|
||||
final ValueChanged<AccountModel>? onLongPress;
|
||||
const LoginUserGrid({this.users = const [], this.onPressed, this.editMode = false, this.onLongPress, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mainAxisExtent = 175.0;
|
||||
final maxCount = (MediaQuery.of(context).size.width ~/ mainAxisExtent).clamp(1, 3);
|
||||
|
||||
return ReorderableGridView.builder(
|
||||
onReorder: (oldIndex, newIndex) => ref.read(authProvider.notifier).reOrderUsers(oldIndex, newIndex),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
autoScroll: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: users.length == 1 ? 1 : maxCount,
|
||||
mainAxisSpacing: 24,
|
||||
crossAxisSpacing: 24,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
),
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return _CardHolder(
|
||||
key: Key(user.id),
|
||||
content: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Flexible(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxBold.driver_2,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(SizedBox(width: 4, height: 4)),
|
||||
),
|
||||
if (editMode)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: const Icon(
|
||||
IconsaxBold.edit_2,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||
onLongPress: () => onLongPress?.call(user),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CardHolder extends StatelessWidget {
|
||||
final Widget content;
|
||||
final Function() onTap;
|
||||
final Function() onLongPress;
|
||||
|
||||
const _CardHolder({
|
||||
required this.content,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 1,
|
||||
shadowColor: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||
child: FlatButton(
|
||||
onTap: onTap,
|
||||
onLongPress: AdaptiveLayout.of(context).isDesktop ? onLongPress : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
lib/screens/login/widgets/discover_servers_widget.dart
Normal file
161
lib/screens/login/widgets/discover_servers_widget.dart
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:fladder/models/credentials_model.dart';
|
||||
import 'package:fladder/providers/discovery_provider.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/theme_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class DiscoverServersWidget extends ConsumerWidget {
|
||||
final List<CredentialsModel> serverCredentials;
|
||||
final Function(DiscoveryInfo server) onPressed;
|
||||
const DiscoverServersWidget({
|
||||
required this.serverCredentials,
|
||||
required this.onPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final existingServers = serverCredentials
|
||||
.map(
|
||||
(credentials) => DiscoveryInfo(
|
||||
id: credentials.serverId,
|
||||
name: credentials.serverName,
|
||||
address: credentials.server,
|
||||
endPointAddress: null),
|
||||
)
|
||||
.toSet()
|
||||
.toList();
|
||||
final discoverdServersStream = ref.watch(serverDiscoveryProvider);
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(6),
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
if (existingServers.isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
context.localized.saved,
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
Opacity(opacity: 0.65, child: Icon(IconsaxOutline.bookmark, size: 16)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...existingServers
|
||||
.map(
|
||||
(server) => _ServerInfoCard(
|
||||
server: server,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
.addInBetween(const SizedBox(height: 4)),
|
||||
const Divider(),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
context.localized.discovered,
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
Opacity(opacity: 0.65, child: Icon(IconsaxBold.airdrop, size: 16)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
discoverdServersStream.when(
|
||||
data: (data) {
|
||||
final servers = data.where((discoverdServer) => !existingServers.contains(discoverdServer));
|
||||
return servers.isNotEmpty
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...servers.map(
|
||||
(serverInfo) => _ServerInfoCard(
|
||||
server: serverInfo,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
)
|
||||
].toList().addInBetween(const SizedBox(height: 4)),
|
||||
)
|
||||
: Center(
|
||||
child: Opacity(
|
||||
opacity: 0.65,
|
||||
child: Text(
|
||||
context.localized.noServersFound,
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
));
|
||||
},
|
||||
error: (error, stackTrace) => Text(context.localized.error),
|
||||
loading: () => Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 24.0,
|
||||
child: CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ServerInfoCard extends StatelessWidget {
|
||||
final Function(DiscoveryInfo server) onPressed;
|
||||
final DiscoveryInfo server;
|
||||
const _ServerInfoCard({
|
||||
required this.server,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
onTap: () => onPressed(server),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Card(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
IconsaxBold.driver,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
server.name,
|
||||
style: context.textTheme.bodyLarge,
|
||||
),
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: Text(
|
||||
server.address,
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(IconsaxOutline.edit_2, size: 16)
|
||||
].addInBetween(const SizedBox(width: 12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/screens/login/widgets/login_icon.dart
Normal file
97
lib/screens/login/widgets/login_icon.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class LoginIcon extends ConsumerWidget {
|
||||
final AccountModel user;
|
||||
final Function()? onPressed;
|
||||
final Function()? onLongPress;
|
||||
final Function()? onNewPressed;
|
||||
const LoginIcon({
|
||||
required this.user,
|
||||
this.onPressed,
|
||||
this.onLongPress,
|
||||
this.onNewPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Card(
|
||||
elevation: 1,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: EdgeInsets.zero,
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.displayMedium,
|
||||
size: const Size(125, 125),
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (onNewPressed != null)
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 26,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.dns_rounded,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(SizedBox(width: 8, height: 8)),
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onTap: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue