mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 18:38:48 +01:00
Restore legacy sign in in debug mode for quick dev access
This commit is contained in:
parent
34066215c0
commit
dbe74cf2d5
5 changed files with 842 additions and 0 deletions
136
app/lib/legacy/app_password_exchange_bloc.dart
Normal file
136
app/lib/legacy/app_password_exchange_bloc.dart
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
|
||||||
|
abstract class AppPasswordExchangeBlocEvent {
|
||||||
|
const AppPasswordExchangeBlocEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordExchangeBlocConnect extends AppPasswordExchangeBlocEvent {
|
||||||
|
const AppPasswordExchangeBlocConnect(this.account);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"account: $account, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AppPasswordExchangeBlocState {
|
||||||
|
const AppPasswordExchangeBlocState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordExchangeBlocInit extends AppPasswordExchangeBlocState {
|
||||||
|
const AppPasswordExchangeBlocInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordExchangeBlocSuccess extends AppPasswordExchangeBlocState {
|
||||||
|
const AppPasswordExchangeBlocSuccess(this.password);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"password: ${kDebugMode ? password : '***'}, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final String password;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordExchangeBlocFailure extends AppPasswordExchangeBlocState {
|
||||||
|
const AppPasswordExchangeBlocFailure(this.exception);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"exception: $exception, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final dynamic exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy sign in support, may be removed any time in the future
|
||||||
|
class AppPasswordExchangeBloc
|
||||||
|
extends Bloc<AppPasswordExchangeBlocEvent, AppPasswordExchangeBlocState> {
|
||||||
|
AppPasswordExchangeBloc() : super(const AppPasswordExchangeBlocInit()) {
|
||||||
|
on<AppPasswordExchangeBlocEvent>(_onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEvent(AppPasswordExchangeBlocEvent event,
|
||||||
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
|
_log.info("[_onEvent] $event");
|
||||||
|
if (event is AppPasswordExchangeBlocConnect) {
|
||||||
|
await _onEventConnect(event, emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEventConnect(AppPasswordExchangeBlocConnect ev,
|
||||||
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
|
final account = ev.account;
|
||||||
|
try {
|
||||||
|
final appPwd = await _exchangePassword(account);
|
||||||
|
emit(AppPasswordExchangeBlocSuccess(appPwd));
|
||||||
|
} on InvalidBaseUrlException catch (e) {
|
||||||
|
_log.warning("[_onEventConnect] Invalid base url");
|
||||||
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
|
} on HandshakeException catch (e) {
|
||||||
|
_log.info("[_onEventConnect] Self-signed cert");
|
||||||
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
if (e is ApiException && e.response.statusCode == 401) {
|
||||||
|
// wrong password, normal
|
||||||
|
_log.warning("[_onEventConnect] Server response 401, wrong password?");
|
||||||
|
} else {
|
||||||
|
_log.shout("[_onEventConnect] Failed while exchanging password", e,
|
||||||
|
stacktrace);
|
||||||
|
}
|
||||||
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the app password for [account]
|
||||||
|
static Future<String> _exchangePassword(Account account) async {
|
||||||
|
final response = await Api(account).request(
|
||||||
|
"GET",
|
||||||
|
"ocs/v2.php/core/getapppassword",
|
||||||
|
header: {
|
||||||
|
"OCS-APIRequest": "true",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.isGood) {
|
||||||
|
try {
|
||||||
|
final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>");
|
||||||
|
final appPwdMatch = appPwdRegex.firstMatch(response.body);
|
||||||
|
return appPwdMatch!.group(1)!;
|
||||||
|
} catch (_) {
|
||||||
|
// this happens when the address is not the base URL and so Nextcloud
|
||||||
|
// returned the login page
|
||||||
|
throw InvalidBaseUrlException();
|
||||||
|
}
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
// If the client is authenticated with an app password a 403 will be
|
||||||
|
// returned
|
||||||
|
_log.info("[_exchangePassword] Already an app password");
|
||||||
|
return account.password;
|
||||||
|
} else {
|
||||||
|
_log.severe(
|
||||||
|
"[_exchangePassword] Failed while requesting app password: $response");
|
||||||
|
throw ApiException(
|
||||||
|
response: response,
|
||||||
|
message:
|
||||||
|
"Server responed with an error: HTTP ${response.statusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final _log =
|
||||||
|
Logger("legacy.app_password_exchange_bloc.AppPasswordExchangeBloc");
|
||||||
|
}
|
319
app/lib/legacy/connect.dart
Normal file
319
app/lib/legacy/connect.dart
Normal file
|
@ -0,0 +1,319 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/ci_string.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
|
import 'package:nc_photos/help_utils.dart' as help_util;
|
||||||
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
import 'package:nc_photos/legacy/app_password_exchange_bloc.dart';
|
||||||
|
import 'package:nc_photos/mobile/self_signed_cert_manager.dart';
|
||||||
|
import 'package:nc_photos/platform/features.dart' as features;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
import 'package:nc_photos/string_extension.dart';
|
||||||
|
import 'package:nc_photos/url_launcher_util.dart';
|
||||||
|
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||||
|
|
||||||
|
class ConnectArguments {
|
||||||
|
ConnectArguments(this.account);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy sign in support, may be removed any time in the future
|
||||||
|
class Connect extends StatefulWidget {
|
||||||
|
static const routeName = "/connect-legacy";
|
||||||
|
|
||||||
|
static Route buildRoute(ConnectArguments args) => MaterialPageRoute<Account>(
|
||||||
|
builder: (context) => Connect.fromArgs(args),
|
||||||
|
);
|
||||||
|
|
||||||
|
const Connect({
|
||||||
|
Key? key,
|
||||||
|
required this.account,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
Connect.fromArgs(ConnectArguments args, {Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
account: args.account,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _ConnectState();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ConnectState extends State<Connect> {
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_initBloc();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: BlocListener<AppPasswordExchangeBloc, AppPasswordExchangeBlocState>(
|
||||||
|
bloc: _bloc,
|
||||||
|
listener: (context, state) => _onStateChange(context, state),
|
||||||
|
child: Builder(builder: (context) => _buildContent(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initBloc() {
|
||||||
|
_log.info("[_initBloc] Initialize bloc");
|
||||||
|
_connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cloud,
|
||||||
|
size: 128,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
L10n.global().connectingToServer(widget.account.url),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChange(
|
||||||
|
BuildContext context, AppPasswordExchangeBlocState state) {
|
||||||
|
if (state is AppPasswordExchangeBlocSuccess) {
|
||||||
|
final newAccount = widget.account.copyWith(password: state.password);
|
||||||
|
_log.info("[_onStateChange] Password exchanged: $newAccount");
|
||||||
|
_checkWebDavUrl(context, newAccount);
|
||||||
|
} else if (state is AppPasswordExchangeBlocFailure) {
|
||||||
|
if (features.isSupportSelfSignedCert &&
|
||||||
|
state.exception is HandshakeException) {
|
||||||
|
_onSelfSignedCert(context);
|
||||||
|
} else if (state.exception is ApiException &&
|
||||||
|
(state.exception as ApiException).response.statusCode == 401) {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(L10n.global().errorWrongPassword),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
} else {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(state.exception)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelfSignedCert(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(L10n.global().serverCertErrorDialogTitle),
|
||||||
|
content: Text(L10n.global().serverCertErrorDialogContent),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: Text(L10n.global().advancedButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value != true) {
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(L10n.global().whitelistCertDialogTitle),
|
||||||
|
content: Text(L10n.global().whitelistCertDialogContent(
|
||||||
|
SelfSignedCertManager().getLastBadCertHost(),
|
||||||
|
SelfSignedCertManager().getLastBadCertFingerprint())),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: Text(L10n.global().whitelistCertButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value != true) {
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SelfSignedCertManager().whitelistLastBadCert().then((value) {
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _connect() {
|
||||||
|
_bloc.add(AppPasswordExchangeBlocConnect(widget.account));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onCheckWebDavUrlFailed(
|
||||||
|
BuildContext context, Account account) async {
|
||||||
|
final userId = await _askWebDavUrl(context, account);
|
||||||
|
if (userId != null) {
|
||||||
|
final newAccount = account.copyWith(
|
||||||
|
userId: userId.toCi(),
|
||||||
|
);
|
||||||
|
return _checkWebDavUrl(context, newAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkWebDavUrl(BuildContext context, Account account) async {
|
||||||
|
// check the WebDAV URL
|
||||||
|
try {
|
||||||
|
final c = KiwiContainer().resolve<DiContainer>();
|
||||||
|
await LsSingleFile(c.withRemoteFileRepo())(
|
||||||
|
account, file_util.unstripPath(account, ""));
|
||||||
|
_log.info("[_checkWebDavUrl] Account is good: $account");
|
||||||
|
Navigator.of(context).pop(account);
|
||||||
|
} on ApiException catch (e) {
|
||||||
|
if (e.response.statusCode == 404) {
|
||||||
|
return _onCheckWebDavUrlFailed(context, account);
|
||||||
|
}
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(e)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
} on StateError catch (_) {
|
||||||
|
// Nextcloud for some reason doesn't return HTTP error when listing home
|
||||||
|
// dir of other users
|
||||||
|
return _onCheckWebDavUrlFailed(context, account);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout("[_checkWebDavUrl] Failed", e, stackTrace);
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(exception_util.toUserString(e)),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _askWebDavUrl(BuildContext context, Account account) {
|
||||||
|
return showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => _WebDavUrlDialog(account: account),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _bloc = AppPasswordExchangeBloc();
|
||||||
|
|
||||||
|
static final _log = Logger("widget.connect._ConnectState");
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebDavUrlDialog extends StatefulWidget {
|
||||||
|
const _WebDavUrlDialog({
|
||||||
|
Key? key,
|
||||||
|
required this.account,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _WebDavUrlDialogState();
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WebDavUrlDialogState extends State<_WebDavUrlDialog> {
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(L10n.global().homeFolderNotFoundDialogTitle),
|
||||||
|
content: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(L10n.global().homeFolderNotFoundDialogContent),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text("${widget.account.url}/remote.php/dav/files/"),
|
||||||
|
TextFormField(
|
||||||
|
validator: (value) {
|
||||||
|
if (value?.trimAny("/").isNotEmpty == true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return L10n.global().homeFolderInputInvalidEmpty;
|
||||||
|
},
|
||||||
|
onSaved: (value) {
|
||||||
|
_formValue.userId = value!.trimAny("/");
|
||||||
|
},
|
||||||
|
initialValue: widget.account.userId.toString(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _onHelpPressed,
|
||||||
|
child: Text(L10n.global().helpButtonLabel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _onOkPressed,
|
||||||
|
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onOkPressed() {
|
||||||
|
if (_formKey.currentState?.validate() == true) {
|
||||||
|
_formKey.currentState!.save();
|
||||||
|
Navigator.of(context).pop(_formValue.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onHelpPressed() {
|
||||||
|
launch(help_util.homeFolderNotFoundUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _formValue = _FormValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FormValue {
|
||||||
|
late String userId;
|
||||||
|
}
|
356
app/lib/legacy/sign_in.dart
Normal file
356
app/lib/legacy/sign_in.dart
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kiwi/kiwi.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
|
import 'package:nc_photos/ci_string.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||||
|
import 'package:nc_photos/help_utils.dart' as help_utils;
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/legacy/connect.dart';
|
||||||
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/pref_util.dart' as pref_util;
|
||||||
|
import 'package:nc_photos/string_extension.dart';
|
||||||
|
import 'package:nc_photos/theme.dart';
|
||||||
|
import 'package:nc_photos/url_launcher_util.dart';
|
||||||
|
import 'package:nc_photos/widget/home.dart';
|
||||||
|
import 'package:nc_photos/widget/root_picker.dart';
|
||||||
|
|
||||||
|
class SignIn extends StatefulWidget {
|
||||||
|
static const routeName = "/sign-in-legacy";
|
||||||
|
|
||||||
|
const SignIn({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _SignInState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SignInState extends State<SignIn> {
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Builder(builder: (context) => _buildContent(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent(BuildContext context) {
|
||||||
|
if (_isConnecting) {
|
||||||
|
return Stack(
|
||||||
|
children: const [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 64,
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} 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.headline5,
|
||||||
|
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 (!platform_k.isWeb) 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<void> _connect() async {
|
||||||
|
_formKey.currentState!.save();
|
||||||
|
Account? account = Account(
|
||||||
|
Account.newId(),
|
||||||
|
_formValue.scheme,
|
||||||
|
_formValue.address,
|
||||||
|
_formValue.username.toCi(),
|
||||||
|
_formValue.username,
|
||||||
|
_formValue.password,
|
||||||
|
[""],
|
||||||
|
);
|
||||||
|
_log.info("[_connect] Try connecting with account: $account");
|
||||||
|
account = await Navigator.pushNamed<Account>(context, Connect.routeName,
|
||||||
|
arguments: ConnectArguments(account));
|
||||||
|
if (account == null) {
|
||||||
|
// connection failed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
account = await Navigator.pushNamed<Account>(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<void> _persistAccount(Account account) async {
|
||||||
|
final c = KiwiContainer().resolve<DiContainer>();
|
||||||
|
await c.sqliteDb.use((db) async {
|
||||||
|
await db.insertAccountOf(account);
|
||||||
|
});
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
unawaited(Pref().setAccounts3(accounts));
|
||||||
|
unawaited(Pref().setCurrentAccountIndex(accounts.indexOf(account)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
var _scheme = _Scheme.https;
|
||||||
|
var _isConnecting = false;
|
||||||
|
|
||||||
|
final _formValue = _FormValue();
|
||||||
|
|
||||||
|
static final _log = Logger("widget.sign_in._SignInState");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
import 'package:nc_photos/language_util.dart' as language_util;
|
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/navigation_manager.dart';
|
import 'package:nc_photos/navigation_manager.dart';
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
|
@ -137,6 +139,7 @@ class _MyAppState extends State<MyApp>
|
||||||
Setup.routeName: (context) => const Setup(),
|
Setup.routeName: (context) => const Setup(),
|
||||||
SignIn.routeName: (context) => const SignIn(),
|
SignIn.routeName: (context) => const SignIn(),
|
||||||
Splash.routeName: (context) => const Splash(),
|
Splash.routeName: (context) => const Splash(),
|
||||||
|
legacy.SignIn.routeName: (_) => const legacy.SignIn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||||
|
@ -145,6 +148,7 @@ class _MyAppState extends State<MyApp>
|
||||||
route ??= _handleBasicRoute(settings);
|
route ??= _handleBasicRoute(settings);
|
||||||
route ??= _handleViewerRoute(settings);
|
route ??= _handleViewerRoute(settings);
|
||||||
route ??= _handleConnectRoute(settings);
|
route ??= _handleConnectRoute(settings);
|
||||||
|
route ??= _handleConnectLegacyRoute(settings);
|
||||||
route ??= _handleHomeRoute(settings);
|
route ??= _handleHomeRoute(settings);
|
||||||
route ??= _handleRootPickerRoute(settings);
|
route ??= _handleRootPickerRoute(settings);
|
||||||
route ??= _handleAlbumBrowserRoute(settings);
|
route ??= _handleAlbumBrowserRoute(settings);
|
||||||
|
@ -221,6 +225,19 @@ class _MyAppState extends State<MyApp>
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Route<dynamic>? _handleConnectLegacyRoute(RouteSettings settings) {
|
||||||
|
try {
|
||||||
|
if (settings.name == legacy.Connect.routeName &&
|
||||||
|
settings.arguments != null) {
|
||||||
|
final args = settings.arguments as legacy.ConnectArguments;
|
||||||
|
return legacy.Connect.buildRoute(args);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_log.severe("[_handleConnectLegacyRoute] Failed while handling route", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Route<dynamic>? _handleHomeRoute(RouteSettings settings) {
|
Route<dynamic>? _handleHomeRoute(RouteSettings settings) {
|
||||||
try {
|
try {
|
||||||
if (settings.name == Home.routeName && settings.arguments != null) {
|
if (settings.name == Home.routeName && settings.arguments != null) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -8,6 +9,7 @@ import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/legacy/sign_in.dart' as legacy;
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
import 'package:nc_photos/pref_util.dart' as pref_util;
|
import 'package:nc_photos/pref_util.dart' as pref_util;
|
||||||
|
@ -191,6 +193,18 @@ class _SignInState extends State<SignIn> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue