From bfcf0c4586d4aa8c771b15b0f7f02e652c00939f Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Thu, 17 Oct 2024 01:51:49 +0800 Subject: [PATCH 1/3] Rewrite sign in page --- app/lib/widget/account_picker_dialog.dart | 3 - .../widget/account_picker_dialog/bloc.dart | 11 +- app/lib/widget/sign_in.dart | 420 ++++-------------- app/lib/widget/sign_in.g.dart | 112 ++++- app/lib/widget/sign_in/bloc.dart | 94 ++++ app/lib/widget/sign_in/state_event.dart | 85 ++++ app/lib/widget/sign_in/type.dart | 16 + app/lib/widget/sign_in/view.dart | 230 ++++++++++ 8 files changed, 631 insertions(+), 340 deletions(-) create mode 100644 app/lib/widget/sign_in/bloc.dart create mode 100644 app/lib/widget/sign_in/state_event.dart create mode 100644 app/lib/widget/sign_in/type.dart create mode 100644 app/lib/widget/sign_in/view.dart diff --git a/app/lib/widget/account_picker_dialog.dart b/app/lib/widget/account_picker_dialog.dart index 15097c01..29649eec 100644 --- a/app/lib/widget/account_picker_dialog.dart +++ b/app/lib/widget/account_picker_dialog.dart @@ -6,7 +6,6 @@ import 'package:clock/clock.dart'; import 'package:copy_with/copy_with.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:nc_photos/account.dart'; @@ -16,7 +15,6 @@ import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/db/entity_converter.dart'; -import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/server_status.dart'; import 'package:nc_photos/exception_event.dart'; @@ -49,7 +47,6 @@ class AccountPickerDialog extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => _Bloc( - container: KiwiContainer().resolve(), accountController: context.read(), prefController: context.read(), db: context.read(), diff --git a/app/lib/widget/account_picker_dialog/bloc.dart b/app/lib/widget/account_picker_dialog/bloc.dart index 96887b4a..02a75ce8 100644 --- a/app/lib/widget/account_picker_dialog/bloc.dart +++ b/app/lib/widget/account_picker_dialog/bloc.dart @@ -3,13 +3,11 @@ part of '../account_picker_dialog.dart'; @npLog class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ - required DiContainer container, required this.accountController, required this.prefController, required this.db, - }) : _c = container, - super(_State.init( - accounts: container.pref.getAccounts3Or([]), + }) : super(_State.init( + accounts: prefController.accountsValue, )) { on<_ToggleDropdown>(_onToggleDropdown); on<_SwitchAccount>(_onSwitchAccount); @@ -59,7 +57,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { )); try { await _prefLock.protect(() async { - final accounts = _c.pref.getAccounts3()!; + final accounts = prefController.accountsValue; final currentAccount = accounts[prefController.currentAccountIndexValue!]; accounts.remove(ev.account); @@ -74,7 +72,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.shout("[_onDeleteAccount] Failed while removing account pref", e, stackTrace); } - await Pref().setAccounts3(accounts); + await prefController.setAccounts(accounts); await prefController.setCurrentAccountIndex(newAccountIndex); // check if the same account (server + userId) still exists in known @@ -109,7 +107,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } } - final DiContainer _c; final AccountController accountController; final PrefController prefController; final NpDb db; diff --git a/app/lib/widget/sign_in.dart b/app/lib/widget/sign_in.dart index c604498e..9952d14c 100644 --- a/app/lib/widget/sign_in.dart +++ b/app/lib/widget/sign_in.dart @@ -1,41 +1,66 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'package:copy_with/copy_with.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref_util.dart' as pref_util; -import 'package:nc_photos/legacy/sign_in.dart' as legacy; +import 'package:nc_photos/exception_event.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart'; import 'package:nc_photos/widget/connect.dart'; import 'package:nc_photos/widget/home.dart'; +import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:nc_photos/widget/root_picker.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_collection/np_collection.dart'; +import 'package:np_common/unique.dart'; import 'package:np_db/np_db.dart'; import 'package:np_string/np_string.dart'; +import 'package:to_string/to_string.dart'; part 'sign_in.g.dart'; +part 'sign_in/bloc.dart'; +part 'sign_in/state_event.dart'; +part 'sign_in/type.dart'; +part 'sign_in/view.dart'; -class SignIn extends StatefulWidget { +class SignIn extends StatelessWidget { static const routeName = "/sign-in"; const SignIn({super.key}); @override - createState() => _SignInState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => _Bloc( + npDb: context.read(), + prefController: context.read(), + ), + child: const _WrappedSignIn(), + ); + } } @npLog -class _SignInState extends State { +class _WrappedSignIn extends StatefulWidget { + const _WrappedSignIn(); + @override - build(BuildContext context) { + State createState() => _WrappedSignInState(); +} + +class _WrappedSignInState extends State<_WrappedSignIn> + with RouteAware, PageVisibilityMixin { + @override + Widget build(BuildContext context) { return Theme( data: buildDarkTheme(context).copyWith( scaffoldBackgroundColor: Colors.transparent, @@ -51,346 +76,85 @@ class _SignInState extends State { child: Stack( fit: StackFit.expand, children: [ - const _SingInBackground(), + const _Background(), Scaffold( - body: Builder(builder: (context) => _buildContent(context)), + body: MultiBlocListener( + listeners: [ + _BlocListenerT( + selector: (state) => state.connectUri, + listener: (context, connectUri) { + if (connectUri != null) { + _onConnect(context, connectUri.value); + } + }, + ), + _BlocListenerT( + selector: (state) => state.isCompleted, + listener: (context, isCompleted) { + if (isCompleted) { + Navigator.pushNamedAndRemoveUntil( + context, + Home.routeName, + (route) => false, + arguments: + HomeArguments(context.state.connectedAccount!), + ); + } + }, + ), + _BlocListenerT( + selector: (state) => state.error, + listener: (context, error) { + if (error != null && isPageVisible()) { + SnackBarManager().showSnackBarForException(error.error); + } + }, + ), + ], + child: _BlocSelector( + selector: (state) => state.isConnecting, + builder: (context, isConnecting) => + isConnecting ? const _ConnectingBody() : const _Body(), + ), + ), ), ], ), ); } - Widget _buildContent(BuildContext context) { - if (_isConnecting) { - return const Stack( - children: [ - Positioned( - left: 0, - right: 0, - bottom: 64, - child: Center( - child: SizedBox( - width: 24, - height: 24, - child: AppIntermediateCircularProgressIndicator(), - ), - ), - ), - ], - ); - } else { - return Form( - key: _formKey, - child: Center( - child: Container( - constraints: BoxConstraints( - maxWidth: Theme.of(context).widthLimitedContentMaxWidth, - ), - child: Column( - children: [ - Expanded( - child: Center( - child: SingleChildScrollView( - child: _SignInBody( - onSchemeSaved: (scheme) { - _formValue.scheme = scheme; - }, - onServerUrlSaved: (url) { - _formValue.address = url; - }, - ), - ), - ), - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (!ModalRoute.of(context)!.isFirst) - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text(MaterialLocalizations.of(context) - .cancelButtonLabel), - ) - else - Container(), - ElevatedButton( - onPressed: () { - if (_formKey.currentState?.validate() == true) { - _connect(); - } - }, - child: Text(L10n.global().connectButtonLabel), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } - } - - Future _connect() async { - _formKey.currentState!.save(); - Uri url = Uri.parse("${_formValue.scheme}://${_formValue.address}"); - _log.info("[_connect] Try connecting with url: $url"); - Account? account = await Navigator.pushNamed( - context, Connect.routeName, - arguments: ConnectArguments(url)); + Future _onConnect(BuildContext context, Uri connectUri) async { + var account = await Navigator.pushNamed( + context, + Connect.routeName, + arguments: ConnectArguments(connectUri), + ); if (account == null) { // connection failed return; } - account = await Navigator.pushNamed(context, RootPicker.routeName, - arguments: RootPickerArguments(account)); + account = await Navigator.pushNamed( + context, + RootPicker.routeName, + arguments: RootPickerArguments(account), + ); if (account == null) { // ??? return; } // we've got a good account - setState(() { - _isConnecting = true; - }); - try { - await _persistAccount(account); - unawaited( - Navigator.pushNamedAndRemoveUntil( - context, Home.routeName, (route) => false, - arguments: HomeArguments(account)), - ); - } catch (_) { - setState(() { - _isConnecting = false; - }); - rethrow; - } - } - - Future _persistAccount(Account account) async { - await context.read().addAccounts([account.toDb()]); - // only signing in with app password would trigger distinct - final accounts = (Pref().getAccounts3Or([])..add(account)).distinct(); - try { - AccountPref.setGlobalInstance( - account, await pref_util.loadAccountPref(account)); - } catch (e, stackTrace) { - _log.shout("[_connect] Failed reading pref for account: $account", e, - stackTrace); - } - final prefController = context.read(); - unawaited(Pref().setAccounts3(accounts)); - unawaited(prefController.setCurrentAccountIndex(accounts.indexOf(account))); - } - - final _formKey = GlobalKey(); - var _isConnecting = false; - - final _formValue = _FormValue(); -} - -/// A nice background that matches Nextcloud without breaking any copyright law -class _SingInBackground extends StatelessWidget { - const _SingInBackground(); - - @override - Widget build(BuildContext context) { - return Stack( - fit: StackFit.expand, - children: [ - ColoredBox(color: Theme.of(context).nextcloudBlue), - const Positioned( - bottom: 60, - left: -200, - child: Opacity( - opacity: .22, - child: Icon( - Icons.circle_outlined, - color: Colors.white, - size: 340, - ), - ), - ), - const Positioned( - top: -120, - left: -180, - right: 0, - child: Opacity( - opacity: .1, - child: Icon( - Icons.circle_outlined, - color: Colors.white, - size: 620, - ), - ), - ), - const Positioned( - bottom: -50, - right: -120, - child: Opacity( - opacity: .27, - child: Icon( - Icons.circle_outlined, - color: Colors.white, - size: 400, - ), - ), - ), - ], - ); + context.addEvent(_SetConnectedAccount(account)); } } -class _SignInBody extends StatelessWidget { - const _SignInBody({ - this.onSchemeSaved, - this.onServerUrlSaved, - }); +// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +// typedef _BlocListener = BlocListener<_Bloc, _State>; +typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +typedef _Emitter = Emitter<_State>; - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - L10n.global().signInHeaderText2, - style: Theme.of(context).textTheme.displayLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w100, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - SizedBox( - width: 64, - child: _SchemeDropdown( - onSaved: onSchemeSaved, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: Text("://"), - ), - Expanded( - child: _ServerUrlInput( - onSaved: onServerUrlSaved, - ), - ), - ], - ), - if (kDebugMode) ...[ - const SizedBox(height: 8), - InkWell( - onTap: () { - Navigator.pushReplacementNamed( - context, legacy.SignIn.routeName); - }, - child: const Text( - "Legacy sign in", - style: TextStyle(decoration: TextDecoration.underline), - ), - ), - ], - ], - ), - ); - } - - final void Function(String scheme)? onSchemeSaved; - final void Function(String url)? onServerUrlSaved; -} - -enum _Scheme { - http, - https; - - String toValueString() { - switch (this) { - case http: - return "http"; - - case https: - return "https"; - } - } -} - -class _SchemeDropdown extends StatefulWidget { - const _SchemeDropdown({ - this.onSaved, - }); - - @override - State createState() => _SchemeDropdownState(); - - final void Function(String scheme)? onSaved; -} - -class _SchemeDropdownState extends State<_SchemeDropdown> { - @override - Widget build(BuildContext context) { - return DropdownButtonHideUnderline( - child: DropdownButtonFormField<_Scheme>( - value: _scheme, - items: _Scheme.values - .map((e) => DropdownMenuItem<_Scheme>( - value: e, - child: Text(e.toValueString()), - )) - .toList(), - onChanged: (newValue) { - setState(() { - _scheme = newValue!; - }); - }, - onSaved: (value) { - widget.onSaved?.call(value!.toValueString()); - }, - ), - ); - } - - var _scheme = _Scheme.https; -} - -class _ServerUrlInput extends StatelessWidget { - const _ServerUrlInput({ - this.onSaved, - }); - - @override - Widget build(BuildContext context) { - return TextFormField( - decoration: InputDecoration( - hintText: L10n.global().serverAddressInputHint, - ), - keyboardType: TextInputType.url, - validator: (value) { - if (value!.trim().trimRightAny("/").isEmpty) { - return L10n.global().serverAddressInputInvalidEmpty; - } - return null; - }, - onSaved: (value) { - onSaved?.call(value!.trim().trimRightAny("/")); - }, - ); - } - - final void Function(String url)? onSaved; -} - -class _FormValue { - late String scheme; - late String address; +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); } diff --git a/app/lib/widget/sign_in.g.dart b/app/lib/widget/sign_in.g.dart index 847360d6..fcc35f9b 100644 --- a/app/lib/widget/sign_in.g.dart +++ b/app/lib/widget/sign_in.g.dart @@ -2,13 +2,121 @@ part of 'sign_in.dart'; +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call( + {_Scheme? scheme, + String? serverUrl, + Unique? connectUri, + Account? connectedAccount, + bool? isConnecting, + bool? isCompleted, + ExceptionEvent? error}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call( + {dynamic scheme, + dynamic serverUrl, + dynamic connectUri = copyWithNull, + dynamic connectedAccount = copyWithNull, + dynamic isConnecting, + dynamic isCompleted, + dynamic error = copyWithNull}) { + return _State( + scheme: scheme as _Scheme? ?? that.scheme, + serverUrl: serverUrl as String? ?? that.serverUrl, + connectUri: connectUri == copyWithNull + ? that.connectUri + : connectUri as Unique?, + connectedAccount: connectedAccount == copyWithNull + ? that.connectedAccount + : connectedAccount as Account?, + isConnecting: isConnecting as bool? ?? that.isConnecting, + isCompleted: isCompleted as bool? ?? that.isCompleted, + error: error == copyWithNull ? that.error : error as ExceptionEvent?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + // ************************************************************************** // NpLogGenerator // ************************************************************************** -extension _$_SignInStateNpLog on _SignInState { +extension _$_WrappedSignInNpLog on _WrappedSignIn { // ignore: unused_element Logger get _log => log; - static final log = Logger("widget.sign_in._SignInState"); + static final log = Logger("widget.sign_in._WrappedSignIn"); +} + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.sign_in._Bloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {scheme: ${scheme.name}, serverUrl: $serverUrl, connectUri: $connectUri, connectedAccount: $connectedAccount, isConnecting: $isConnecting, isCompleted: $isCompleted, error: $error}"; + } +} + +extension _$_SetSchemeToString on _SetScheme { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetScheme {value: ${value.name}}"; + } +} + +extension _$_SetServerUrlToString on _SetServerUrl { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetServerUrl {value: $value}"; + } +} + +extension _$_ConnectToString on _Connect { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Connect {}"; + } +} + +extension _$_SetConnectedAccountToString on _SetConnectedAccount { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetConnectedAccount {value: $value}"; + } +} + +extension _$_SetErrorToString on _SetError { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetError {error: $error, stackTrace: $stackTrace}"; + } } diff --git a/app/lib/widget/sign_in/bloc.dart b/app/lib/widget/sign_in/bloc.dart new file mode 100644 index 00000000..120c8255 --- /dev/null +++ b/app/lib/widget/sign_in/bloc.dart @@ -0,0 +1,94 @@ +part of '../sign_in.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc({ + required this.npDb, + required this.prefController, + }) : super(_State.init()) { + on<_SetScheme>(_onSetScheme); + on<_SetServerUrl>(_onSetServerUrl); + on<_Connect>(_onConnect); + on<_SetConnectedAccount>(_onSetConnectedAccount); + + on<_SetError>(_onSetError); + } + + @override + String get tag => _log.fullName; + + @override + void onError(Object error, StackTrace stackTrace) { + // we need this to prevent onError being triggered recursively + if (!isClosed && !_isHandlingError) { + _isHandlingError = true; + try { + add(_SetError(error, stackTrace)); + } catch (_) {} + _isHandlingError = false; + } + super.onError(error, stackTrace); + } + + void _onSetScheme(_SetScheme ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(scheme: ev.value)); + } + + void _onSetServerUrl(_SetServerUrl ev, _Emitter emit) { + _log.info(ev); + emit(state.copyWith(serverUrl: ev.value)); + } + + void _onConnect(_Connect ev, _Emitter emit) { + _log.info(ev); + final scheme = state.scheme.toValueString(); + final serverUrl = state.serverUrl.trim().trimRightAny("/"); + final uri = Uri.parse("$scheme://$serverUrl"); + _log.info("[_onConnect] Try connecting with url: $uri"); + emit(state.copyWith(connectUri: Unique(uri))); + } + + Future _onSetConnectedAccount( + _SetConnectedAccount ev, _Emitter emit) async { + _log.info(ev); + emit(state.copyWith(isConnecting: true)); + try { + await _persistAccount(ev.value); + emit(state.copyWith( + isCompleted: true, + connectedAccount: ev.value, + )); + } catch (_) { + emit(state.copyWith(isConnecting: false)); + rethrow; + } + } + + void _onSetError(_SetError ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); + } + + Future _persistAccount(Account account) async { + await npDb.addAccounts([account.toDb()]); + // only signing in with app password would trigger distinct + final accounts = prefController.accountsValue.added(account).distinct(); + try { + AccountPref.setGlobalInstance( + account, + await pref_util.loadAccountPref(account), + ); + } catch (e, stackTrace) { + _log.shout("[_persistAccount] Failed reading pref for account: $account", + e, stackTrace); + } + unawaited(prefController.setAccounts(accounts)); + unawaited(prefController.setCurrentAccountIndex(accounts.indexOf(account))); + } + + final NpDb npDb; + final PrefController prefController; + + var _isHandlingError = false; +} diff --git a/app/lib/widget/sign_in/state_event.dart b/app/lib/widget/sign_in/state_event.dart new file mode 100644 index 00000000..186fa0db --- /dev/null +++ b/app/lib/widget/sign_in/state_event.dart @@ -0,0 +1,85 @@ +part of '../sign_in.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + required this.scheme, + required this.serverUrl, + this.connectUri, + this.connectedAccount, + required this.isConnecting, + required this.isCompleted, + this.error, + }); + + factory _State.init() => const _State( + scheme: _Scheme.https, + serverUrl: "", + isConnecting: false, + isCompleted: false, + ); + + @override + String toString() => _$toString(); + + final _Scheme scheme; + final String serverUrl; + final Unique? connectUri; + final Account? connectedAccount; + final bool isConnecting; + final bool isCompleted; + + final ExceptionEvent? error; +} + +abstract class _Event {} + +@toString +class _SetScheme implements _Event { + const _SetScheme(this.value); + + @override + String toString() => _$toString(); + + final _Scheme value; +} + +@toString +class _SetServerUrl implements _Event { + const _SetServerUrl(this.value); + + @override + String toString() => _$toString(); + + final String value; +} + +@toString +class _Connect implements _Event { + const _Connect(); + + @override + String toString() => _$toString(); +} + +@toString +class _SetConnectedAccount implements _Event { + const _SetConnectedAccount(this.value); + + @override + String toString() => _$toString(); + + final Account value; +} + +@toString +class _SetError implements _Event { + const _SetError(this.error, [this.stackTrace]); + + @override + String toString() => _$toString(); + + final Object error; + final StackTrace? stackTrace; +} diff --git a/app/lib/widget/sign_in/type.dart b/app/lib/widget/sign_in/type.dart new file mode 100644 index 00000000..109b3a07 --- /dev/null +++ b/app/lib/widget/sign_in/type.dart @@ -0,0 +1,16 @@ +part of '../sign_in.dart'; + +enum _Scheme { + http, + https; + + String toValueString() { + switch (this) { + case http: + return "http"; + + case https: + return "https"; + } + } +} diff --git a/app/lib/widget/sign_in/view.dart b/app/lib/widget/sign_in/view.dart new file mode 100644 index 00000000..d6acfb5c --- /dev/null +++ b/app/lib/widget/sign_in/view.dart @@ -0,0 +1,230 @@ +part of '../sign_in.dart'; + +/// A nice background that matches Nextcloud without breaking any copyright law +class _Background extends StatelessWidget { + const _Background(); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + ColoredBox(color: Theme.of(context).nextcloudBlue), + const Positioned( + bottom: 60, + left: -200, + child: Opacity( + opacity: .22, + child: Icon( + Icons.circle_outlined, + color: Colors.white, + size: 340, + ), + ), + ), + const Positioned( + top: -120, + left: -180, + right: 0, + child: Opacity( + opacity: .1, + child: Icon( + Icons.circle_outlined, + color: Colors.white, + size: 620, + ), + ), + ), + const Positioned( + bottom: -50, + right: -120, + child: Opacity( + opacity: .27, + child: Icon( + Icons.circle_outlined, + color: Colors.white, + size: 400, + ), + ), + ), + ], + ); + } +} + +class _ConnectingBody extends StatelessWidget { + const _ConnectingBody(); + + @override + Widget build(BuildContext context) { + return const Stack( + children: [ + Positioned( + left: 0, + right: 0, + bottom: 64, + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: AppIntermediateCircularProgressIndicator(), + ), + ), + ), + ], + ); + } +} + +class _Body extends StatefulWidget { + const _Body(); + + @override + State createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Center( + child: Container( + constraints: BoxConstraints( + maxWidth: Theme.of(context).widthLimitedContentMaxWidth, + ), + child: Column( + children: [ + const Expanded( + child: Center( + child: SingleChildScrollView( + child: _SignInBody(), + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (!ModalRoute.of(context)!.isFirst) + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(MaterialLocalizations.of(context) + .cancelButtonLabel), + ) + else + Container(), + ElevatedButton( + onPressed: () { + if (_formKey.currentState?.validate() == true) { + context.addEvent(const _Connect()); + } + }, + child: Text(L10n.global().connectButtonLabel), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + final _formKey = GlobalKey(); +} + +class _SignInBody extends StatelessWidget { + const _SignInBody(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + L10n.global().signInHeaderText2, + style: Theme.of(context).textTheme.displayLarge!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w100, + ), + ), + const SizedBox(height: 16), + const Row( + children: [ + SizedBox( + width: 64, + child: _SchemeDropdown(), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text("://"), + ), + Expanded( + child: _ServerUrlInput(), + ), + ], + ), + ], + ), + ); + } +} + +class _SchemeDropdown extends StatelessWidget { + const _SchemeDropdown(); + + @override + Widget build(BuildContext context) { + return DropdownButtonHideUnderline( + child: _BlocSelector( + selector: (state) => state.scheme, + builder: (context, scheme) => DropdownButtonFormField<_Scheme>( + value: scheme, + items: _Scheme.values + .map((e) => DropdownMenuItem<_Scheme>( + value: e, + child: Text(e.toValueString()), + )) + .toList(), + onChanged: (value) { + if (value != null) { + context.addEvent(_SetScheme(value)); + } + }, + ), + ), + ); + } +} + +class _ServerUrlInput extends StatelessWidget { + const _ServerUrlInput(); + + @override + Widget build(BuildContext context) { + return TextFormField( + decoration: InputDecoration( + hintText: L10n.global().serverAddressInputHint, + ), + keyboardType: TextInputType.url, + validator: (value) { + if (value!.trim().trimRightAny("/").isEmpty) { + return L10n.global().serverAddressInputInvalidEmpty; + } + return null; + }, + onChanged: (value) { + context.addEvent(_SetServerUrl(value)); + }, + ); + } +} From 69dada413b0d7922d9fdb60a59aeeb757a07dfac Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 19 Oct 2024 00:52:38 +0800 Subject: [PATCH 2/3] Reintroduce legacy sign in using username and password --- app/lib/l10n/app_en.arb | 4 + app/lib/l10n/untranslated-messages.txt | 42 ++++++++--- app/lib/widget/expandable_container.dart | 72 ++++++++++++++++++ app/lib/widget/sign_in.dart | 54 +++++++++++-- app/lib/widget/sign_in.g.dart | 64 ++++++++++++++-- app/lib/widget/sign_in/bloc.dart | 43 ++++++++++- app/lib/widget/sign_in/state_event.dart | 57 +++++++++++++- app/lib/widget/sign_in/type.dart | 18 +++++ app/lib/widget/sign_in/view.dart | 96 ++++++++++++++++++++++++ 9 files changed, 419 insertions(+), 31 deletions(-) create mode 100644 app/lib/widget/expandable_container.dart diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 5c6e499e..3bb91707 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1501,6 +1501,10 @@ "homeTabMapBrowser": "Map", "mapBrowserSetDefaultDateRangeButton": "Set as default", "todayText": "Today", + "alternativeSignIn": "Alternative sign in with username and password", + "@alternativeSignIn": { + "description": "Sign in using username and password instead of the recommended Nextcloud login flow" + }, "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 16995f14..f512f0ef 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -260,6 +260,7 @@ "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", "todayText", + "alternativeSignIn", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -272,7 +273,8 @@ "cs": [ "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "de": [ @@ -292,7 +294,8 @@ "searchLandingPeopleListEmptyText2", "mapBrowserDateRangeLabel", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "el": [ @@ -440,12 +443,14 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "es": [ "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "fi": [ @@ -484,7 +489,8 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "fr": [ @@ -523,7 +529,8 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "it": [ @@ -567,7 +574,8 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "nl": [ @@ -948,6 +956,7 @@ "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", "todayText", + "alternativeSignIn", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -998,7 +1007,8 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "pt": [ @@ -1057,7 +1067,8 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "ru": [ @@ -1096,7 +1107,12 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" + ], + + "tr": [ + "alternativeSignIn" ], "zh": [ @@ -1166,7 +1182,8 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ], "zh_Hant": [ @@ -1330,6 +1347,7 @@ "mapBrowserDateRangeCustom", "homeTabMapBrowser", "mapBrowserSetDefaultDateRangeButton", - "todayText" + "todayText", + "alternativeSignIn" ] } diff --git a/app/lib/widget/expandable_container.dart b/app/lib/widget/expandable_container.dart new file mode 100644 index 00000000..85618941 --- /dev/null +++ b/app/lib/widget/expandable_container.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/widget/measure.dart'; + +class ExpandableContainer extends StatefulWidget { + const ExpandableContainer({ + super.key, + required this.isShow, + required this.child, + }); + + @override + State createState() => ExpandableContainerState(); + + final bool isShow; + final Widget child; +} + +class ExpandableContainerState extends State + with TickerProviderStateMixin { + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: k.animationDurationNormal, + vsync: this, + value: 0, + ); + _animation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant ExpandableContainer oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isShow != widget.isShow) { + if (widget.isShow) { + _animationController.animateTo(1); + } else { + _animationController.animateBack(0); + } + } + } + + @override + Widget build(BuildContext context) { + return MatrixTransition( + animation: _animation, + onTransform: (animationValue) => Matrix4.identity() + ..translate(0.0, -(_size.height / 2) * (1 - animationValue), 0.0) + ..scale(1.0, animationValue, 1.0), + child: MeasureSize( + onChange: (size) => setState(() { + _size = size; + }), + child: widget.child, + ), + ); + } + + late AnimationController _animationController; + late Animation _animation; + var _size = Size.zero; +} diff --git a/app/lib/widget/sign_in.dart b/app/lib/widget/sign_in.dart index 9952d14c..f628ae87 100644 --- a/app/lib/widget/sign_in.dart +++ b/app/lib/widget/sign_in.dart @@ -12,16 +12,17 @@ import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref_util.dart' as pref_util; import 'package:nc_photos/exception_event.dart'; +import 'package:nc_photos/legacy/connect.dart' as legacy; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart'; import 'package:nc_photos/widget/connect.dart'; +import 'package:nc_photos/widget/expandable_container.dart'; import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/page_visibility_mixin.dart'; import 'package:nc_photos/widget/root_picker.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_collection/np_collection.dart'; -import 'package:np_common/unique.dart'; import 'package:np_db/np_db.dart'; import 'package:np_string/np_string.dart'; import 'package:to_string/to_string.dart'; @@ -49,7 +50,6 @@ class SignIn extends StatelessWidget { } } -@npLog class _WrappedSignIn extends StatefulWidget { const _WrappedSignIn(); @@ -57,6 +57,7 @@ class _WrappedSignIn extends StatefulWidget { State createState() => _WrappedSignInState(); } +@npLog class _WrappedSignInState extends State<_WrappedSignIn> with RouteAware, PageVisibilityMixin { @override @@ -81,10 +82,18 @@ class _WrappedSignInState extends State<_WrappedSignIn> body: MultiBlocListener( listeners: [ _BlocListenerT( - selector: (state) => state.connectUri, - listener: (context, connectUri) { - if (connectUri != null) { - _onConnect(context, connectUri.value); + selector: (state) => state.connectArg, + listener: (context, connectArg) { + if (connectArg == null) { + return; + } + if (connectArg.username != null && + connectArg.password != null) { + _onLegacyConnect(context, connectArg); + } else { + final uri = Uri.parse( + "${connectArg.scheme}://${connectArg.address}"); + _onConnect(context, uri); } }, ), @@ -145,6 +154,39 @@ class _WrappedSignInState extends State<_WrappedSignIn> // we've got a good account context.addEvent(_SetConnectedAccount(account)); } + + Future _onLegacyConnect(BuildContext context, _ConnectArg arg) async { + Account? account = Account( + id: Account.newId(), + scheme: arg.scheme, + address: arg.address, + userId: arg.username!.toCi(), + username2: arg.username!, + password: arg.password!, + roots: [""], + ); + _log.info("[_onLegacyConnect] Try connecting with account: $account"); + account = await Navigator.pushNamed( + context, + legacy.Connect.routeName, + arguments: legacy.ConnectArguments(account), + ); + if (account == null) { + // connection failed + return; + } + account = await Navigator.pushNamed( + context, + RootPicker.routeName, + arguments: RootPickerArguments(account), + ); + if (account == null) { + // ??? + return; + } + // we've got a good account + context.addEvent(_SetConnectedAccount(account)); + } } // typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; diff --git a/app/lib/widget/sign_in.g.dart b/app/lib/widget/sign_in.g.dart index fcc35f9b..6c11c9c6 100644 --- a/app/lib/widget/sign_in.g.dart +++ b/app/lib/widget/sign_in.g.dart @@ -16,10 +16,14 @@ abstract class $_StateCopyWithWorker { _State call( {_Scheme? scheme, String? serverUrl, - Unique? connectUri, + String? username, + String? password, + bool? shouldObscurePassword, + _ConnectArg? connectArg, Account? connectedAccount, bool? isConnecting, bool? isCompleted, + bool? isAltMode, ExceptionEvent? error}); } @@ -30,22 +34,31 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { _State call( {dynamic scheme, dynamic serverUrl, - dynamic connectUri = copyWithNull, + dynamic username, + dynamic password, + dynamic shouldObscurePassword, + dynamic connectArg = copyWithNull, dynamic connectedAccount = copyWithNull, dynamic isConnecting, dynamic isCompleted, + dynamic isAltMode, dynamic error = copyWithNull}) { return _State( scheme: scheme as _Scheme? ?? that.scheme, serverUrl: serverUrl as String? ?? that.serverUrl, - connectUri: connectUri == copyWithNull - ? that.connectUri - : connectUri as Unique?, + username: username as String? ?? that.username, + password: password as String? ?? that.password, + shouldObscurePassword: + shouldObscurePassword as bool? ?? that.shouldObscurePassword, + connectArg: connectArg == copyWithNull + ? that.connectArg + : connectArg as _ConnectArg?, connectedAccount: connectedAccount == copyWithNull ? that.connectedAccount : connectedAccount as Account?, isConnecting: isConnecting as bool? ?? that.isConnecting, isCompleted: isCompleted as bool? ?? that.isCompleted, + isAltMode: isAltMode as bool? ?? that.isAltMode, error: error == copyWithNull ? that.error : error as ExceptionEvent?); } @@ -61,11 +74,11 @@ extension $_StateCopyWith on _State { // NpLogGenerator // ************************************************************************** -extension _$_WrappedSignInNpLog on _WrappedSignIn { +extension _$_WrappedSignInStateNpLog on _WrappedSignInState { // ignore: unused_element Logger get _log => log; - static final log = Logger("widget.sign_in._WrappedSignIn"); + static final log = Logger("widget.sign_in._WrappedSignInState"); } extension _$_BlocNpLog on _Bloc { @@ -82,7 +95,7 @@ extension _$_BlocNpLog on _Bloc { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {scheme: ${scheme.name}, serverUrl: $serverUrl, connectUri: $connectUri, connectedAccount: $connectedAccount, isConnecting: $isConnecting, isCompleted: $isCompleted, error: $error}"; + return "_State {scheme: ${scheme.name}, serverUrl: $serverUrl, username: $username, password: $password, shouldObscurePassword: $shouldObscurePassword, connectArg: $connectArg, connectedAccount: $connectedAccount, isConnecting: $isConnecting, isCompleted: $isCompleted, isAltMode: $isAltMode, error: $error}"; } } @@ -114,9 +127,44 @@ extension _$_SetConnectedAccountToString on _SetConnectedAccount { } } +extension _$_SetAltModeToString on _SetAltMode { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetAltMode {value: $value}"; + } +} + +extension _$_SetUsernameToString on _SetUsername { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetUsername {value: $value}"; + } +} + +extension _$_SetPasswordToString on _SetPassword { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetPassword {value: $value}"; + } +} + +extension _$_SetObscurePasswordToString on _SetObscurePassword { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetObscurePassword {value: $value}"; + } +} + extension _$_SetErrorToString on _SetError { String _$toString() { // ignore: unnecessary_string_interpolations return "_SetError {error: $error, stackTrace: $stackTrace}"; } } + +extension _$_ConnectArgToString on _ConnectArg { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_ConnectArg {scheme: $scheme, address: $address, username: $username, password: $password}"; + } +} diff --git a/app/lib/widget/sign_in/bloc.dart b/app/lib/widget/sign_in/bloc.dart index 120c8255..66b02fe7 100644 --- a/app/lib/widget/sign_in/bloc.dart +++ b/app/lib/widget/sign_in/bloc.dart @@ -10,6 +10,10 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetServerUrl>(_onSetServerUrl); on<_Connect>(_onConnect); on<_SetConnectedAccount>(_onSetConnectedAccount); + on<_SetAltMode>(_onSetAltMode); + on<_SetUsername>(_onSetUsername); + on<_SetPassword>(_onSetPassword); + on<_SetObscurePassword>(_onSetObscurePassword); on<_SetError>(_onSetError); } @@ -44,9 +48,22 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _log.info(ev); final scheme = state.scheme.toValueString(); final serverUrl = state.serverUrl.trim().trimRightAny("/"); - final uri = Uri.parse("$scheme://$serverUrl"); - _log.info("[_onConnect] Try connecting with url: $uri"); - emit(state.copyWith(connectUri: Unique(uri))); + final _ConnectArg arg; + if (!state.isAltMode) { + arg = _ConnectArg( + scheme: scheme, + address: serverUrl, + ); + } else { + arg = _ConnectArg( + scheme: scheme, + address: serverUrl, + username: state.username, + password: state.password, + ); + } + _log.info("[_onConnect] Try connecting: $arg"); + emit(state.copyWith(connectArg: arg)); } Future _onSetConnectedAccount( @@ -65,6 +82,26 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } } + void _onSetAltMode(_SetAltMode ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(isAltMode: ev.value)); + } + + void _onSetUsername(_SetUsername ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(username: ev.value)); + } + + void _onSetPassword(_SetPassword ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(password: ev.value)); + } + + void _onSetObscurePassword(_SetObscurePassword ev, Emitter<_State> emit) { + _log.info(ev); + emit(state.copyWith(shouldObscurePassword: ev.value)); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); diff --git a/app/lib/widget/sign_in/state_event.dart b/app/lib/widget/sign_in/state_event.dart index 186fa0db..645acd1e 100644 --- a/app/lib/widget/sign_in/state_event.dart +++ b/app/lib/widget/sign_in/state_event.dart @@ -6,18 +6,26 @@ class _State { const _State({ required this.scheme, required this.serverUrl, - this.connectUri, + required this.username, + required this.password, + required this.shouldObscurePassword, + this.connectArg, this.connectedAccount, required this.isConnecting, required this.isCompleted, + required this.isAltMode, this.error, }); factory _State.init() => const _State( scheme: _Scheme.https, serverUrl: "", + username: "", + password: "", + shouldObscurePassword: true, isConnecting: false, isCompleted: false, + isAltMode: false, ); @override @@ -25,11 +33,16 @@ class _State { final _Scheme scheme; final String serverUrl; - final Unique? connectUri; + final String username; + final String password; + final bool shouldObscurePassword; + final _ConnectArg? connectArg; final Account? connectedAccount; final bool isConnecting; final bool isCompleted; + final bool isAltMode; + final ExceptionEvent? error; } @@ -73,6 +86,46 @@ class _SetConnectedAccount implements _Event { final Account value; } +@toString +class _SetAltMode implements _Event { + const _SetAltMode(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + +@toString +class _SetUsername implements _Event { + const _SetUsername(this.value); + + @override + String toString() => _$toString(); + + final String value; +} + +@toString +class _SetPassword implements _Event { + const _SetPassword(this.value); + + @override + String toString() => _$toString(); + + final String value; +} + +@toString +class _SetObscurePassword implements _Event { + const _SetObscurePassword(this.value); + + @override + String toString() => _$toString(); + + final bool value; +} + @toString class _SetError implements _Event { const _SetError(this.error, [this.stackTrace]); diff --git a/app/lib/widget/sign_in/type.dart b/app/lib/widget/sign_in/type.dart index 109b3a07..436d88a4 100644 --- a/app/lib/widget/sign_in/type.dart +++ b/app/lib/widget/sign_in/type.dart @@ -14,3 +14,21 @@ enum _Scheme { } } } + +@toString +class _ConnectArg { + const _ConnectArg({ + required this.scheme, + required this.address, + this.username, + this.password, + }); + + @override + String toString() => _$toString(); + + final String scheme; + final String address; + final String? username; + final String? password; +} diff --git a/app/lib/widget/sign_in/view.dart b/app/lib/widget/sign_in/view.dart index d6acfb5c..a01c4d68 100644 --- a/app/lib/widget/sign_in/view.dart +++ b/app/lib/widget/sign_in/view.dart @@ -173,6 +173,35 @@ class _SignInBody extends StatelessWidget { ), ], ), + const SizedBox(height: 8), + Stack( + children: [ + _BlocSelector( + selector: (state) => state.isAltMode, + builder: (context, isAltMode) => ExpandableContainer( + isShow: isAltMode, + child: const _LegacySignInForm(), + ), + ), + _BlocSelector( + selector: (state) => state.isAltMode, + builder: (context, isAltMode) => Visibility( + visible: !isAltMode, + child: InkWell( + onTap: () { + context.addEvent(const _SetAltMode(true)); + }, + child: Text( + L10n.global().alternativeSignIn, + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ), + ), + ), + ), + ], + ), ], ), ); @@ -228,3 +257,70 @@ class _ServerUrlInput extends StatelessWidget { ); } } + +class _LegacySignInForm extends StatelessWidget { + const _LegacySignInForm(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + hintText: L10n.global().usernameInputHint, + ), + keyboardType: TextInputType.text, + validator: (value) { + if (!context.state.isAltMode) { + return null; + } + if (value!.trim().isEmpty) { + return L10n.global().usernameInputInvalidEmpty; + } + return null; + }, + onChanged: (value) { + context.addEvent(_SetUsername(value)); + }, + ), + const SizedBox(height: 8), + _BlocSelector( + selector: (state) => state.shouldObscurePassword, + builder: (context, shouldObscurePassword) => TextFormField( + decoration: InputDecoration( + hintText: L10n.global().passwordInputHint, + suffixIcon: shouldObscurePassword + ? IconButton( + icon: const Icon(Icons.visibility_off_outlined), + onPressed: () { + context.addEvent(const _SetObscurePassword(false)); + }, + ) + : IconButton( + icon: const Icon(Icons.visibility_outlined), + onPressed: () { + context.addEvent(const _SetObscurePassword(true)); + }, + ), + ), + keyboardType: TextInputType.text, + obscureText: shouldObscurePassword, + validator: (value) { + if (!context.state.isAltMode) { + return null; + } + if (value!.trim().isEmpty) { + return L10n.global().passwordInputInvalidEmpty; + } + return null; + }, + onChanged: (value) { + context.addEvent(_SetPassword(value)); + }, + ), + ), + ], + ); + } +} From a000bc98e4ede9fa4a70933b5fc1acc31aa04fcd Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sat, 19 Oct 2024 00:56:17 +0800 Subject: [PATCH 3/3] Remove obsolete file --- app/lib/legacy/sign_in.dart | 356 ---------------------------------- app/lib/legacy/sign_in.g.dart | 14 -- app/lib/widget/my_app.dart | 4 - 3 files changed, 374 deletions(-) delete mode 100644 app/lib/legacy/sign_in.dart delete mode 100644 app/lib/legacy/sign_in.g.dart diff --git a/app/lib/legacy/sign_in.dart b/app/lib/legacy/sign_in.dart deleted file mode 100644 index 6ced4749..00000000 --- a/app/lib/legacy/sign_in.dart +++ /dev/null @@ -1,356 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:logging/logging.dart'; -import 'package:nc_photos/account.dart'; -import 'package:nc_photos/app_localizations.dart'; -import 'package:nc_photos/controller/pref_controller.dart'; -import 'package:nc_photos/db/entity_converter.dart'; -import 'package:nc_photos/entity/pref.dart'; -import 'package:nc_photos/entity/pref_util.dart' as pref_util; -import 'package:nc_photos/help_utils.dart' as help_utils; -import 'package:nc_photos/legacy/connect.dart'; -import 'package:nc_photos/theme.dart'; -import 'package:nc_photos/url_launcher_util.dart'; -import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart'; -import 'package:nc_photos/widget/home.dart'; -import 'package:nc_photos/widget/root_picker.dart'; -import 'package:np_codegen/np_codegen.dart'; -import 'package:np_collection/np_collection.dart'; -import 'package:np_db/np_db.dart'; -import 'package:np_platform_util/np_platform_util.dart'; -import 'package:np_string/np_string.dart'; - -part 'sign_in.g.dart'; - -class SignIn extends StatefulWidget { - static const routeName = "/sign-in-legacy"; - - const SignIn({super.key}); - - @override - createState() => _SignInState(); -} - -@npLog -class _SignInState extends State { - @override - build(BuildContext context) { - return Scaffold( - body: Builder(builder: (context) => _buildContent(context)), - ); - } - - Widget _buildContent(BuildContext context) { - if (_isConnecting) { - return const Stack( - children: [ - Positioned( - left: 0, - right: 0, - bottom: 64, - child: Center( - child: SizedBox( - width: 24, - height: 24, - child: AppIntermediateCircularProgressIndicator(), - ), - ), - ), - ], - ); - } else { - return SafeArea( - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints viewportConstraints) { - return Form( - key: _formKey, - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: Text( - L10n.global().signInHeaderText, - style: Theme.of(context).textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ), - Align( - alignment: Alignment.center, - child: Container( - constraints: BoxConstraints( - maxWidth: - Theme.of(context).widthLimitedContentMaxWidth, - ), - padding: const EdgeInsets.symmetric(horizontal: 32), - child: _buildForm(context), - ), - ), - Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints( - maxWidth: - Theme.of(context).widthLimitedContentMaxWidth, - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: InkWell( - onTap: () { - launch(help_utils.twoFactorAuthUrl); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 16), - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.help_outline, size: 16), - const SizedBox(width: 4), - Expanded( - child: - Text(L10n.global().signIn2faHintText), - ), - ], - ), - ), - ), - ), - if (getRawPlatform() != NpPlatform.web) - Expanded(child: Container()), - Container( - constraints: BoxConstraints( - maxWidth: - Theme.of(context).widthLimitedContentMaxWidth, - ), - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (!ModalRoute.of(context)!.isFirst) - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text(MaterialLocalizations.of(context) - .cancelButtonLabel), - ) - else - Container(), - ElevatedButton( - onPressed: () { - if (_formKey.currentState?.validate() == - true) { - _connect(); - } - }, - child: Text(L10n.global().connectButtonLabel), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - }, - ), - ); - } - } - - Widget _buildForm(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Icon( - Icons.cloud, - color: Theme.of(context).colorScheme.primary, - size: 72, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - SizedBox( - width: 64, - child: DropdownButtonHideUnderline( - child: DropdownButtonFormField<_Scheme>( - value: _scheme, - items: [_Scheme.http, _Scheme.https] - .map((e) => DropdownMenuItem<_Scheme>( - value: e, - child: Text(e.toValueString()), - )) - .toList(), - onChanged: (newValue) { - setState(() { - _scheme = newValue!; - }); - }, - onSaved: (value) { - _formValue.scheme = value!.toValueString(); - }, - ), - ), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 4), - child: Text("://"), - ), - Expanded( - child: TextFormField( - decoration: InputDecoration( - hintText: L10n.global().serverAddressInputHint, - ), - keyboardType: TextInputType.url, - validator: (value) { - if (value!.trim().trimRightAny("/").isEmpty) { - return L10n.global().serverAddressInputInvalidEmpty; - } - return null; - }, - onSaved: (value) { - _formValue.address = value!.trim().trimRightAny("/"); - }, - ), - ), - ], - ), - const SizedBox(height: 8), - TextFormField( - decoration: InputDecoration( - hintText: L10n.global().usernameInputHint, - ), - validator: (value) { - if (value!.trim().isEmpty) { - return L10n.global().usernameInputInvalidEmpty; - } - return null; - }, - onSaved: (value) { - _formValue.username = value!; - }, - ), - const SizedBox(height: 8), - TextFormField( - decoration: InputDecoration( - hintText: L10n.global().passwordInputHint, - ), - obscureText: true, - validator: (value) { - if (value!.trim().isEmpty) { - return L10n.global().passwordInputInvalidEmpty; - } - return null; - }, - onSaved: (value) { - _formValue.password = value!; - }, - ), - ], - ); - } - - Future _connect() async { - _formKey.currentState!.save(); - Account? account = Account( - id: Account.newId(), - scheme: _formValue.scheme, - address: _formValue.address, - userId: _formValue.username.toCi(), - username2: _formValue.username, - password: _formValue.password, - roots: [""], - ); - _log.info("[_connect] Try connecting with account: $account"); - account = await Navigator.pushNamed(context, Connect.routeName, - arguments: ConnectArguments(account)); - if (account == null) { - // connection failed - return; - } - account = await Navigator.pushNamed(context, RootPicker.routeName, - arguments: RootPickerArguments(account)); - if (account == null) { - // ??? - return; - } - // we've got a good account - setState(() { - _isConnecting = true; - }); - try { - await _persistAccount(account); - unawaited( - Navigator.pushNamedAndRemoveUntil( - context, Home.routeName, (route) => false, - arguments: HomeArguments(account)), - ); - } catch (_) { - setState(() { - _isConnecting = false; - }); - rethrow; - } - } - - Future _persistAccount(Account account) async { - await context.read().addAccounts([account.toDb()]); - // only signing in with app password would trigger distinct - final accounts = (Pref().getAccounts3Or([])..add(account)).distinct(); - try { - AccountPref.setGlobalInstance( - account, await pref_util.loadAccountPref(account)); - } catch (e, stackTrace) { - _log.shout("[_connect] Failed reading pref for account: $account", e, - stackTrace); - } - final prefController = context.read(); - unawaited(Pref().setAccounts3(accounts)); - unawaited(prefController.setCurrentAccountIndex(accounts.indexOf(account))); - } - - final _formKey = GlobalKey(); - var _scheme = _Scheme.https; - var _isConnecting = false; - - final _formValue = _FormValue(); -} - -enum _Scheme { - http, - https, -} - -extension on _Scheme { - String toValueString() { - switch (this) { - case _Scheme.http: - return "http"; - - case _Scheme.https: - return "https"; - - default: - throw StateError("Unknown value: $this"); - } - } -} - -class _FormValue { - late String scheme; - late String address; - late String username; - late String password; -} diff --git a/app/lib/legacy/sign_in.g.dart b/app/lib/legacy/sign_in.g.dart deleted file mode 100644 index 24355410..00000000 --- a/app/lib/legacy/sign_in.g.dart +++ /dev/null @@ -1,14 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'sign_in.dart'; - -// ************************************************************************** -// NpLogGenerator -// ************************************************************************** - -extension _$_SignInStateNpLog on _SignInState { - // ignore: unused_element - Logger get _log => log; - - static final log = Logger("legacy.sign_in._SignInState"); -} diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index 4e263ef7..d60b39dd 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -17,7 +17,6 @@ import 'package:nc_photos/controller/trusted_cert_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/legacy/connect.dart' as legacy; -import 'package:nc_photos/legacy/sign_in.dart' as legacy; import 'package:nc_photos/mobile/self_signed_cert_manager.dart'; import 'package:nc_photos/navigation_manager.dart'; import 'package:nc_photos/protected_page_handler.dart'; @@ -207,9 +206,6 @@ class _WrappedAppState extends State<_WrappedApp> Splash.routeName: () => MaterialPageRoute( builder: (context) => const Splash(), ), - legacy.SignIn.routeName: () => MaterialPageRoute( - builder: (context) => const legacy.SignIn(), - ), CollectionPicker.routeName: CollectionPicker.buildRoute, LanguageSettings.routeName: LanguageSettings.buildRoute, PeopleBrowser.routeName: PeopleBrowser.buildRoute,