diff --git a/assets/info-logo.png b/assets/info-logo.png index 5971387c..1232a3c1 100644 Binary files a/assets/info-logo.png and b/assets/info-logo.png differ diff --git a/assets/login_wallpaper.jpg b/assets/login_wallpaper.jpg deleted file mode 100644 index b11de492..00000000 Binary files a/assets/login_wallpaper.jpg and /dev/null differ diff --git a/assets/login_wallpaper.png b/assets/login_wallpaper.png new file mode 100644 index 00000000..2ca8c39e Binary files /dev/null and b/assets/login_wallpaper.png differ diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 279df9f1..1200374c 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart'; +import 'package:fluffychat/pages/connect/connect_page.dart'; import 'package:fluffychat/pages/device_settings/device_settings.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; @@ -257,10 +258,21 @@ class AppRoutes { buildTransition: _fadeTransition, ), VWidget( - path: 'signup', - widget: const SignupPage(), - buildTransition: _fadeTransition, - ), + path: 'connect', + widget: const ConnectPage(), + buildTransition: _fadeTransition, + stackedRoutes: [ + VWidget( + path: 'login', + widget: const Login(), + buildTransition: _fadeTransition, + ), + VWidget( + path: 'signup', + widget: const SignupPage(), + buildTransition: _fadeTransition, + ), + ]), VWidget( path: 'logs', widget: const LogViewer(), diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 1d364a22..dd268512 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -13,6 +13,43 @@ abstract class FluffyThemes { static const fallbackTextStyle = TextStyle(fontFamily: 'Roboto', fontFamilyFallback: ['NotoEmoji']); + static InputDecoration loginTextFieldDecoration({ + String? errorText, + String? labelText, + String? hintText, + Widget? suffixIcon, + Widget? prefixIcon, + }) => + InputDecoration( + fillColor: Colors.white.withAlpha(200), + labelText: labelText, + hintText: hintText, + suffixIcon: suffixIcon, + prefixIcon: prefixIcon, + errorText: errorText, + errorStyle: TextStyle( + color: Colors.red.shade200, + shadows: const [ + Shadow( + color: Colors.black, + offset: Offset(0, 0), + blurRadius: 5, + ), + ], + ), + labelStyle: const TextStyle( + color: Colors.white, + shadows: [ + Shadow( + color: Colors.black, + offset: Offset(0, 0), + blurRadius: 5, + ), + ], + ), + contentPadding: const EdgeInsets.all(16), + ); + static var fallbackTextTheme = const TextTheme( bodyText1: fallbackTextStyle, bodyText2: fallbackTextStyle, @@ -77,6 +114,7 @@ abstract class FluffyThemes { style: ElevatedButton.styleFrom( primary: AppConfig.chatColor, onPrimary: Colors.white, + textStyle: const TextStyle(fontSize: 16), elevation: 6, shadowColor: const Color(0x44000000), minimumSize: const Size.fromHeight(48), @@ -186,6 +224,7 @@ abstract class FluffyThemes { primary: AppConfig.chatColor, onPrimary: Colors.white, minimumSize: const Size.fromHeight(48), + textStyle: const TextStyle(fontSize: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), diff --git a/lib/pages/connect/connect_page.dart b/lib/pages/connect/connect_page.dart new file mode 100644 index 00000000..01086bec --- /dev/null +++ b/lib/pages/connect/connect_page.dart @@ -0,0 +1,191 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_web_auth/flutter_web_auth.dart'; +import 'package:future_loading_dialog/future_loading_dialog.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:vrouter/vrouter.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/connect/connect_page_view.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ConnectPage extends StatefulWidget { + const ConnectPage({Key? key}) : super(key: key); + + @override + State createState() => ConnectPageController(); +} + +class ConnectPageController extends State { + final TextEditingController usernameController = TextEditingController(); + String? signupError; + bool loading = false; + + void pickAvatar() async { + final source = !PlatformInfos.isMobile + ? ImageSource.gallery + : await showModalActionSheet( + context: context, + title: L10n.of(context)!.changeYourAvatar, + actions: [ + SheetAction( + key: ImageSource.camera, + label: L10n.of(context)!.openCamera, + isDefaultAction: true, + icon: Icons.camera_alt_outlined, + ), + SheetAction( + key: ImageSource.gallery, + label: L10n.of(context)!.openGallery, + icon: Icons.photo_outlined, + ), + ], + ); + if (source == null) return; + final picked = await ImagePicker().pickImage( + source: source, + imageQuality: 50, + maxWidth: 512, + maxHeight: 512, + ); + setState(() { + Matrix.of(context).loginAvatar = picked; + }); + } + + void signUp() async { + usernameController.text = usernameController.text.trim(); + final localpart = + usernameController.text.toLowerCase().replaceAll(' ', '_'); + if (localpart.isEmpty) { + setState(() { + signupError = L10n.of(context)!.pleaseChooseAUsername; + }); + return; + } + + setState(() { + signupError = null; + loading = true; + }); + + try { + try { + await Matrix.of(context).getLoginClient().register(username: localpart); + } on MatrixException catch (e) { + if (!e.requireAdditionalAuthentication) rethrow; + } + setState(() { + loading = false; + }); + Matrix.of(context).loginUsername = usernameController.text; + VRouter.of(context).to('signup'); + } catch (e, s) { + Logs().d('Sign up failed', e, s); + setState(() { + signupError = e.toLocalizedString(context); + loading = false; + }); + } + } + + bool _supportsFlow(String flowType) => + Matrix.of(context) + .loginHomeserverSummary + ?.loginFlows + .any((flow) => flow.type == flowType) ?? + false; + + bool get supportsSso => + (PlatformInfos.isMobile || + PlatformInfos.isWeb || + PlatformInfos.isMacOS) && + _supportsFlow('m.login.sso'); + + bool get supportsLogin => _supportsFlow('m.login.password'); + + void login() => VRouter.of(context).to('login'); + + Map? _rawLoginTypes; + + List? get identityProviders { + final loginTypes = _rawLoginTypes; + if (loginTypes == null) return null; + final rawProviders = loginTypes.tryGetList('flows')!.singleWhere((flow) => + flow['type'] == AuthenticationTypes.sso)['identity_providers']; + final list = (rawProviders as List) + .map((json) => IdentityProvider.fromJson(json)) + .toList(); + if (PlatformInfos.isCupertinoStyle) { + list.sort((a, b) => a.brand == 'apple' ? -1 : 1); + } + return list; + } + + void ssoLoginAction(String id) async { + final redirectUrl = kIsWeb + ? html.window.origin! + '/web/auth.html' + : AppConfig.appOpenUrlScheme.toLowerCase() + '://login'; + final url = + '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}'; + final urlScheme = Uri.parse(redirectUrl).scheme; + final result = await FlutterWebAuth.authenticate( + url: url, + callbackUrlScheme: urlScheme, + ); + final token = Uri.parse(result).queryParameters['loginToken']; + if (token?.isEmpty ?? false) return; + + await showFutureLoadingDialog( + context: context, + future: () => Matrix.of(context).getLoginClient().login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ), + ); + } + + @override + void initState() { + super.initState(); + if (supportsSso) { + Matrix.of(context) + .getLoginClient() + .request( + RequestType.GET, + '/client/r0/login', + ) + .then((loginTypes) => setState(() { + _rawLoginTypes = loginTypes; + })); + } + } + + @override + Widget build(BuildContext context) => ConnectPageView(this); +} + +class IdentityProvider { + final String? id; + final String? name; + final String? icon; + final String? brand; + + IdentityProvider({this.id, this.name, this.icon, this.brand}); + + factory IdentityProvider.fromJson(Map json) => + IdentityProvider( + id: json['id'], + name: json['name'], + icon: json['icon'], + brand: json['brand'], + ); +} diff --git a/lib/pages/connect/connect_page_view.dart b/lib/pages/connect/connect_page_view.dart new file mode 100644 index 00000000..f9939d45 --- /dev/null +++ b/lib/pages/connect/connect_page_view.dart @@ -0,0 +1,184 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/connect/connect_page.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'sso_button.dart'; + +class ConnectPageView extends StatelessWidget { + final ConnectPageController controller; + const ConnectPageView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final avatar = Matrix.of(context).loginAvatar; + final identityProviders = controller.identityProviders; + return LoginScaffold( + appBar: AppBar( + automaticallyImplyLeading: !controller.loading, + backgroundColor: Colors.transparent, + iconTheme: const IconThemeData(color: Colors.white), + elevation: 0, + centerTitle: true, + title: Text( + Matrix.of(context).getLoginClient().homeserver?.host ?? '', + style: const TextStyle(color: Colors.white), + ), + ), + body: ListView( + children: [ + if (Matrix.of(context).loginRegistrationSupported ?? false) ...[ + Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Material( + borderRadius: BorderRadius.circular(64), + elevation: 10, + color: Colors.transparent, + child: CircleAvatar( + radius: 64, + backgroundColor: Colors.white.withAlpha(200), + child: Stack( + children: [ + Center( + child: avatar == null + ? const Icon( + Icons.person_outlined, + color: Colors.black, + size: 64, + ) + : FutureBuilder( + future: avatar.readAsBytes(), + builder: (context, snapshot) { + final bytes = snapshot.data; + if (bytes == null) { + return const CircularProgressIndicator + .adaptive(); + } + return Image.memory(bytes); + }, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: FloatingActionButton( + mini: true, + onPressed: controller.pickAvatar, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + child: const Icon(Icons.camera_alt_outlined), + ), + ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: controller.usernameController, + onSubmitted: (_) => controller.signUp(), + decoration: FluffyThemes.loginTextFieldDecoration( + prefixIcon: const Icon(Icons.account_box_outlined), + hintText: L10n.of(context)!.chooseAUsername, + errorText: controller.signupError, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Hero( + tag: 'loginButton', + child: ElevatedButton( + onPressed: controller.loading ? null : controller.signUp, + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, + ), + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.signUp), + ), + ), + ), + Row( + children: [ + const Expanded(child: Divider(color: Colors.white)), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context)!.or, + style: const TextStyle(color: Colors.white), + ), + ), + const Expanded(child: Divider(color: Colors.white)), + ], + ), + ], + if (controller.supportsSso) + identityProviders == null + ? const SizedBox( + height: 74, + child: Center( + child: CircularProgressIndicator.adaptive( + backgroundColor: Colors.white, + )), + ) + : Center( + child: identityProviders.length == 1 + ? Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton( + onPressed: () => controller + .ssoLoginAction(identityProviders.single.id!), + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, + ), + child: Text(identityProviders.single.name ?? + identityProviders.single.brand ?? + L10n.of(context)!.loginWithOneClick), + ), + ) + : Wrap( + children: [ + for (final identityProvider in identityProviders) + SsoButton( + onPressed: () => controller + .ssoLoginAction(identityProvider.id!), + identityProvider: identityProvider, + ), + ].toList(), + ), + ), + if (controller.supportsLogin) + Padding( + padding: const EdgeInsets.all(16.0), + child: Hero( + tag: 'signinButton', + child: ElevatedButton( + onPressed: controller.loading ? () {} : controller.login, + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, + ), + child: Text(L10n.of(context)!.login), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/connect/sso_button.dart b/lib/pages/connect/sso_button.dart new file mode 100644 index 00000000..dc7589ce --- /dev/null +++ b/lib/pages/connect/sso_button.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pages/connect/connect_page.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class SsoButton extends StatelessWidget { + final IdentityProvider identityProvider; + final void Function()? onPressed; + const SsoButton({ + Key? key, + required this.identityProvider, + this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(7), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: Colors.white, + borderRadius: BorderRadius.circular(7), + clipBehavior: Clip.hardEdge, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: identityProvider.icon == null + ? const Icon(Icons.web_outlined) + : CachedNetworkImage( + imageUrl: Uri.parse(identityProvider.icon!) + .getDownloadLink( + Matrix.of(context).getLoginClient()) + .toString(), + width: 32, + height: 32, + ), + ), + ), + const SizedBox(height: 8), + Text( + identityProvider.name ?? + identityProvider.brand ?? + L10n.of(context)!.singlesignon, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index cf01587c..c85cc529 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -1,19 +1,12 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_web_auth/flutter_web_auth.dart'; -import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:matrix/matrix.dart'; -import 'package:universal_html/html.dart' as html; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; -import 'package:fluffychat/utils/famedlysdk_store.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; @@ -26,182 +19,58 @@ class HomeserverPicker extends StatefulWidget { class HomeserverPickerController extends State { bool isLoading = false; - String domain = AppConfig.defaultHomeserver; final TextEditingController homeserverController = TextEditingController(text: AppConfig.defaultHomeserver); - StreamSubscription? _intentDataStreamSubscription; String? error; - Timer? _coolDown; - - void setDomain(String domain) { - this.domain = domain; - _coolDown?.cancel(); - if (domain.isNotEmpty) { - _coolDown = - Timer(const Duration(milliseconds: 500), checkHomeserverAction); - } - } - - void _loginWithToken(String token) { - if (token.isEmpty) return; - - showFutureLoadingDialog( - context: context, - future: () async { - if (Matrix.of(context).getLoginClient().homeserver == null) { - await Matrix.of(context).getLoginClient().checkHomeserver( - await Store() - .getItem(HomeserverPickerController.ssoHomeserverKey), - ); - } - await Matrix.of(context).getLoginClient().login( - LoginType.mLoginToken, - token: token, - initialDeviceDisplayName: PlatformInfos.clientName, - ); - }, - ); - } - - @override - void initState() { - super.initState(); - checkHomeserverAction(); - } - - @override - void dispose() { - super.dispose(); - _intentDataStreamSubscription?.cancel(); - } - - String? _lastCheckedHomeserver; /// Starts an analysis of the given homeserver. It uses the current domain and /// makes sure that it is prefixed with https. Then it searches for the /// well-known information and forwards to the login page depending on the /// login type. Future checkHomeserverAction() async { - _coolDown?.cancel(); - if (_lastCheckedHomeserver == domain) return; - if (domain.isEmpty) throw L10n.of(context)!.changeTheHomeserver; - var homeserver = domain; - - if (!homeserver.startsWith('https://')) { - homeserver = 'https://$homeserver'; - } - setState(() { - error = _rawLoginTypes = registrationSupported = null; + error = null; isLoading = true; }); try { - await Matrix.of(context).getLoginClient().checkHomeserver(homeserver); + homeserverController.text = + homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); + var homeserver = Uri.parse(homeserverController.text); + if (homeserver.scheme.isEmpty) { + homeserver = Uri.https(homeserverController.text, ''); + } + final matrix = Matrix.of(context); + matrix.loginHomeserverSummary = + await matrix.getLoginClient().checkHomeserver(homeserver); + final ssoSupported = matrix.loginHomeserverSummary!.loginFlows + .any((flow) => flow.type == 'm.login.sso'); - _rawLoginTypes = await Matrix.of(context).getLoginClient().request( - RequestType.GET, - '/client/r0/login', - ); try { await Matrix.of(context).getLoginClient().register(); - registrationSupported = true; + matrix.loginRegistrationSupported = true; } on MatrixException catch (e) { - registrationSupported = e.requireAdditionalAuthentication; + matrix.loginRegistrationSupported = e.requireAdditionalAuthentication; + } + + if (!ssoSupported && matrix.loginRegistrationSupported == false) { + // Server does not support SSO or registration. We can skip to login page: + VRouter.of(context).to('login'); + } else { + VRouter.of(context).to('connect'); } } catch (e) { setState(() => error = (e).toLocalizedString(context)); } finally { - _lastCheckedHomeserver = domain; if (mounted) { setState(() => isLoading = false); } } } - Map? _rawLoginTypes; - bool? registrationSupported; - - List get identityProviders { - if (!ssoLoginSupported) return []; - final rawProviders = _rawLoginTypes!.tryGetList('flows')!.singleWhere( - (flow) => - flow['type'] == AuthenticationTypes.sso)['identity_providers']; - final list = (rawProviders as List) - .map((json) => IdentityProvider.fromJson(json)) - .toList(); - if (PlatformInfos.isCupertinoStyle) { - list.sort((a, b) => a.brand == 'apple' ? -1 : 1); - } - return list; - } - - bool get passwordLoginSupported => - Matrix.of(context) - .client - .supportedLoginTypes - .contains(AuthenticationTypes.password) && - _rawLoginTypes! - .tryGetList('flows')! - .any((flow) => flow['type'] == AuthenticationTypes.password); - - bool get ssoLoginSupported => - Matrix.of(context) - .client - .supportedLoginTypes - .contains(AuthenticationTypes.sso) && - _rawLoginTypes! - .tryGetList('flows')! - .any((flow) => flow['type'] == AuthenticationTypes.sso); - - static const String ssoHomeserverKey = 'sso-homeserver'; - - void ssoLoginAction(String id) async { - if (kIsWeb) { - // We store the homserver in the local storage instead of a redirect - // parameter because of possible CSRF attacks. - Store().setItem(ssoHomeserverKey, - Matrix.of(context).getLoginClient().homeserver.toString()); - } - final redirectUrl = kIsWeb - ? html.window.origin! + '/web/auth.html' - : AppConfig.appOpenUrlScheme.toLowerCase() + '://login'; - final url = - '${Matrix.of(context).getLoginClient().homeserver?.toString()}/_matrix/client/r0/login/sso/redirect/${Uri.encodeComponent(id)}?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}'; - final urlScheme = Uri.parse(redirectUrl).scheme; - final result = await FlutterWebAuth.authenticate( - url: url, - callbackUrlScheme: urlScheme, - ); - final token = Uri.parse(result).queryParameters['loginToken']; - if (token != null) _loginWithToken(token); - } - - void signUpAction() => VRouter.of(context).to( - 'signup', - queryParameters: {'domain': domain}, - ); - @override Widget build(BuildContext context) { Matrix.of(context).navigatorContext = context; return HomeserverPickerView(this); } } - -class IdentityProvider { - final String? id; - final String? name; - final String? icon; - final String? brand; - - IdentityProvider({this.id, this.name, this.icon, this.brand}); - - factory IdentityProvider.fromJson(Map json) => - IdentityProvider( - id: json['id'], - name: json['name'], - icon: json['icon'], - brand: json['brand'], - ); -} diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index c79e84e7..d6ec147a 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -1,17 +1,13 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/default_app_bar_search_field.dart'; -import 'package:fluffychat/widgets/layouts/one_page_card.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'homeserver_picker.dart'; class HomeserverPickerView extends StatelessWidget { @@ -21,237 +17,86 @@ class HomeserverPickerView extends StatelessWidget { @override Widget build(BuildContext context) { - return OnePageCard( - child: Scaffold( - appBar: AppBar( - titleSpacing: 8, - title: DefaultAppBarSearchField( - prefixText: 'https://', - hintText: L10n.of(context)!.enterYourHomeserver, - searchController: controller.homeserverController, - suffix: const Icon(Icons.edit_outlined), - padding: EdgeInsets.zero, - onChanged: controller.setDomain, - readOnly: !AppConfig.allowOtherHomeservers, - onSubmit: (_) => controller.checkHomeserverAction(), - unfocusOnClear: false, - autocorrect: false, - labelText: L10n.of(context)!.homeserver, - ), - elevation: 0, - ), - body: ListView(children: [ - Image.asset( - Theme.of(context).brightness == Brightness.dark - ? 'assets/banner_dark.png' - : 'assets/banner.png', - ), - if (controller.isLoading) - const Center( - child: CircularProgressIndicator.adaptive(strokeWidth: 2)) - else if (controller.error != null) - Center( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - controller.error!, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - color: Colors.red[900], + return LoginScaffold( + appBar: VRouter.of(context).path == '/home' + ? null + : AppBar(title: Text(L10n.of(context)!.addAccount)), + body: Column( + children: [ + Expanded( + child: ListView( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 256), + child: Image.asset( + 'assets/info-logo.png', + ), ), ), - ), - ) - else - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 4.0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (controller.ssoLoginSupported) - Row(children: [ - const Expanded(child: Divider()), - Padding( - padding: const EdgeInsets.all(12.0), - child: Text(L10n.of(context)!.loginWithOneClick), - ), - const Expanded(child: Divider()), - ]), - Wrap( - children: [ - if (controller.ssoLoginSupported) ...{ - for (final identityProvider - in controller.identityProviders) - _SsoButton( - onPressed: () => - controller.ssoLoginAction(identityProvider.id!), - identityProvider: identityProvider, - ), - }, - ].toList(), + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: controller.homeserverController, + decoration: FluffyThemes.loginTextFieldDecoration( + labelText: L10n.of(context)!.homeserver, + hintText: L10n.of(context)!.enterYourHomeserver, + suffixIcon: const Icon(Icons.search), + errorText: controller.error, + ), + readOnly: !AppConfig.allowOtherHomeservers, + onSubmitted: (_) => controller.checkHomeserverAction(), + autocorrect: false, ), - if (controller.ssoLoginSupported && - (controller.registrationSupported! || - controller.passwordLoginSupported)) - Row(children: [ - const Expanded(child: Divider()), - Padding( - padding: const EdgeInsets.all(12.0), - child: Text(L10n.of(context)!.or), - ), - const Expanded(child: Divider()), - ]), - if (controller.passwordLoginSupported) ...[ - Center( - child: _LoginButton( - onPressed: () => VRouter.of(context).to('login'), - icon: Icon( - CupertinoIcons.lock_open_fill, - color: Theme.of(context).textTheme.bodyText1!.color, + ), + Wrap( + alignment: WrapAlignment.center, + children: [ + TextButton( + onPressed: () => launch(AppConfig.privacyUrl), + child: Text( + L10n.of(context)!.privacy, + style: const TextStyle( + decoration: TextDecoration.underline, + color: Colors.white, + ), + ), + ), + TextButton( + onPressed: () => PlatformInfos.showDialog(context), + child: Text( + L10n.of(context)!.about, + style: const TextStyle( + decoration: TextDecoration.underline, + color: Colors.white, ), - labelText: L10n.of(context)!.login, ), ), - const SizedBox(height: 12), ], - if (controller.registrationSupported!) - Center( - child: _LoginButton( - onPressed: controller.signUpAction, - icon: Icon( - CupertinoIcons.person_add, - color: Theme.of(context).textTheme.bodyText1!.color, - ), - labelText: L10n.of(context)!.register, - ), - ), - ], - ), + ), + ], ), - ]), - bottomNavigationBar: Material( - elevation: 6, - color: Theme.of(context).scaffoldBackgroundColor, - child: Wrap( - alignment: WrapAlignment.center, - children: [ - TextButton( - onPressed: () => launch(AppConfig.privacyUrl), - child: Text( - L10n.of(context)!.privacy, - style: const TextStyle( - decoration: TextDecoration.underline, - color: Colors.blueGrey, - ), - ), - ), - TextButton( - onPressed: () => PlatformInfos.showDialog(context), - child: Text( - L10n.of(context)!.about, - style: const TextStyle( - decoration: TextDecoration.underline, - color: Colors.blueGrey, - ), - ), - ), - ], ), - ), - ), - ); - } -} - -class _SsoButton extends StatelessWidget { - final IdentityProvider identityProvider; - final void Function()? onPressed; - const _SsoButton({ - Key? key, - required this.identityProvider, - this.onPressed, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(7), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Material( - color: Colors.white, - borderRadius: BorderRadius.circular(7), - clipBehavior: Clip.hardEdge, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: identityProvider.icon == null - ? const Icon(Icons.web_outlined) - : CachedNetworkImage( - imageUrl: Uri.parse(identityProvider.icon!) - .getDownloadLink( - Matrix.of(context).getLoginClient()) - .toString(), - width: 32, - height: 32, - ), + Padding( + padding: const EdgeInsets.all(16), + child: Hero( + tag: 'loginButton', + child: ElevatedButton( + onPressed: controller.isLoading + ? () {} + : controller.checkHomeserverAction, + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, + ), + child: controller.isLoading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.connect), ), ), - const SizedBox(height: 8), - Text( - identityProvider.name ?? - identityProvider.brand ?? - L10n.of(context)!.singlesignon, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.subtitle2!.color, - ), - ), - ], - ), - ), - ); - } -} - -class _LoginButton extends StatelessWidget { - final String? labelText; - final Widget? icon; - final void Function()? onPressed; - const _LoginButton({ - Key? key, - this.labelText, - this.icon, - this.onPressed, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return OutlinedButton.icon( - style: OutlinedButton.styleFrom( - minimumSize: const Size(256, 56), - textStyle: const TextStyle(fontWeight: FontWeight.bold), - backgroundColor: Theme.of(context).backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - ), - onPressed: onPressed, - icon: icon!, - label: Text( - labelText!, - style: TextStyle( - color: Theme.of(context).textTheme.bodyText1!.color, - ), + ), + ], ), ); } diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index affd8323..c545779e 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/widgets/layouts/one_page_card.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'login.dart'; @@ -13,102 +14,119 @@ class LoginView extends StatelessWidget { @override Widget build(BuildContext context) { - return OnePageCard( - child: Scaffold( - appBar: AppBar( - leading: controller.loading ? Container() : const BackButton(), - elevation: 0, - title: Text( - L10n.of(context)!.logInTo(Matrix.of(context) - .getLoginClient() - .homeserver - .toString() - .replaceFirst('https://', '')), - ), + return LoginScaffold( + appBar: AppBar( + automaticallyImplyLeading: !controller.loading, + backgroundColor: Colors.transparent, + iconTheme: const IconThemeData(color: Colors.white), + elevation: 0, + centerTitle: true, + title: Text( + L10n.of(context)!.logInTo(Matrix.of(context) + .getLoginClient() + .homeserver + .toString() + .replaceFirst('https://', '')), + style: const TextStyle(color: Colors.white), ), - body: Builder(builder: (context) { - return AutofillGroup( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - readOnly: controller.loading, - autocorrect: false, - autofocus: true, - onChanged: controller.checkWellKnownWithCoolDown, - controller: controller.usernameController, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.emailAddress, - autofillHints: - controller.loading ? null : [AutofillHints.username], - decoration: InputDecoration( - prefixIcon: const Icon(Icons.account_box_outlined), - hintText: L10n.of(context)!.emailOrUsername, - errorText: controller.usernameError, - labelText: L10n.of(context)!.emailOrUsername), + ), + body: Builder(builder: (context) { + return AutofillGroup( + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + readOnly: controller.loading, + autocorrect: false, + autofocus: true, + onChanged: controller.checkWellKnownWithCoolDown, + controller: controller.usernameController, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.emailAddress, + autofillHints: + controller.loading ? null : [AutofillHints.username], + decoration: FluffyThemes.loginTextFieldDecoration( + prefixIcon: const Icon(Icons.account_box_outlined), + errorText: controller.usernameError, + hintText: L10n.of(context)!.emailOrUsername, ), ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - readOnly: controller.loading, - autocorrect: false, - autofillHints: - controller.loading ? null : [AutofillHints.password], - controller: controller.passwordController, - textInputAction: TextInputAction.next, - obscureText: !controller.showPassword, - onSubmitted: controller.login, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.lock_outlined), - hintText: '****', - errorText: controller.passwordError, - suffixIcon: IconButton( - tooltip: L10n.of(context)!.showPassword, - icon: Icon(controller.showPassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined), - onPressed: controller.toggleShowPassword, - ), - labelText: L10n.of(context)!.password, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + readOnly: controller.loading, + autocorrect: false, + autofillHints: + controller.loading ? null : [AutofillHints.password], + controller: controller.passwordController, + textInputAction: TextInputAction.next, + obscureText: !controller.showPassword, + onSubmitted: controller.login, + decoration: FluffyThemes.loginTextFieldDecoration( + prefixIcon: const Icon(Icons.lock_outlined), + errorText: controller.passwordError, + suffixIcon: IconButton( + tooltip: L10n.of(context)!.showPassword, + icon: Icon(controller.showPassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined), + onPressed: controller.toggleShowPassword, ), + hintText: L10n.of(context)!.password, ), ), - const SizedBox(height: 12), - Hero( - tag: 'loginButton', - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: ElevatedButton( - onPressed: controller.loading - ? null - : () => controller.login(context), - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context)!.login), - ), - ), - ), - const Divider(height: 32), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + ), + Hero( + tag: 'signinButton', + child: Padding( + padding: const EdgeInsets.all(16), child: ElevatedButton( onPressed: controller.loading ? null - : controller.passwordForgotten, + : () => controller.login(context), style: ElevatedButton.styleFrom( - primary: Theme.of(context).secondaryHeaderColor, - onPrimary: Colors.red, + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, ), - child: Text(L10n.of(context)!.passwordForgotten), + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.login), ), ), - ], - ), - ); - }), - ), + ), + Row( + children: [ + const Expanded(child: Divider(color: Colors.white)), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context)!.or, + style: const TextStyle(color: Colors.white), + ), + ), + const Expanded(child: Divider(color: Colors.white)), + ], + ), + Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + onPressed: + controller.loading ? () {} : controller.passwordForgotten, + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(156), + onPrimary: Colors.red, + shadowColor: Colors.white, + ), + child: Text(L10n.of(context)!.passwordForgotten), + ), + ), + ], + ), + ); + }), ); } } diff --git a/lib/pages/sign_up/signup.dart b/lib/pages/sign_up/signup.dart index a9df0d37..487854cb 100644 --- a/lib/pages/sign_up/signup.dart +++ b/lib/pages/sign_up/signup.dart @@ -16,13 +16,11 @@ class SignupPage extends StatefulWidget { } class SignupPageController extends State { - final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); - final TextEditingController passwordController2 = TextEditingController(); final TextEditingController emailController = TextEditingController(); String? error; bool loading = false; - bool showPassword = false; + bool showPassword = true; void toggleShowPassword() => setState(() => showPassword = !showPassword); @@ -30,15 +28,6 @@ class SignupPageController extends State { final GlobalKey formKey = GlobalKey(); - String? usernameTextFieldValidator(String? value) { - usernameController.text = - usernameController.text.trim().toLowerCase().replaceAll(' ', '_'); - if (value!.isEmpty) { - return L10n.of(context)!.pleaseChooseAUsername; - } - return null; - } - String? password1TextFieldValidator(String? value) { const minLength = 8; if (value!.isEmpty) { @@ -50,18 +39,11 @@ class SignupPageController extends State { return null; } - String? password2TextFieldValidator(String? value) { - if (value!.isEmpty) { - return L10n.of(context)!.chooseAStrongPassword; - } - if (value != passwordController.text) { - return L10n.of(context)!.passwordsDoNotMatch; - } - return null; - } - String? emailTextFieldValidator(String? value) { - if (value!.isNotEmpty && !value.contains('@')) { + if (value!.isEmpty) { + return L10n.of(context)!.addEmail; + } + if (value.isNotEmpty && !value.contains('@')) { return L10n.of(context)!.pleaseEnterValidEmail; } return null; @@ -92,7 +74,7 @@ class SignupPageController extends State { } await client.uiaRequestBackground( (auth) => client.register( - username: usernameController.text, + username: Matrix.of(context).loginUsername!, password: passwordController.text, initialDeviceDisplayName: PlatformInfos.clientName, auth: auth, diff --git a/lib/pages/sign_up/signup_view.dart b/lib/pages/sign_up/signup_view.dart index 4cbd4492..3e3bdc33 100644 --- a/lib/pages/sign_up/signup_view.dart +++ b/lib/pages/sign_up/signup_view.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/widgets/layouts/one_page_card.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'signup.dart'; class SignupPageView extends StatelessWidget { @@ -11,121 +12,78 @@ class SignupPageView extends StatelessWidget { @override Widget build(BuildContext context) { - return OnePageCard( - child: Scaffold( - appBar: AppBar( - title: Text(L10n.of(context)!.signUp), + return LoginScaffold( + appBar: AppBar( + automaticallyImplyLeading: !controller.loading, + backgroundColor: Colors.transparent, + iconTheme: const IconThemeData(color: Colors.white), + elevation: 0, + title: Text( + L10n.of(context)!.signUp, + style: const TextStyle(color: Colors.white), ), - body: Form( - key: controller.formKey, - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - controller: controller.usernameController, - autofillHints: - controller.loading ? null : [AutofillHints.username], - validator: controller.usernameTextFieldValidator, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.account_circle_outlined), - hintText: L10n.of(context)!.username, - labelText: L10n.of(context)!.username, - prefixText: '@', - prefixStyle: const TextStyle(fontWeight: FontWeight.bold), - suffixStyle: const TextStyle(fontWeight: FontWeight.w200), - suffixText: ':${controller.domain}'), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - autofillHints: - controller.loading ? null : [AutofillHints.password], - controller: controller.passwordController, - obscureText: !controller.showPassword, - validator: controller.password1TextFieldValidator, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.vpn_key_outlined), - hintText: '****', - suffixIcon: IconButton( - tooltip: L10n.of(context)!.showPassword, - icon: Icon(controller.showPassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined), - onPressed: controller.toggleShowPassword, - ), - labelText: L10n.of(context)!.password, + ), + body: Form( + key: controller.formKey, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + autofillHints: + controller.loading ? null : [AutofillHints.password], + controller: controller.passwordController, + obscureText: !controller.showPassword, + validator: controller.password1TextFieldValidator, + decoration: FluffyThemes.loginTextFieldDecoration( + prefixIcon: const Icon(Icons.vpn_key_outlined), + suffixIcon: IconButton( + tooltip: L10n.of(context)!.showPassword, + icon: Icon(controller.showPassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined), + onPressed: controller.toggleShowPassword, ), + hintText: L10n.of(context)!.chooseAStrongPassword, ), ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - autofillHints: - controller.loading ? null : [AutofillHints.password], - controller: controller.passwordController2, - obscureText: true, - validator: controller.password2TextFieldValidator, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.repeat_outlined), - hintText: '****', - labelText: L10n.of(context)!.repeatPassword, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextFormField( - readOnly: controller.loading, - autocorrect: false, - controller: controller.emailController, - keyboardType: TextInputType.emailAddress, - autofillHints: - controller.loading ? null : [AutofillHints.username], - validator: controller.emailTextFieldValidator, - decoration: InputDecoration( + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + controller: controller.emailController, + keyboardType: TextInputType.emailAddress, + autofillHints: + controller.loading ? null : [AutofillHints.username], + validator: controller.emailTextFieldValidator, + decoration: FluffyThemes.loginTextFieldDecoration( prefixIcon: const Icon(Icons.mail_outlined), - labelText: L10n.of(context)!.addEmail, - hintText: 'email@example.abc', + hintText: L10n.of(context)!.enterAnEmailAddress, + errorText: controller.error), + ), + ), + Hero( + tag: 'loginButton', + child: Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + onPressed: controller.loading ? () {} : controller.signup, + style: ElevatedButton.styleFrom( + primary: Colors.white.withAlpha(200), + onPrimary: Colors.black, + shadowColor: Colors.white, ), + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context)!.signUp), ), ), - const Divider(), - const SizedBox(height: 12), - if (controller.error != null) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - controller.error!, - style: const TextStyle(color: Colors.red), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 12), - const Divider(), - const SizedBox(height: 12), - ], - Hero( - tag: 'loginButton', - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: ElevatedButton( - onPressed: controller.loading ? null : controller.signup, - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context)!.signUp), - ), - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index f8e6ebc6..60a966da 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -99,7 +99,9 @@ abstract class ClientManager { legacyDatabaseBuilder: FlutterMatrixHiveStore.hiveDatabaseBuilder, supportedLoginTypes: { AuthenticationTypes.password, - if (PlatformInfos.isMobile || PlatformInfos.isWeb) + if (PlatformInfos.isMobile || + PlatformInfos.isWeb || + PlatformInfos.isMacOS) AuthenticationTypes.sso }, compute: compute, diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart new file mode 100644 index 00000000..39d93d3c --- /dev/null +++ b/lib/widgets/layouts/login_scaffold.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class LoginScaffold extends StatelessWidget { + final Widget body; + final AppBar? appBar; + + const LoginScaffold({ + Key? key, + required this.body, + this.appBar, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarIconBrightness: Brightness.light, + statusBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.light, + ), + ); + return Scaffold( + appBar: appBar, + extendBodyBehindAppBar: true, + body: Container( + decoration: const BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: AssetImage( + 'assets/login_wallpaper.png', + ), + ), + ), + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: body, + ), + ), + ); + } +} diff --git a/lib/widgets/layouts/one_page_card.dart b/lib/widgets/layouts/one_page_card.dart deleted file mode 100644 index 1c2d0e91..00000000 --- a/lib/widgets/layouts/one_page_card.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class OnePageCard extends StatelessWidget { - final Widget child; - - /// This will cause the "isLogged()" check to be skipped and force a - /// OnePageCard without login wallpaper. This can be used in situations where - /// "Matrix.of(context) is not yet available, e.g. in the LockScreen widget. - final bool forceBackgroundless; - - const OnePageCard( - {Key? key, required this.child, this.forceBackgroundless = false}) - : super(key: key); - - static const int alpha = 12; - static num breakpoint = FluffyThemes.columnWidth * 2; - @override - Widget build(BuildContext context) { - final horizontalPadding = - max((MediaQuery.of(context).size.width - 600) / 2, 24); - return MediaQuery.of(context).size.width <= breakpoint || - forceBackgroundless || - Matrix.of(context).client.isLogged() - ? child - : Container( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/login_wallpaper.jpg'), - fit: BoxFit.cover, - ), - ), - child: Padding( - padding: EdgeInsets.only( - top: 16, - left: horizontalPadding, - right: horizontalPadding, - bottom: max((MediaQuery.of(context).size.height - 600) / 2, 24), - ), - child: SafeArea(child: Card(elevation: 16, child: child)), - ), - ); - } -} diff --git a/lib/widgets/lock_screen.dart b/lib/widgets/lock_screen.dart index b35061f1..c4633b3c 100644 --- a/lib/widgets/lock_screen.dart +++ b/lib/widgets/lock_screen.dart @@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; -import 'layouts/one_page_card.dart'; +import 'layouts/login_scaffold.dart'; class LockScreen extends StatefulWidget { const LockScreen({Key? key}) : super(key: key); @@ -30,62 +30,58 @@ class _LockScreenState extends State { localizationsDelegates: L10n.localizationsDelegates, supportedLocales: L10n.supportedLocales, home: Builder( - builder: (context) => OnePageCard( - forceBackgroundless: true, - child: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - elevation: 0, - centerTitle: true, - title: Text(L10n.of(context)!.pleaseEnterYourPin), - backgroundColor: Colors.transparent, + builder: (context) => LoginScaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + elevation: 0, + centerTitle: true, + title: Text(L10n.of(context)!.pleaseEnterYourPin), + backgroundColor: Colors.transparent, + ), + body: Container( + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + stops: const [ + 0.1, + 0.4, + 0.6, + 0.9, + ], + colors: [ + Theme.of(context).secondaryHeaderColor.withAlpha(16), + Theme.of(context).primaryColor.withAlpha(16), + Theme.of(context).colorScheme.secondary.withAlpha(16), + Theme.of(context).backgroundColor.withAlpha(16), + ], + ), ), - extendBodyBehindAppBar: true, - body: Container( - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - gradient: LinearGradient( - begin: Alignment.topRight, - end: Alignment.bottomLeft, - stops: const [ - 0.1, - 0.4, - 0.6, - 0.9, - ], - colors: [ - Theme.of(context).secondaryHeaderColor.withAlpha(16), - Theme.of(context).primaryColor.withAlpha(16), - Theme.of(context).colorScheme.secondary.withAlpha(16), - Theme.of(context).backgroundColor.withAlpha(16), - ], - ), - ), - alignment: Alignment.center, - child: PinCodeTextField( - autofocus: true, - controller: _textEditingController, - focusNode: _focusNode, - pinBoxRadius: AppConfig.borderRadius, - pinTextStyle: const TextStyle(fontSize: 32), - hideCharacter: true, - hasError: _wrongInput, - onDone: (String input) async { - if (input == - await ([TargetPlatform.linux] - .contains(Theme.of(context).platform) - ? SharedPreferences.getInstance().then((prefs) => - prefs.getString(SettingKeys.appLockKey)) - : const FlutterSecureStorage() - .read(key: SettingKeys.appLockKey))) { - AppLock.of(context)!.didUnlock(); - } else { - _textEditingController.clear(); - setState(() => _wrongInput = true); - _focusNode.requestFocus(); - } - }, - ), + alignment: Alignment.center, + child: PinCodeTextField( + autofocus: true, + controller: _textEditingController, + focusNode: _focusNode, + pinBoxRadius: AppConfig.borderRadius, + pinTextStyle: const TextStyle(fontSize: 32), + hideCharacter: true, + hasError: _wrongInput, + onDone: (String input) async { + if (input == + await ([TargetPlatform.linux] + .contains(Theme.of(context).platform) + ? SharedPreferences.getInstance().then( + (prefs) => prefs.getString(SettingKeys.appLockKey)) + : const FlutterSecureStorage() + .read(key: SettingKeys.appLockKey))) { + AppLock.of(context)!.didUnlock(); + } else { + _textEditingController.clear(); + setState(() => _wrongInput = true); + _focusNode.requestFocus(); + } + }, ), ), ), diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 8bad307a..255383b7 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -12,6 +12,7 @@ import 'package:flutter_app_lock/flutter_app_lock.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; +import 'package:image_picker/image_picker.dart'; import 'package:matrix/encryption.dart'; import 'package:matrix/matrix.dart'; import 'package:provider/provider.dart'; @@ -71,6 +72,11 @@ class MatrixState extends State with WidgetsBindingObserver { Store store = Store(); late BuildContext navigatorContext; + HomeserverSummary? loginHomeserverSummary; + XFile? loginAvatar; + String? loginUsername; + bool? loginRegistrationSupported; + BackgroundPush? _backgroundPush; Client get client {