From f808bc488f7c1b2f776d6e4dd72122c5d793d747 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 16 Jul 2023 07:07:47 +0200 Subject: [PATCH 1/3] fix(neon): Fix login works with Nextcloud --- packages/neon/neon/lib/src/pages/login.dart | 8 ++++---- packages/neon/neon/lib/src/theme/branding.dart | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index 3ad74bae..58b75ab8 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -56,16 +56,16 @@ class _LoginPageState extends State { branding.name, style: Theme.of(context).textTheme.titleLarge, ), - const SizedBox( - height: 20, - ), if (branding.showLoginWithNextcloud) ...[ + const SizedBox( + height: 20, + ), Text(AppLocalizations.of(context).loginWorksWith), const SizedBox( height: 20, ), + const NextcloudLogo(), ], - const NextcloudLogo(), const SizedBox( height: 40, ), diff --git a/packages/neon/neon/lib/src/theme/branding.dart b/packages/neon/neon/lib/src/theme/branding.dart index 77fc3b1f..3f9fea47 100644 --- a/packages/neon/neon/lib/src/theme/branding.dart +++ b/packages/neon/neon/lib/src/theme/branding.dart @@ -13,7 +13,7 @@ class Branding { required this.name, required this.logo, this.legalese, - this.showLoginWithNextcloud = false, + this.showLoginWithNextcloud = true, }); /// App name From ce2d95ee3eead3ff3a38f889e454a2d5951276c2 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 16 Jul 2023 07:09:28 +0200 Subject: [PATCH 2/3] fix(neon): Cleanup login pages layouts --- packages/neon/neon/lib/l10n/en.arb | 2 + .../neon/neon/lib/l10n/localizations.dart | 12 +++ .../neon/neon/lib/l10n/localizations_en.dart | 6 ++ packages/neon/neon/lib/src/pages/login.dart | 94 ++++++++++--------- .../lib/src/pages/login_check_account.dart | 45 +++++++-- .../src/pages/login_check_server_status.dart | 36 +++++-- .../neon/neon/lib/src/pages/login_flow.dart | 2 +- packages/neon/neon/lib/src/theme/theme.dart | 6 ++ .../neon/neon/lib/src/widgets/exception.dart | 6 +- .../neon/lib/src/widgets/nextcloud_logo.dart | 2 + .../neon/lib/src/widgets/validation_tile.dart | 11 ++- 11 files changed, 152 insertions(+), 70 deletions(-) diff --git a/packages/neon/neon/lib/l10n/en.arb b/packages/neon/neon/lib/l10n/en.arb index f302dfde..a4dec86c 100644 --- a/packages/neon/neon/lib/l10n/en.arb +++ b/packages/neon/neon/lib/l10n/en.arb @@ -1,5 +1,7 @@ { "@@locale": "en", + "nextcloud": "Nextcloud", + "nextcloudLogo": "Nextcloud logo", "appImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}", "@appImplementationName": { "placeholders": { diff --git a/packages/neon/neon/lib/l10n/localizations.dart b/packages/neon/neon/lib/l10n/localizations.dart index 67ba04cd..609d5a97 100644 --- a/packages/neon/neon/lib/l10n/localizations.dart +++ b/packages/neon/neon/lib/l10n/localizations.dart @@ -89,6 +89,18 @@ abstract class AppLocalizations { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [Locale('en')]; + /// No description provided for @nextcloud. + /// + /// In en, this message translates to: + /// **'Nextcloud'** + String get nextcloud; + + /// No description provided for @nextcloudLogo. + /// + /// In en, this message translates to: + /// **'Nextcloud logo'** + String get nextcloudLogo; + /// No description provided for @appImplementationName. /// /// In en, this message translates to: diff --git a/packages/neon/neon/lib/l10n/localizations_en.dart b/packages/neon/neon/lib/l10n/localizations_en.dart index 45eb240b..73ff480e 100644 --- a/packages/neon/neon/lib/l10n/localizations_en.dart +++ b/packages/neon/neon/lib/l10n/localizations_en.dart @@ -6,6 +6,12 @@ import 'localizations.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); + @override + String get nextcloud => 'Nextcloud'; + + @override + String get nextcloudLogo => 'Nextcloud logo'; + @override String appImplementationName(String app) { String _temp0 = intl.Intl.selectLogic( diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index 58b75ab8..166e6dad 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -20,18 +20,23 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final _formKey = GlobalKey(); final _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - } + final _controller = TextEditingController(); @override void dispose() { _focusNode.dispose(); + _controller.dispose(); super.dispose(); } + void login(final String url) { + if (_formKey.currentState!.validate()) { + LoginCheckServerStatusRoute(serverUrl: url).go(context); + } else { + _focusNode.requestFocus(); + } + } + @override Widget build(final BuildContext context) { final branding = Branding.of(context); @@ -39,77 +44,76 @@ class _LoginPageState extends State { return Scaffold( resizeToAvoidBottomInset: true, - appBar: AppBar( - leading: Navigator.canPop(context) ? const CloseButton() : null, - ), + appBar: Navigator.canPop(context) + ? AppBar( + leading: const CloseButton(), + ) + : null, body: Center( child: ConstrainedBox( constraints: NeonDialogTheme.of(context).constraints, child: Scrollbar( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), + padding: const EdgeInsets.all(10), primary: true, child: Column( children: [ - branding.logo, + ExcludeSemantics( + child: branding.logo, + ), Text( branding.name, style: Theme.of(context).textTheme.titleLarge, ), if (branding.showLoginWithNextcloud) ...[ const SizedBox( - height: 20, + height: 10, ), Text(AppLocalizations.of(context).loginWorksWith), const SizedBox( - height: 20, + height: 10, + ), + Semantics( + label: AppLocalizations.of(context).nextcloud, + child: const NextcloudLogo(), ), - const NextcloudLogo(), ], const SizedBox( - height: 40, + height: 50, ), - if (platform.canUseCamera) ...[ - ExcludeSemantics( - child: Center( - child: Text(AppLocalizations.of(context).loginUsingQrcode), - ), - ), - IconButton( - tooltip: AppLocalizations.of(context).loginUsingQrcode, - icon: const Icon( - Icons.qr_code_scanner, - size: 50, - ), - onPressed: () => const LoginQrcodeRoute().go(context), - ), - const SizedBox( - height: 20, - ), - ExcludeSemantics( - child: Center( - child: Text(AppLocalizations.of(context).loginUsingServerAddress), - ), - ), - ], Form( key: _formKey, child: TextFormField( focusNode: _focusNode, - decoration: const InputDecoration( + controller: _controller, + decoration: InputDecoration( hintText: 'https://...', + labelText: AppLocalizations.of(context).loginUsingServerAddress, + suffixIcon: IconButton( + icon: const Icon(Icons.arrow_forward), + onPressed: () { + login(_controller.text); + }, + ), ), keyboardType: TextInputType.url, validator: (final input) => validateHttpUrl(context, input), - onFieldSubmitted: (final input) { - if (_formKey.currentState!.validate()) { - LoginCheckServerStatusRoute(serverUrl: input).go(context); - } else { - _focusNode.requestFocus(); - } - }, + onFieldSubmitted: login, ), ), + if (platform.canUseCamera || true) ...[ + const SizedBox( + height: 50, + ), + IconButton( + tooltip: AppLocalizations.of(context).loginUsingQrcode, + icon: const Icon( + Icons.qr_code_scanner_rounded, + size: 60, + ), + onPressed: () => const LoginQrcodeRoute().go(context), + ), + ], ], ), ), diff --git a/packages/neon/neon/lib/src/pages/login_check_account.dart b/packages/neon/neon/lib/src/pages/login_check_account.dart index 9d35a2eb..47aad9c1 100644 --- a/packages/neon/neon/lib/src/pages/login_check_account.dart +++ b/packages/neon/neon/lib/src/pages/login_check_account.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/bloc/result.dart'; @@ -9,7 +11,6 @@ import 'package:neon/src/router.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/widgets/account_tile.dart'; import 'package:neon/src/widgets/exception.dart'; -import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/validation_tile.dart'; import 'package:provider/provider.dart'; @@ -62,13 +63,19 @@ class _LoginCheckAccountPageState extends State { builder: (final context, final state) => Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - NeonLinearProgressIndicator( - visible: state.isLoading, - ), - NeonException( - state.error, - onRetry: bloc.refresh, - ), + if (state.hasError) ...[ + Builder( + builder: (final context) { + final details = NeonException.getDetails(context, state.error); + return NeonValidationTile( + title: details.isUnauthorized + ? AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch + : details.text, + state: ValidationState.failure, + ); + }, + ), + ], _buildAccountTile(state), Align( alignment: Alignment.bottomRight, @@ -81,8 +88,19 @@ class _LoginCheckAccountPageState extends State { const HomeRoute().go(context); } - : null, - child: Text(AppLocalizations.of(context).actionContinue), + : () { + if (state.hasError && NeonException.getDetails(context, state.error).isUnauthorized) { + Navigator.pop(context); + return; + } + + unawaited(bloc.refresh()); + }, + child: Text( + state.hasData + ? AppLocalizations.of(context).actionContinue + : AppLocalizations.of(context).actionRetry, + ), ), ), ], @@ -94,6 +112,13 @@ class _LoginCheckAccountPageState extends State { ); Widget _buildAccountTile(final Result result) { + if (result.hasError) { + return NeonValidationTile( + title: AppLocalizations.of(context).loginCheckingAccount, + state: ValidationState.canceled, + ); + } + if (result.hasData) { return NeonAccountTile( account: result.requireData, diff --git a/packages/neon/neon/lib/src/pages/login_check_server_status.dart b/packages/neon/neon/lib/src/pages/login_check_server_status.dart index 48432fc0..9a6afac3 100644 --- a/packages/neon/neon/lib/src/pages/login_check_server_status.dart +++ b/packages/neon/neon/lib/src/pages/login_check_server_status.dart @@ -6,7 +6,6 @@ import 'package:neon/src/blocs/login_check_server_status.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/widgets/exception.dart'; -import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/validation_tile.dart'; import 'package:nextcloud/nextcloud.dart'; @@ -64,20 +63,23 @@ class _LoginCheckServerStatusPageState extends State return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - NeonLinearProgressIndicator( - visible: state.isLoading, - ), - NeonException( - state.error, - onRetry: bloc.refresh, - ), + if (state.hasError) ...[ + NeonValidationTile( + title: NeonException.getDetails(context, state.error).text, + state: ValidationState.failure, + ), + ], _buildServerVersionTile(state), _buildMaintenanceModeTile(state), Align( alignment: Alignment.bottomRight, child: ElevatedButton( - onPressed: success ? _onContinue : null, - child: Text(AppLocalizations.of(context).actionContinue), + onPressed: success ? _onContinue : bloc.refresh, + child: Text( + success + ? AppLocalizations.of(context).actionContinue + : AppLocalizations.of(context).actionRetry, + ), ), ), ], @@ -102,6 +104,13 @@ class _LoginCheckServerStatusPageState extends State } Widget _buildServerVersionTile(final Result result) { + if (result.hasError) { + return NeonValidationTile( + title: AppLocalizations.of(context).loginCheckingServerVersion, + state: ValidationState.canceled, + ); + } + if (!result.hasData) { return NeonValidationTile( title: AppLocalizations.of(context).loginCheckingServerVersion, @@ -123,6 +132,13 @@ class _LoginCheckServerStatusPageState extends State } Widget _buildMaintenanceModeTile(final Result result) { + if (result.hasError) { + return NeonValidationTile( + title: AppLocalizations.of(context).loginCheckingMaintenanceMode, + state: ValidationState.canceled, + ); + } + if (!result.hasData) { return NeonValidationTile( title: AppLocalizations.of(context).loginCheckingMaintenanceMode, diff --git a/packages/neon/neon/lib/src/pages/login_flow.dart b/packages/neon/neon/lib/src/pages/login_flow.dart index 7931e2a3..f94eade6 100644 --- a/packages/neon/neon/lib/src/pages/login_flow.dart +++ b/packages/neon/neon/lib/src/pages/login_flow.dart @@ -57,7 +57,7 @@ class _LoginFlowPageState extends State { appBar: AppBar(), body: Center( child: Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.all(10), child: ResultBuilder.behaviorSubject( stream: bloc.init, builder: (final context, final init) => Column( diff --git a/packages/neon/neon/lib/src/theme/theme.dart b/packages/neon/neon/lib/src/theme/theme.dart index 2f9aa685..4a1d37a2 100644 --- a/packages/neon/neon/lib/src/theme/theme.dart +++ b/packages/neon/neon/lib/src/theme/theme.dart @@ -48,6 +48,7 @@ class AppTheme { snackBarTheme: _snackBarTheme, dividerTheme: _dividerTheme, scrollbarTheme: _scrollbarTheme, + inputDecorationTheme: _inputDecorationTheme, extensions: [ neonTheme, ...?appThemes, @@ -70,4 +71,9 @@ class AppTheme { static const _scrollbarTheme = ScrollbarThemeData( interactive: true, ); + + static const _inputDecorationTheme = InputDecorationTheme( + border: OutlineInputBorder(), + floatingLabelBehavior: FloatingLabelBehavior.always, + ); } diff --git a/packages/neon/neon/lib/src/widgets/exception.dart b/packages/neon/neon/lib/src/widgets/exception.dart index db9f8db9..22426f35 100644 --- a/packages/neon/neon/lib/src/widgets/exception.dart +++ b/packages/neon/neon/lib/src/widgets/exception.dart @@ -27,7 +27,7 @@ class NeonException extends StatelessWidget { final Color? color; static void showSnackbar(final BuildContext context, final dynamic exception) { - final details = _getExceptionDetails(context, exception); + final details = getDetails(context, exception); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -48,7 +48,7 @@ class NeonException extends StatelessWidget { return const SizedBox(); } - final details = _getExceptionDetails(context, exception); + final details = getDetails(context, exception); final color = this.color ?? Theme.of(context).colorScheme.error; final errorIcon = Icon( @@ -109,7 +109,7 @@ class NeonException extends StatelessWidget { ); } - static _ExceptionDetails _getExceptionDetails(final BuildContext context, final dynamic exception) { + static _ExceptionDetails getDetails(final BuildContext context, final dynamic exception) { if (exception is String) { return _ExceptionDetails( text: exception, diff --git a/packages/neon/neon/lib/src/widgets/nextcloud_logo.dart b/packages/neon/neon/lib/src/widgets/nextcloud_logo.dart index 6e8c6538..bd11a90f 100644 --- a/packages/neon/neon/lib/src/widgets/nextcloud_logo.dart +++ b/packages/neon/neon/lib/src/widgets/nextcloud_logo.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:neon/l10n/localizations.dart'; class NextcloudLogo extends StatelessWidget { const NextcloudLogo({ @@ -12,5 +13,6 @@ class NextcloudLogo extends StatelessWidget { package: 'neon', width: 100, height: 100, + semanticsLabel: AppLocalizations.of(context).nextcloudLogo, ); } diff --git a/packages/neon/neon/lib/src/widgets/validation_tile.dart b/packages/neon/neon/lib/src/widgets/validation_tile.dart index 48c6c650..62616db1 100644 --- a/packages/neon/neon/lib/src/widgets/validation_tile.dart +++ b/packages/neon/neon/lib/src/widgets/validation_tile.dart @@ -27,6 +27,11 @@ class NeonValidationTile extends StatelessWidget { color: Theme.of(context).colorScheme.error, size: size, ), + ValidationState.canceled => Icon( + Icons.cancel_outlined, + color: Theme.of(context).disabledColor, + size: size, + ), ValidationState.success => Icon( Icons.check_circle, color: Theme.of(context).colorScheme.primary, @@ -35,7 +40,10 @@ class NeonValidationTile extends StatelessWidget { }; return ListTile( leading: leading, - title: Text(title), + title: Text( + title, + style: state == ValidationState.canceled ? TextStyle(color: Theme.of(context).disabledColor) : null, + ), ); } } @@ -43,5 +51,6 @@ class NeonValidationTile extends StatelessWidget { enum ValidationState { loading, failure, + canceled, success, } From 4fdc5cec54211fa13bd8a35e65c56f9e9f6ca030 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Sun, 16 Jul 2023 07:33:49 +0200 Subject: [PATCH 3/3] chore(docs): Remove disclaimer about login user flow diagram --- docs/login.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/login.md b/docs/login.md index 46817809..442b5ddd 100644 --- a/docs/login.md +++ b/docs/login.md @@ -1,5 +1,5 @@ # Login user flow -This diagram displays the user flow for logging into the app. This is not how it currently works, but how it should work at some point. +This diagram displays the user flow for logging into the app. ![Login user flow diagram](login.svg)