mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Merge branch 'revive-legacy-signin'
This commit is contained in:
commit
4a1900b283
14 changed files with 1029 additions and 724 deletions
|
@ -1501,6 +1501,10 @@
|
||||||
"homeTabMapBrowser": "Map",
|
"homeTabMapBrowser": "Map",
|
||||||
"mapBrowserSetDefaultDateRangeButton": "Set as default",
|
"mapBrowserSetDefaultDateRangeButton": "Set as default",
|
||||||
"todayText": "Today",
|
"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": "Unauthenticated access. Please sign-in again if the problem continues",
|
||||||
"@errorUnauthenticated": {
|
"@errorUnauthenticated": {
|
||||||
|
|
|
@ -260,6 +260,7 @@
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText",
|
"todayText",
|
||||||
|
"alternativeSignIn",
|
||||||
"errorUnauthenticated",
|
"errorUnauthenticated",
|
||||||
"errorDisconnected",
|
"errorDisconnected",
|
||||||
"errorLocked",
|
"errorLocked",
|
||||||
|
@ -272,7 +273,8 @@
|
||||||
|
|
||||||
"cs": [
|
"cs": [
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
|
@ -292,7 +294,8 @@
|
||||||
"searchLandingPeopleListEmptyText2",
|
"searchLandingPeopleListEmptyText2",
|
||||||
"mapBrowserDateRangeLabel",
|
"mapBrowserDateRangeLabel",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
|
@ -440,12 +443,14 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fi": [
|
"fi": [
|
||||||
|
@ -484,7 +489,8 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
|
@ -523,7 +529,8 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
|
@ -567,7 +574,8 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"nl": [
|
"nl": [
|
||||||
|
@ -948,6 +956,7 @@
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText",
|
"todayText",
|
||||||
|
"alternativeSignIn",
|
||||||
"errorUnauthenticated",
|
"errorUnauthenticated",
|
||||||
"errorDisconnected",
|
"errorDisconnected",
|
||||||
"errorLocked",
|
"errorLocked",
|
||||||
|
@ -998,7 +1007,8 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
|
@ -1057,7 +1067,8 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -1096,7 +1107,12 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
|
],
|
||||||
|
|
||||||
|
"tr": [
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
|
@ -1166,7 +1182,8 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh_Hant": [
|
"zh_Hant": [
|
||||||
|
@ -1330,6 +1347,7 @@
|
||||||
"mapBrowserDateRangeCustom",
|
"mapBrowserDateRangeCustom",
|
||||||
"homeTabMapBrowser",
|
"homeTabMapBrowser",
|
||||||
"mapBrowserSetDefaultDateRangeButton",
|
"mapBrowserSetDefaultDateRangeButton",
|
||||||
"todayText"
|
"todayText",
|
||||||
|
"alternativeSignIn"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<SignIn> {
|
|
||||||
@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<void> _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<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 {
|
|
||||||
await context.read<NpDb>().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<PrefController>();
|
|
||||||
unawaited(Pref().setAccounts3(accounts));
|
|
||||||
unawaited(prefController.setCurrentAccountIndex(accounts.indexOf(account)));
|
|
||||||
}
|
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -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");
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import 'package:clock/clock.dart';
|
||||||
import 'package:copy_with/copy_with.dart';
|
import 'package:copy_with/copy_with.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:nc_photos/account.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/account_controller.dart';
|
||||||
import 'package:nc_photos/controller/pref_controller.dart';
|
import 'package:nc_photos/controller/pref_controller.dart';
|
||||||
import 'package:nc_photos/db/entity_converter.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/pref.dart';
|
||||||
import 'package:nc_photos/entity/server_status.dart';
|
import 'package:nc_photos/entity/server_status.dart';
|
||||||
import 'package:nc_photos/exception_event.dart';
|
import 'package:nc_photos/exception_event.dart';
|
||||||
|
@ -49,7 +47,6 @@ class AccountPickerDialog extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => _Bloc(
|
create: (context) => _Bloc(
|
||||||
container: KiwiContainer().resolve(),
|
|
||||||
accountController: context.read(),
|
accountController: context.read(),
|
||||||
prefController: context.read(),
|
prefController: context.read(),
|
||||||
db: context.read(),
|
db: context.read(),
|
||||||
|
|
|
@ -3,13 +3,11 @@ part of '../account_picker_dialog.dart';
|
||||||
@npLog
|
@npLog
|
||||||
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
_Bloc({
|
_Bloc({
|
||||||
required DiContainer container,
|
|
||||||
required this.accountController,
|
required this.accountController,
|
||||||
required this.prefController,
|
required this.prefController,
|
||||||
required this.db,
|
required this.db,
|
||||||
}) : _c = container,
|
}) : super(_State.init(
|
||||||
super(_State.init(
|
accounts: prefController.accountsValue,
|
||||||
accounts: container.pref.getAccounts3Or([]),
|
|
||||||
)) {
|
)) {
|
||||||
on<_ToggleDropdown>(_onToggleDropdown);
|
on<_ToggleDropdown>(_onToggleDropdown);
|
||||||
on<_SwitchAccount>(_onSwitchAccount);
|
on<_SwitchAccount>(_onSwitchAccount);
|
||||||
|
@ -59,7 +57,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
));
|
));
|
||||||
try {
|
try {
|
||||||
await _prefLock.protect(() async {
|
await _prefLock.protect(() async {
|
||||||
final accounts = _c.pref.getAccounts3()!;
|
final accounts = prefController.accountsValue;
|
||||||
final currentAccount =
|
final currentAccount =
|
||||||
accounts[prefController.currentAccountIndexValue!];
|
accounts[prefController.currentAccountIndexValue!];
|
||||||
accounts.remove(ev.account);
|
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,
|
_log.shout("[_onDeleteAccount] Failed while removing account pref", e,
|
||||||
stackTrace);
|
stackTrace);
|
||||||
}
|
}
|
||||||
await Pref().setAccounts3(accounts);
|
await prefController.setAccounts(accounts);
|
||||||
await prefController.setCurrentAccountIndex(newAccountIndex);
|
await prefController.setCurrentAccountIndex(newAccountIndex);
|
||||||
|
|
||||||
// check if the same account (server + userId) still exists in known
|
// 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 AccountController accountController;
|
||||||
final PrefController prefController;
|
final PrefController prefController;
|
||||||
final NpDb db;
|
final NpDb db;
|
||||||
|
|
72
app/lib/widget/expandable_container.dart
Normal file
72
app/lib/widget/expandable_container.dart
Normal file
|
@ -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<StatefulWidget> createState() => ExpandableContainerState();
|
||||||
|
|
||||||
|
final bool isShow;
|
||||||
|
final Widget child;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpandableContainerState extends State<ExpandableContainer>
|
||||||
|
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<double> _animation;
|
||||||
|
var _size = Size.zero;
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ import 'package:nc_photos/controller/trusted_cert_controller.dart';
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.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/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/mobile/self_signed_cert_manager.dart';
|
||||||
import 'package:nc_photos/navigation_manager.dart';
|
import 'package:nc_photos/navigation_manager.dart';
|
||||||
import 'package:nc_photos/protected_page_handler.dart';
|
import 'package:nc_photos/protected_page_handler.dart';
|
||||||
|
@ -207,9 +206,6 @@ class _WrappedAppState extends State<_WrappedApp>
|
||||||
Splash.routeName: () => MaterialPageRoute(
|
Splash.routeName: () => MaterialPageRoute(
|
||||||
builder: (context) => const Splash(),
|
builder: (context) => const Splash(),
|
||||||
),
|
),
|
||||||
legacy.SignIn.routeName: () => MaterialPageRoute(
|
|
||||||
builder: (context) => const legacy.SignIn(),
|
|
||||||
),
|
|
||||||
CollectionPicker.routeName: CollectionPicker.buildRoute,
|
CollectionPicker.routeName: CollectionPicker.buildRoute,
|
||||||
LanguageSettings.routeName: LanguageSettings.buildRoute,
|
LanguageSettings.routeName: LanguageSettings.buildRoute,
|
||||||
PeopleBrowser.routeName: PeopleBrowser.buildRoute,
|
PeopleBrowser.routeName: PeopleBrowser.buildRoute,
|
||||||
|
|
|
@ -1,41 +1,67 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:copy_with/copy_with.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/app_localizations.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/controller/pref_controller.dart';
|
||||||
import 'package:nc_photos/db/entity_converter.dart';
|
import 'package:nc_photos/db/entity_converter.dart';
|
||||||
import 'package:nc_photos/entity/pref.dart';
|
import 'package:nc_photos/entity/pref.dart';
|
||||||
import 'package:nc_photos/entity/pref_util.dart' as pref_util;
|
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/legacy/connect.dart' as legacy;
|
||||||
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
|
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
|
||||||
import 'package:nc_photos/widget/connect.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/home.dart';
|
||||||
|
import 'package:nc_photos/widget/page_visibility_mixin.dart';
|
||||||
import 'package:nc_photos/widget/root_picker.dart';
|
import 'package:nc_photos/widget/root_picker.dart';
|
||||||
import 'package:np_codegen/np_codegen.dart';
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
import 'package:np_collection/np_collection.dart';
|
import 'package:np_collection/np_collection.dart';
|
||||||
import 'package:np_db/np_db.dart';
|
import 'package:np_db/np_db.dart';
|
||||||
import 'package:np_string/np_string.dart';
|
import 'package:np_string/np_string.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
part 'sign_in.g.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";
|
static const routeName = "/sign-in";
|
||||||
|
|
||||||
const SignIn({super.key});
|
const SignIn({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
createState() => _SignInState();
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => _Bloc(
|
||||||
|
npDb: context.read(),
|
||||||
|
prefController: context.read(),
|
||||||
|
),
|
||||||
|
child: const _WrappedSignIn(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WrappedSignIn extends StatefulWidget {
|
||||||
|
const _WrappedSignIn();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _WrappedSignInState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@npLog
|
@npLog
|
||||||
class _SignInState extends State<SignIn> {
|
class _WrappedSignInState extends State<_WrappedSignIn>
|
||||||
|
with RouteAware, PageVisibilityMixin {
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Theme(
|
return Theme(
|
||||||
data: buildDarkTheme(context).copyWith(
|
data: buildDarkTheme(context).copyWith(
|
||||||
scaffoldBackgroundColor: Colors.transparent,
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
|
@ -51,346 +77,126 @@ class _SignInState extends State<SignIn> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
const _SingInBackground(),
|
const _Background(),
|
||||||
Scaffold(
|
Scaffold(
|
||||||
body: Builder(builder: (context) => _buildContent(context)),
|
body: MultiBlocListener(
|
||||||
),
|
listeners: [
|
||||||
],
|
_BlocListenerT(
|
||||||
),
|
selector: (state) => state.connectArg,
|
||||||
);
|
listener: (context, connectArg) {
|
||||||
|
if (connectArg == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (connectArg.username != null &&
|
||||||
Widget _buildContent(BuildContext context) {
|
connectArg.password != null) {
|
||||||
if (_isConnecting) {
|
_onLegacyConnect(context, connectArg);
|
||||||
return const Stack(
|
|
||||||
children: [
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 64,
|
|
||||||
child: Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: AppIntermediateCircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return Form(
|
final uri = Uri.parse(
|
||||||
key: _formKey,
|
"${connectArg.scheme}://${connectArg.address}");
|
||||||
child: Center(
|
_onConnect(context, uri);
|
||||||
child: Container(
|
}
|
||||||
constraints: BoxConstraints(
|
},
|
||||||
maxWidth: Theme.of(context).widthLimitedContentMaxWidth,
|
),
|
||||||
),
|
_BlocListenerT(
|
||||||
child: Column(
|
selector: (state) => state.isCompleted,
|
||||||
children: [
|
listener: (context, isCompleted) {
|
||||||
Expanded(
|
if (isCompleted) {
|
||||||
child: Center(
|
Navigator.pushNamedAndRemoveUntil(
|
||||||
child: SingleChildScrollView(
|
context,
|
||||||
child: _SignInBody(
|
Home.routeName,
|
||||||
onSchemeSaved: (scheme) {
|
(route) => false,
|
||||||
_formValue.scheme = scheme;
|
arguments:
|
||||||
},
|
HomeArguments(context.state.connectedAccount!),
|
||||||
onServerUrlSaved: (url) {
|
);
|
||||||
_formValue.address = url;
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
_BlocListenerT(
|
||||||
),
|
selector: (state) => state.error,
|
||||||
),
|
listener: (context, error) {
|
||||||
Padding(
|
if (error != null && isPageVisible()) {
|
||||||
padding:
|
SnackBarManager().showSnackBarForException(error.error);
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
child: _BlocSelector(
|
||||||
|
selector: (state) => state.isConnecting,
|
||||||
|
builder: (context, isConnecting) =>
|
||||||
|
isConnecting ? const _ConnectingBody() : const _Body(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _connect() async {
|
Future<void> _onConnect(BuildContext context, Uri connectUri) async {
|
||||||
_formKey.currentState!.save();
|
var account = await Navigator.pushNamed<Account>(
|
||||||
Uri url = Uri.parse("${_formValue.scheme}://${_formValue.address}");
|
context,
|
||||||
_log.info("[_connect] Try connecting with url: $url");
|
Connect.routeName,
|
||||||
Account? account = await Navigator.pushNamed<Account>(
|
arguments: ConnectArguments(connectUri),
|
||||||
context, Connect.routeName,
|
);
|
||||||
arguments: ConnectArguments(url));
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
// connection failed
|
// connection failed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
account = await Navigator.pushNamed<Account>(context, RootPicker.routeName,
|
account = await Navigator.pushNamed<Account>(
|
||||||
arguments: RootPickerArguments(account));
|
context,
|
||||||
|
RootPicker.routeName,
|
||||||
|
arguments: RootPickerArguments(account),
|
||||||
|
);
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
// ???
|
// ???
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we've got a good account
|
// we've got a good account
|
||||||
setState(() {
|
context.addEvent(_SetConnectedAccount(account));
|
||||||
_isConnecting = true;
|
}
|
||||||
});
|
|
||||||
try {
|
Future<void> _onLegacyConnect(BuildContext context, _ConnectArg arg) async {
|
||||||
await _persistAccount(account);
|
Account? account = Account(
|
||||||
unawaited(
|
id: Account.newId(),
|
||||||
Navigator.pushNamedAndRemoveUntil(
|
scheme: arg.scheme,
|
||||||
context, Home.routeName, (route) => false,
|
address: arg.address,
|
||||||
arguments: HomeArguments(account)),
|
userId: arg.username!.toCi(),
|
||||||
|
username2: arg.username!,
|
||||||
|
password: arg.password!,
|
||||||
|
roots: [""],
|
||||||
);
|
);
|
||||||
} catch (_) {
|
_log.info("[_onLegacyConnect] Try connecting with account: $account");
|
||||||
setState(() {
|
account = await Navigator.pushNamed<Account>(
|
||||||
_isConnecting = false;
|
context,
|
||||||
});
|
legacy.Connect.routeName,
|
||||||
rethrow;
|
arguments: legacy.ConnectArguments(account),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _persistAccount(Account account) async {
|
|
||||||
await context.read<NpDb>().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<PrefController>();
|
|
||||||
unawaited(Pref().setAccounts3(accounts));
|
|
||||||
unawaited(prefController.setCurrentAccountIndex(accounts.indexOf(account)));
|
|
||||||
}
|
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
if (account == null) {
|
||||||
|
// connection failed
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
account = await Navigator.pushNamed<Account>(
|
||||||
|
context,
|
||||||
class _SignInBody extends StatelessWidget {
|
RootPicker.routeName,
|
||||||
const _SignInBody({
|
arguments: RootPickerArguments(account),
|
||||||
this.onSchemeSaved,
|
|
||||||
this.onServerUrlSaved,
|
|
||||||
});
|
|
||||||
|
|
||||||
@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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (account == null) {
|
||||||
|
// ???
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// we've got a good account
|
||||||
final void Function(String scheme)? onSchemeSaved;
|
context.addEvent(_SetConnectedAccount(account));
|
||||||
final void Function(String url)? onServerUrlSaved;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum _Scheme {
|
|
||||||
http,
|
|
||||||
https;
|
|
||||||
|
|
||||||
String toValueString() {
|
|
||||||
switch (this) {
|
|
||||||
case http:
|
|
||||||
return "http";
|
|
||||||
|
|
||||||
case https:
|
|
||||||
return "https";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||||
class _SchemeDropdown extends StatefulWidget {
|
// typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||||
const _SchemeDropdown({
|
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||||
this.onSaved,
|
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||||
});
|
typedef _Emitter = Emitter<_State>;
|
||||||
|
|
||||||
@override
|
extension on BuildContext {
|
||||||
State<StatefulWidget> createState() => _SchemeDropdownState();
|
_Bloc get bloc => read<_Bloc>();
|
||||||
|
_State get state => bloc.state;
|
||||||
final void Function(String scheme)? onSaved;
|
void addEvent(_Event event) => bloc.add(event);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,169 @@
|
||||||
|
|
||||||
part of 'sign_in.dart';
|
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,
|
||||||
|
String? username,
|
||||||
|
String? password,
|
||||||
|
bool? shouldObscurePassword,
|
||||||
|
_ConnectArg? connectArg,
|
||||||
|
Account? connectedAccount,
|
||||||
|
bool? isConnecting,
|
||||||
|
bool? isCompleted,
|
||||||
|
bool? isAltMode,
|
||||||
|
ExceptionEvent? error});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
|
_$_StateCopyWithWorkerImpl(this.that);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_State call(
|
||||||
|
{dynamic scheme,
|
||||||
|
dynamic serverUrl,
|
||||||
|
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,
|
||||||
|
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?);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _State that;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $_StateCopyWith on _State {
|
||||||
|
$_StateCopyWithWorker get copyWith => _$copyWith;
|
||||||
|
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// NpLogGenerator
|
// NpLogGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
extension _$_SignInStateNpLog on _SignInState {
|
extension _$_WrappedSignInStateNpLog on _WrappedSignInState {
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
Logger get _log => log;
|
Logger get _log => log;
|
||||||
|
|
||||||
static final log = Logger("widget.sign_in._SignInState");
|
static final log = Logger("widget.sign_in._WrappedSignInState");
|
||||||
|
}
|
||||||
|
|
||||||
|
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, username: $username, password: $password, shouldObscurePassword: $shouldObscurePassword, connectArg: $connectArg, connectedAccount: $connectedAccount, isConnecting: $isConnecting, isCompleted: $isCompleted, isAltMode: $isAltMode, 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 _$_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}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
131
app/lib/widget/sign_in/bloc.dart
Normal file
131
app/lib/widget/sign_in/bloc.dart
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
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<_SetAltMode>(_onSetAltMode);
|
||||||
|
on<_SetUsername>(_onSetUsername);
|
||||||
|
on<_SetPassword>(_onSetPassword);
|
||||||
|
on<_SetObscurePassword>(_onSetObscurePassword);
|
||||||
|
|
||||||
|
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 _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<void> _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 _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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _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;
|
||||||
|
}
|
138
app/lib/widget/sign_in/state_event.dart
Normal file
138
app/lib/widget/sign_in/state_event.dart
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
part of '../sign_in.dart';
|
||||||
|
|
||||||
|
@genCopyWith
|
||||||
|
@toString
|
||||||
|
class _State {
|
||||||
|
const _State({
|
||||||
|
required this.scheme,
|
||||||
|
required this.serverUrl,
|
||||||
|
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
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final _Scheme scheme;
|
||||||
|
final String serverUrl;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _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]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final Object error;
|
||||||
|
final StackTrace? stackTrace;
|
||||||
|
}
|
34
app/lib/widget/sign_in/type.dart
Normal file
34
app/lib/widget/sign_in/type.dart
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
part of '../sign_in.dart';
|
||||||
|
|
||||||
|
enum _Scheme {
|
||||||
|
http,
|
||||||
|
https;
|
||||||
|
|
||||||
|
String toValueString() {
|
||||||
|
switch (this) {
|
||||||
|
case http:
|
||||||
|
return "http";
|
||||||
|
|
||||||
|
case https:
|
||||||
|
return "https";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
326
app/lib/widget/sign_in/view.dart
Normal file
326
app/lib/widget/sign_in/view.dart
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
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<StatefulWidget> 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<FormState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue