Browse Source

Merge pull request #489 from provokateurin/cleanup/login

Cleanup/login
pull/525/head
Kate 1 year ago committed by GitHub
parent
commit
185f749bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      docs/login.md
  2. 2
      packages/neon/neon/lib/l10n/en.arb
  3. 12
      packages/neon/neon/lib/l10n/localizations.dart
  4. 6
      packages/neon/neon/lib/l10n/localizations_en.dart
  5. 96
      packages/neon/neon/lib/src/pages/login.dart
  6. 43
      packages/neon/neon/lib/src/pages/login_check_account.dart
  7. 34
      packages/neon/neon/lib/src/pages/login_check_server_status.dart
  8. 2
      packages/neon/neon/lib/src/pages/login_flow.dart
  9. 2
      packages/neon/neon/lib/src/theme/branding.dart
  10. 6
      packages/neon/neon/lib/src/theme/theme.dart
  11. 6
      packages/neon/neon/lib/src/widgets/exception.dart
  12. 2
      packages/neon/neon/lib/src/widgets/nextcloud_logo.dart
  13. 11
      packages/neon/neon/lib/src/widgets/validation_tile.dart

2
docs/login.md

@ -1,5 +1,5 @@
# Login user flow # 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) ![Login user flow diagram](login.svg)

2
packages/neon/neon/lib/l10n/en.arb

@ -1,5 +1,7 @@
{ {
"@@locale": "en", "@@locale": "en",
"nextcloud": "Nextcloud",
"nextcloudLogo": "Nextcloud logo",
"appImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}", "appImplementationName": "{app, select, nextcloud{Nextcloud} core{Server} files{Files} news{News} notes{Notes} notifications{Notifications} other{}}",
"@appImplementationName": { "@appImplementationName": {
"placeholders": { "placeholders": {

12
packages/neon/neon/lib/l10n/localizations.dart

@ -89,6 +89,18 @@ abstract class AppLocalizations {
/// A list of this localizations delegate's supported locales. /// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[Locale('en')]; static const List<Locale> supportedLocales = <Locale>[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. /// No description provided for @appImplementationName.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

6
packages/neon/neon/lib/l10n/localizations_en.dart

@ -6,6 +6,12 @@ import 'localizations.dart';
class AppLocalizationsEn extends AppLocalizations { class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get nextcloud => 'Nextcloud';
@override
String get nextcloudLogo => 'Nextcloud logo';
@override @override
String appImplementationName(String app) { String appImplementationName(String app) {
String _temp0 = intl.Intl.selectLogic( String _temp0 = intl.Intl.selectLogic(

96
packages/neon/neon/lib/src/pages/login.dart

@ -20,18 +20,23 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> { class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _focusNode = FocusNode(); final _focusNode = FocusNode();
final _controller = TextEditingController();
@override
void initState() {
super.initState();
}
@override @override
void dispose() { void dispose() {
_focusNode.dispose(); _focusNode.dispose();
_controller.dispose();
super.dispose(); super.dispose();
} }
void login(final String url) {
if (_formKey.currentState!.validate()) {
LoginCheckServerStatusRoute(serverUrl: url).go(context);
} else {
_focusNode.requestFocus();
}
}
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final branding = Branding.of(context); final branding = Branding.of(context);
@ -39,77 +44,76 @@ class _LoginPageState extends State<LoginPage> {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: true,
appBar: AppBar( appBar: Navigator.canPop(context)
leading: Navigator.canPop(context) ? const CloseButton() : null, ? AppBar(
), leading: const CloseButton(),
)
: null,
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: NeonDialogTheme.of(context).constraints, constraints: NeonDialogTheme.of(context).constraints,
child: Scrollbar( child: Scrollbar(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), padding: const EdgeInsets.all(10),
primary: true, primary: true,
child: Column( child: Column(
children: [ children: [
branding.logo, ExcludeSemantics(
child: branding.logo,
),
Text( Text(
branding.name, branding.name,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
const SizedBox(
height: 20,
),
if (branding.showLoginWithNextcloud) ...[ if (branding.showLoginWithNextcloud) ...[
Text(AppLocalizations.of(context).loginWorksWith),
const SizedBox( const SizedBox(
height: 20, height: 10,
), ),
], Text(AppLocalizations.of(context).loginWorksWith),
const NextcloudLogo(),
const SizedBox( const SizedBox(
height: 40, height: 10,
),
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), Semantics(
label: AppLocalizations.of(context).nextcloud,
child: const NextcloudLogo(),
), ),
],
const SizedBox( const SizedBox(
height: 20, height: 50,
),
ExcludeSemantics(
child: Center(
child: Text(AppLocalizations.of(context).loginUsingServerAddress),
),
), ),
],
Form( Form(
key: _formKey, key: _formKey,
child: TextFormField( child: TextFormField(
focusNode: _focusNode, focusNode: _focusNode,
decoration: const InputDecoration( controller: _controller,
decoration: InputDecoration(
hintText: 'https://...', hintText: 'https://...',
labelText: AppLocalizations.of(context).loginUsingServerAddress,
suffixIcon: IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () {
login(_controller.text);
},
),
), ),
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
validator: (final input) => validateHttpUrl(context, input), validator: (final input) => validateHttpUrl(context, input),
onFieldSubmitted: (final input) { onFieldSubmitted: login,
if (_formKey.currentState!.validate()) { ),
LoginCheckServerStatusRoute(serverUrl: input).go(context);
} else {
_focusNode.requestFocus();
}
},
), ),
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),
),
],
], ],
), ),
), ),

43
packages/neon/neon/lib/src/pages/login_check_account.dart

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:neon/l10n/localizations.dart'; import 'package:neon/l10n/localizations.dart';
import 'package:neon/src/bloc/result.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/theme/dialog.dart';
import 'package:neon/src/widgets/account_tile.dart'; import 'package:neon/src/widgets/account_tile.dart';
import 'package:neon/src/widgets/exception.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:neon/src/widgets/validation_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -62,13 +63,19 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
builder: (final context, final state) => Column( builder: (final context, final state) => Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
NeonLinearProgressIndicator( if (state.hasError) ...[
visible: state.isLoading, Builder(
), builder: (final context) {
NeonException( final details = NeonException.getDetails(context, state.error);
state.error, return NeonValidationTile(
onRetry: bloc.refresh, title: details.isUnauthorized
? AppLocalizations.of(context).errorCredentialsForAccountNoLongerMatch
: details.text,
state: ValidationState.failure,
);
},
), ),
],
_buildAccountTile(state), _buildAccountTile(state),
Align( Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
@ -81,8 +88,19 @@ class _LoginCheckAccountPageState extends State<LoginCheckAccountPage> {
const HomeRoute().go(context); 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<LoginCheckAccountPage> {
); );
Widget _buildAccountTile(final Result<Account> result) { Widget _buildAccountTile(final Result<Account> result) {
if (result.hasError) {
return NeonValidationTile(
title: AppLocalizations.of(context).loginCheckingAccount,
state: ValidationState.canceled,
);
}
if (result.hasData) { if (result.hasData) {
return NeonAccountTile( return NeonAccountTile(
account: result.requireData, account: result.requireData,

34
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/router.dart';
import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/widgets/exception.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:neon/src/widgets/validation_tile.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';
@ -64,20 +63,23 @@ class _LoginCheckServerStatusPageState extends State<LoginCheckServerStatusPage>
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
NeonLinearProgressIndicator( if (state.hasError) ...[
visible: state.isLoading, NeonValidationTile(
), title: NeonException.getDetails(context, state.error).text,
NeonException( state: ValidationState.failure,
state.error,
onRetry: bloc.refresh,
), ),
],
_buildServerVersionTile(state), _buildServerVersionTile(state),
_buildMaintenanceModeTile(state), _buildMaintenanceModeTile(state),
Align( Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: ElevatedButton( child: ElevatedButton(
onPressed: success ? _onContinue : null, onPressed: success ? _onContinue : bloc.refresh,
child: Text(AppLocalizations.of(context).actionContinue), child: Text(
success
? AppLocalizations.of(context).actionContinue
: AppLocalizations.of(context).actionRetry,
),
), ),
), ),
], ],
@ -102,6 +104,13 @@ class _LoginCheckServerStatusPageState extends State<LoginCheckServerStatusPage>
} }
Widget _buildServerVersionTile(final Result<CoreServerStatus> result) { Widget _buildServerVersionTile(final Result<CoreServerStatus> result) {
if (result.hasError) {
return NeonValidationTile(
title: AppLocalizations.of(context).loginCheckingServerVersion,
state: ValidationState.canceled,
);
}
if (!result.hasData) { if (!result.hasData) {
return NeonValidationTile( return NeonValidationTile(
title: AppLocalizations.of(context).loginCheckingServerVersion, title: AppLocalizations.of(context).loginCheckingServerVersion,
@ -123,6 +132,13 @@ class _LoginCheckServerStatusPageState extends State<LoginCheckServerStatusPage>
} }
Widget _buildMaintenanceModeTile(final Result<CoreServerStatus> result) { Widget _buildMaintenanceModeTile(final Result<CoreServerStatus> result) {
if (result.hasError) {
return NeonValidationTile(
title: AppLocalizations.of(context).loginCheckingMaintenanceMode,
state: ValidationState.canceled,
);
}
if (!result.hasData) { if (!result.hasData) {
return NeonValidationTile( return NeonValidationTile(
title: AppLocalizations.of(context).loginCheckingMaintenanceMode, title: AppLocalizations.of(context).loginCheckingMaintenanceMode,

2
packages/neon/neon/lib/src/pages/login_flow.dart

@ -57,7 +57,7 @@ class _LoginFlowPageState extends State<LoginFlowPage> {
appBar: AppBar(), appBar: AppBar(),
body: Center( body: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(10),
child: ResultBuilder.behaviorSubject( child: ResultBuilder.behaviorSubject(
stream: bloc.init, stream: bloc.init,
builder: (final context, final init) => Column( builder: (final context, final init) => Column(

2
packages/neon/neon/lib/src/theme/branding.dart

@ -13,7 +13,7 @@ class Branding {
required this.name, required this.name,
required this.logo, required this.logo,
this.legalese, this.legalese,
this.showLoginWithNextcloud = false, this.showLoginWithNextcloud = true,
}); });
/// App name /// App name

6
packages/neon/neon/lib/src/theme/theme.dart

@ -48,6 +48,7 @@ class AppTheme {
snackBarTheme: _snackBarTheme, snackBarTheme: _snackBarTheme,
dividerTheme: _dividerTheme, dividerTheme: _dividerTheme,
scrollbarTheme: _scrollbarTheme, scrollbarTheme: _scrollbarTheme,
inputDecorationTheme: _inputDecorationTheme,
extensions: [ extensions: [
neonTheme, neonTheme,
...?appThemes, ...?appThemes,
@ -70,4 +71,9 @@ class AppTheme {
static const _scrollbarTheme = ScrollbarThemeData( static const _scrollbarTheme = ScrollbarThemeData(
interactive: true, interactive: true,
); );
static const _inputDecorationTheme = InputDecorationTheme(
border: OutlineInputBorder(),
floatingLabelBehavior: FloatingLabelBehavior.always,
);
} }

6
packages/neon/neon/lib/src/widgets/exception.dart

@ -27,7 +27,7 @@ class NeonException extends StatelessWidget {
final Color? color; final Color? color;
static void showSnackbar(final BuildContext context, final dynamic exception) { static void showSnackbar(final BuildContext context, final dynamic exception) {
final details = _getExceptionDetails(context, exception); final details = getDetails(context, exception);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -48,7 +48,7 @@ class NeonException extends StatelessWidget {
return const SizedBox(); return const SizedBox();
} }
final details = _getExceptionDetails(context, exception); final details = getDetails(context, exception);
final color = this.color ?? Theme.of(context).colorScheme.error; final color = this.color ?? Theme.of(context).colorScheme.error;
final errorIcon = Icon( 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) { if (exception is String) {
return _ExceptionDetails( return _ExceptionDetails(
text: exception, text: exception,

2
packages/neon/neon/lib/src/widgets/nextcloud_logo.dart

@ -1,5 +1,6 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:neon/l10n/localizations.dart';
class NextcloudLogo extends StatelessWidget { class NextcloudLogo extends StatelessWidget {
const NextcloudLogo({ const NextcloudLogo({
@ -12,5 +13,6 @@ class NextcloudLogo extends StatelessWidget {
package: 'neon', package: 'neon',
width: 100, width: 100,
height: 100, height: 100,
semanticsLabel: AppLocalizations.of(context).nextcloudLogo,
); );
} }

11
packages/neon/neon/lib/src/widgets/validation_tile.dart

@ -27,6 +27,11 @@ class NeonValidationTile extends StatelessWidget {
color: Theme.of(context).colorScheme.error, color: Theme.of(context).colorScheme.error,
size: size, size: size,
), ),
ValidationState.canceled => Icon(
Icons.cancel_outlined,
color: Theme.of(context).disabledColor,
size: size,
),
ValidationState.success => Icon( ValidationState.success => Icon(
Icons.check_circle, Icons.check_circle,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@ -35,7 +40,10 @@ class NeonValidationTile extends StatelessWidget {
}; };
return ListTile( return ListTile(
leading: leading, 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 { enum ValidationState {
loading, loading,
failure, failure,
canceled,
success, success,
} }

Loading…
Cancel
Save