mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 18:13:31 +00:00
Merge branch 'signin-refactoring' into 'main'
Refactor bootstrapping process to have more statusing, timeouts and go to sign... See merge request mysocialportal/relatica!47
This commit is contained in:
commit
2419cfa164
12 changed files with 223 additions and 113 deletions
|
@ -32,6 +32,7 @@ import 'services/persistent_info_service.dart';
|
||||||
import 'services/reshared_via_service.dart';
|
import 'services/reshared_via_service.dart';
|
||||||
import 'services/secrets_service.dart';
|
import 'services/secrets_service.dart';
|
||||||
import 'services/setting_service.dart';
|
import 'services/setting_service.dart';
|
||||||
|
import 'services/status_service.dart';
|
||||||
import 'services/timeline_entry_filter_service.dart';
|
import 'services/timeline_entry_filter_service.dart';
|
||||||
import 'services/timeline_manager.dart';
|
import 'services/timeline_manager.dart';
|
||||||
import 'update_timer_initialization.dart';
|
import 'update_timer_initialization.dart';
|
||||||
|
@ -40,6 +41,8 @@ import 'utils/active_profile_selector.dart';
|
||||||
final _logger = Logger('DI_Init');
|
final _logger = Logger('DI_Init');
|
||||||
|
|
||||||
Future<void> dependencyInjectionInitialization() async {
|
Future<void> dependencyInjectionInitialization() async {
|
||||||
|
getIt.registerSingleton<StatusService>(StatusService());
|
||||||
|
|
||||||
final appSupportdir = await getApplicationSupportDirectory();
|
final appSupportdir = await getApplicationSupportDirectory();
|
||||||
getIt.registerSingleton<ActiveProfileSelector<PersistentInfoService>>(
|
getIt.registerSingleton<ActiveProfileSelector<PersistentInfoService>>(
|
||||||
ActiveProfileSelector(
|
ActiveProfileSelector(
|
||||||
|
|
|
@ -781,7 +781,7 @@ class ProfileClient extends FriendicaClient {
|
||||||
_logger.finest(() => 'Getting logged in user profile');
|
_logger.finest(() => 'Getting logged in user profile');
|
||||||
final request =
|
final request =
|
||||||
Uri.parse('https://$serverName/api/v1/accounts/verify_credentials');
|
Uri.parse('https://$serverName/api/v1/accounts/verify_credentials');
|
||||||
return (await _getApiRequest(request))
|
return (await _getApiRequest(request, timeout: oauthTimeout))
|
||||||
.mapValue((json) => ConnectionMastodonExtensions.fromJson(
|
.mapValue((json) => ConnectionMastodonExtensions.fromJson(
|
||||||
json,
|
json,
|
||||||
defaultServerName: serverName,
|
defaultServerName: serverName,
|
||||||
|
@ -1083,25 +1083,40 @@ abstract class FriendicaClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest(
|
FutureResult<PagedResponse<List<dynamic>>, ExecError> _getApiListRequest(
|
||||||
Uri url) async {
|
Uri url, {
|
||||||
return await getUrl(url, headers: _headers).transformAsync(
|
Duration? timeout,
|
||||||
|
}) async {
|
||||||
|
return await getUrl(
|
||||||
|
url,
|
||||||
|
headers: _headers,
|
||||||
|
timeout: timeout,
|
||||||
|
).transformAsync(
|
||||||
(response) async {
|
(response) async {
|
||||||
return response.map((data) => jsonDecode(data) as List<dynamic>);
|
return response.map((data) => jsonDecode(data) as List<dynamic>);
|
||||||
},
|
},
|
||||||
).execErrorCastAsync();
|
).execErrorCastAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest(
|
FutureResult<PagedResponse<dynamic>, ExecError> _getApiPagedRequest(Uri url,
|
||||||
Uri url) async {
|
{Duration? timeout}) async {
|
||||||
return await getUrl(url, headers: _headers).transformAsync(
|
return await getUrl(
|
||||||
|
url,
|
||||||
|
headers: _headers,
|
||||||
|
timeout: timeout,
|
||||||
|
).transformAsync(
|
||||||
(response) async {
|
(response) async {
|
||||||
return response.map((data) => jsonDecode(data));
|
return response.map((data) => jsonDecode(data));
|
||||||
},
|
},
|
||||||
).execErrorCastAsync();
|
).execErrorCastAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<dynamic, ExecError> _getApiRequest(Uri url) async {
|
FutureResult<dynamic, ExecError> _getApiRequest(Uri url,
|
||||||
return await getUrl(url, headers: _headers).transformAsync(
|
{Duration? timeout}) async {
|
||||||
|
return await getUrl(
|
||||||
|
url,
|
||||||
|
headers: _headers,
|
||||||
|
timeout: timeout,
|
||||||
|
).transformAsync(
|
||||||
(response) async {
|
(response) async {
|
||||||
return jsonDecode(response.data);
|
return jsonDecode(response.data);
|
||||||
},
|
},
|
||||||
|
@ -1111,6 +1126,5 @@ abstract class FriendicaClient {
|
||||||
Map<String, String> get _headers => {
|
Map<String, String> get _headers => {
|
||||||
'Authorization': _profile.credentials.authHeaderValue,
|
'Authorization': _profile.credentials.authHeaderValue,
|
||||||
'Content-Type': 'application/json; charset=UTF-8',
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
if (usePhpDebugging) 'Cookie': 'XDEBUG_SESSION=PHPSTORM;path=/',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ final platformHasCamera = Platform.isIOS || Platform.isAndroid;
|
||||||
|
|
||||||
final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
|
final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
|
||||||
|
|
||||||
const usePhpDebugging = true;
|
const usePhpDebugging = false;
|
||||||
|
|
||||||
const maxViewPortalHeight = 750.0;
|
const maxViewPortalHeight = 750.0;
|
||||||
const maxViewPortalWidth = 750.0;
|
const maxViewPortalWidth = 750.0;
|
||||||
|
@ -23,6 +23,7 @@ const maxViewPortalWidth = 750.0;
|
||||||
const maxProcessingMillis = 3;
|
const maxProcessingMillis = 3;
|
||||||
const processingSleep = Duration(milliseconds: 1);
|
const processingSleep = Duration(milliseconds: 1);
|
||||||
const apiCallTimeout = Duration(seconds: 60);
|
const apiCallTimeout = Duration(seconds: 60);
|
||||||
|
const oauthTimeout = Duration(seconds: 15);
|
||||||
|
|
||||||
Future<bool?> showConfirmDialog(BuildContext context, String caption) {
|
Future<bool?> showConfirmDialog(BuildContext context, String caption) {
|
||||||
return showDialog<bool>(
|
return showDialog<bool>(
|
||||||
|
|
|
@ -21,6 +21,7 @@ import 'services/hashtag_service.dart';
|
||||||
import 'services/interactions_manager.dart';
|
import 'services/interactions_manager.dart';
|
||||||
import 'services/notifications_manager.dart';
|
import 'services/notifications_manager.dart';
|
||||||
import 'services/setting_service.dart';
|
import 'services/setting_service.dart';
|
||||||
|
import 'services/status_service.dart';
|
||||||
import 'services/timeline_entry_filter_service.dart';
|
import 'services/timeline_entry_filter_service.dart';
|
||||||
import 'services/timeline_manager.dart';
|
import 'services/timeline_manager.dart';
|
||||||
import 'utils/active_profile_selector.dart';
|
import 'utils/active_profile_selector.dart';
|
||||||
|
@ -62,6 +63,10 @@ class App extends StatelessWidget {
|
||||||
return Portal(
|
return Portal(
|
||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
ChangeNotifierProvider<StatusService>(
|
||||||
|
create: (_) => getIt<StatusService>(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
ChangeNotifierProvider<SettingsService>(
|
ChangeNotifierProvider<SettingsService>(
|
||||||
create: (_) => getIt<SettingsService>(),
|
create: (_) => getIt<SettingsService>(),
|
||||||
lazy: true,
|
lazy: true,
|
||||||
|
|
|
@ -2,11 +2,13 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../../globals.dart';
|
||||||
|
import '../../services/status_service.dart';
|
||||||
|
import '../../utils/network_utils.dart';
|
||||||
import '../exec_error.dart';
|
import '../exec_error.dart';
|
||||||
import 'credentials_intf.dart';
|
import 'credentials_intf.dart';
|
||||||
|
|
||||||
|
@ -91,30 +93,36 @@ class OAuthCredentials implements ICredentials {
|
||||||
'Client ID and Client Secret are already set, skipping ID fetching');
|
'Client ID and Client Secret are already set, skipping ID fetching');
|
||||||
return Result.ok(true);
|
return Result.ok(true);
|
||||||
}
|
}
|
||||||
|
getIt<StatusService>().setStatus('Attempting to contact $serverName...');
|
||||||
final idEndpoint = Uri.parse('https://$serverName/api/v1/apps');
|
final idEndpoint = Uri.parse('https://$serverName/api/v1/apps');
|
||||||
final response = await http.post(idEndpoint, body: {
|
final response = await postUrl(
|
||||||
|
idEndpoint,
|
||||||
|
{
|
||||||
'client_name': 'Relatica',
|
'client_name': 'Relatica',
|
||||||
'redirect_uris': redirectUrl,
|
'redirect_uris': redirectUrl,
|
||||||
'scopes': 'read write follow push',
|
'scopes': 'read write follow push',
|
||||||
'website': 'https://myportal.social',
|
'website': 'https://myportal.social',
|
||||||
});
|
},
|
||||||
|
timeout: oauthTimeout,
|
||||||
if (response.statusCode != 200) {
|
|
||||||
_logger.severe('Error: ${response.statusCode}: ${response.body}');
|
|
||||||
return buildErrorResult(
|
|
||||||
type: ErrorType.serverError,
|
|
||||||
message: 'Error: ${response.statusCode}: ${response.body}',
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
final json = jsonDecode(response.body);
|
response.match(onSuccess: (body) {
|
||||||
|
getIt<StatusService>().setStatus('Connected to $serverName...');
|
||||||
|
final json = jsonDecode(body);
|
||||||
clientId = json['client_id'];
|
clientId = json['client_id'];
|
||||||
clientSecret = json['client_secret'];
|
clientSecret = json['client_secret'];
|
||||||
return Result.ok(true);
|
}, onError: (error) {
|
||||||
|
getIt<StatusService>()
|
||||||
|
.setStatus('Error contacting $serverName...: $error');
|
||||||
|
_logger.severe('Error contacting $serverName...: $error');
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.mapValue((_) => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<bool, ExecError> _login() async {
|
FutureResult<bool, ExecError> _login() async {
|
||||||
if (accessToken.isNotEmpty) {
|
if (accessToken.isNotEmpty) {
|
||||||
|
getIt<StatusService>().setStatus('Logged into $serverName');
|
||||||
_logger.info('Already have access token, skipping');
|
_logger.info('Already have access token, skipping');
|
||||||
return Result.ok(true);
|
return Result.ok(true);
|
||||||
}
|
}
|
||||||
|
@ -127,12 +135,20 @@ class OAuthCredentials implements ICredentials {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
getIt<StatusService>()
|
||||||
|
.setStatus('Attempting getting authorization to $serverName');
|
||||||
|
|
||||||
final result = await FlutterWebAuth2.authenticate(
|
final result = await FlutterWebAuth2.authenticate(
|
||||||
url: url.toString(), callbackUrlScheme: redirectScheme);
|
url: url.toString(),
|
||||||
|
callbackUrlScheme: redirectScheme,
|
||||||
|
);
|
||||||
final code = Uri.parse(result).queryParameters['code'];
|
final code = Uri.parse(result).queryParameters['code'];
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
'Error code was not returned with the query parameters: $result');
|
'Error code was not returned with the query parameters: $result');
|
||||||
|
getIt<StatusService>().setStatus(
|
||||||
|
'Error getting the response code during authentication to $serverName');
|
||||||
|
|
||||||
return buildErrorResult(
|
return buildErrorResult(
|
||||||
type: ErrorType.serverError,
|
type: ErrorType.serverError,
|
||||||
message: 'Error getting the response code during authentication',
|
message: 'Error getting the response code during authentication',
|
||||||
|
@ -146,16 +162,18 @@ class OAuthCredentials implements ICredentials {
|
||||||
'grant_type': 'authorization_code',
|
'grant_type': 'authorization_code',
|
||||||
'code': code,
|
'code': code,
|
||||||
};
|
};
|
||||||
final response = await http.post(url2, body: body);
|
final response = await postUrl(url2, body);
|
||||||
if (response.statusCode != 200) {
|
response.match(onSuccess: (body) {
|
||||||
_logger.severe('Error: ${response.statusCode}: ${response.body}');
|
getIt<StatusService>().setStatus('Logged into $serverName');
|
||||||
return buildErrorResult(
|
accessToken = jsonDecode(body)['access_token'];
|
||||||
type: ErrorType.serverError,
|
}, onError: (error) {
|
||||||
message: 'Error: ${response.statusCode}: ${response.body}',
|
getIt<StatusService>()
|
||||||
);
|
.setStatus('Error getting authorization to $serverName');
|
||||||
}
|
_logger.severe('Error doing OAUth processing: $error');
|
||||||
accessToken = jsonDecode(response.body)['access_token'];
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
getIt<StatusService>()
|
||||||
|
.setStatus('Error getting authorization to $serverName');
|
||||||
_logger.severe('Exception while Doing OAuth Process: $e');
|
_logger.severe('Exception while Doing OAuth Process: $e');
|
||||||
return buildErrorResult(
|
return buildErrorResult(
|
||||||
type: ErrorType.serverError,
|
type: ErrorType.serverError,
|
||||||
|
|
|
@ -23,7 +23,7 @@ class SignInScreen extends StatefulWidget {
|
||||||
class _SignInScreenState extends State<SignInScreen> {
|
class _SignInScreenState extends State<SignInScreen> {
|
||||||
static final _logger = Logger('$SignInScreen');
|
static final _logger = Logger('$SignInScreen');
|
||||||
static const usernamePasswordType = 'Username/Password';
|
static const usernamePasswordType = 'Username/Password';
|
||||||
static const oauthType = 'OAuth';
|
static const oauthType = 'Standard Login';
|
||||||
static final authTypes = [usernamePasswordType, oauthType];
|
static final authTypes = [usernamePasswordType, oauthType];
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
final usernameController = TextEditingController();
|
final usernameController = TextEditingController();
|
||||||
|
@ -31,9 +31,12 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
final passwordController = TextEditingController();
|
final passwordController = TextEditingController();
|
||||||
var authType = oauthType;
|
var authType = oauthType;
|
||||||
var hidePassword = true;
|
var hidePassword = true;
|
||||||
var showUsernameAndPasswordFields = false;
|
|
||||||
var signInButtonEnabled = false;
|
var signInButtonEnabled = false;
|
||||||
var existingAccount = false;
|
Profile? existingProfile;
|
||||||
|
|
||||||
|
bool get showUsernameAndPasswordFields => authType == usernamePasswordType;
|
||||||
|
|
||||||
|
bool get existingAccount => existingProfile != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -47,32 +50,29 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void newProfile() {
|
void newProfile() {
|
||||||
usernameController.text = '';
|
usernameController.clear();
|
||||||
passwordController.text = '';
|
passwordController.clear();
|
||||||
serverNameController.text = '';
|
serverNameController.clear();
|
||||||
showUsernameAndPasswordFields = false;
|
|
||||||
authType = oauthType;
|
authType = oauthType;
|
||||||
signInButtonEnabled = true;
|
signInButtonEnabled = true;
|
||||||
existingAccount = false;
|
existingProfile = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBasicCredentials(BasicCredentials credentials) {
|
void setBasicCredentials(BasicCredentials credentials) {
|
||||||
usernameController.text = credentials.username;
|
usernameController.text = credentials.username;
|
||||||
passwordController.text = credentials.password;
|
passwordController.text = credentials.password;
|
||||||
serverNameController.text = credentials.serverName;
|
serverNameController.text = credentials.serverName;
|
||||||
showUsernameAndPasswordFields = true;
|
|
||||||
authType = usernamePasswordType;
|
authType = usernamePasswordType;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setOauthCredentials(OAuthCredentials credentials) {
|
void setOauthCredentials(OAuthCredentials credentials) {
|
||||||
serverNameController.text = credentials.serverName;
|
serverNameController.text = credentials.serverName;
|
||||||
showUsernameAndPasswordFields = false;
|
|
||||||
authType = oauthType;
|
authType = oauthType;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCredentials(BuildContext? context, Profile profile) {
|
void setCredentials(BuildContext? context, Profile profile) {
|
||||||
final ICredentials credentials = profile.credentials;
|
final ICredentials credentials = profile.credentials;
|
||||||
existingAccount = true;
|
existingProfile = profile;
|
||||||
signInButtonEnabled = !profile.loggedIn;
|
signInButtonEnabled = !profile.loggedIn;
|
||||||
if (credentials is BasicCredentials) {
|
if (credentials is BasicCredentials) {
|
||||||
setBasicCredentials(credentials);
|
setBasicCredentials(credentials);
|
||||||
|
@ -99,7 +99,20 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
final loggedOutProfiles = service.loggedOutProfiles;
|
final loggedOutProfiles = service.loggedOutProfiles;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Sign In'),
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Sign In'),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
tooltip: 'Add new account',
|
||||||
|
onPressed: () {
|
||||||
|
newProfile();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
@ -115,25 +128,16 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
.map(
|
.map(
|
||||||
(a) => DropdownMenuItem(value: a, child: Text(a)))
|
(a) => DropdownMenuItem(value: a, child: Text(a)))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: existingAccount
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
if (existingAccount) {
|
if (existingAccount) {
|
||||||
buildSnackbar(context,
|
buildSnackbar(context,
|
||||||
"Can't change the type on an existing account");
|
"Can't change the type on an existing account");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
authType = value!;
|
||||||
authType = value ?? '';
|
setState(() {});
|
||||||
switch (value) {
|
|
||||||
case usernamePasswordType:
|
|
||||||
showUsernameAndPasswordFields = true;
|
|
||||||
break;
|
|
||||||
case oauthType:
|
|
||||||
showUsernameAndPasswordFields = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
print("Don't know this");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
|
@ -156,6 +160,15 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
|
if (!showUsernameAndPasswordFields) ...[
|
||||||
|
Text(
|
||||||
|
existingAccount
|
||||||
|
? 'Configured to sign in as user ${existingProfile?.handle}'
|
||||||
|
: 'Relatica will open the requested Friendica site in a web browser where you will be asked to authorize this client.',
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
const VerticalPadding(),
|
||||||
|
],
|
||||||
if (showUsernameAndPasswordFields) ...[
|
if (showUsernameAndPasswordFields) ...[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
readOnly: existingAccount,
|
readOnly: existingAccount,
|
||||||
|
@ -194,6 +207,13 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
readOnly: existingAccount,
|
readOnly: existingAccount,
|
||||||
obscureText: hidePassword,
|
obscureText: hidePassword,
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Password field cannot be empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: const Icon(Icons.password),
|
prefixIcon: const Icon(Icons.password),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
|
@ -223,14 +243,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
onPressed: () => _signIn(context),
|
onPressed: () => _signIn(context),
|
||||||
child: const Text('Signin'),
|
child: const Text('Signin'),
|
||||||
)
|
)
|
||||||
: ElevatedButton(
|
: SizedBox(),
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
newProfile();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: const Text('New'),
|
|
||||||
),
|
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
Text(
|
Text(
|
||||||
'Logged out:',
|
'Logged out:',
|
||||||
|
@ -256,8 +269,8 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
},
|
},
|
||||||
title: Text(p.handle),
|
title: Text(p.handle),
|
||||||
subtitle: Text(p.credentials is BasicCredentials
|
subtitle: Text(p.credentials is BasicCredentials
|
||||||
? 'Username/Password'
|
? usernamePasswordType
|
||||||
: 'OAuth Login'),
|
: oauthType),
|
||||||
trailing: ElevatedButton(
|
trailing: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final confirm = await showYesNoDialog(context,
|
final confirm = await showYesNoDialog(context,
|
||||||
|
@ -265,6 +278,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
if (confirm ?? false) {
|
if (confirm ?? false) {
|
||||||
await service.removeProfile(p);
|
await service.removeProfile(p);
|
||||||
}
|
}
|
||||||
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: const Text('Remove'),
|
child: const Text('Remove'),
|
||||||
),
|
),
|
||||||
|
@ -297,16 +311,6 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setCredentials(context, p);
|
setCredentials(context, p);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
final confirm = await showYesNoDialog(
|
|
||||||
context, 'Switch to account?');
|
|
||||||
|
|
||||||
if (confirm ?? false) {
|
|
||||||
service.setActiveProfile(p);
|
|
||||||
if (mounted) {
|
|
||||||
context.goNamed(ScreenPaths.timelines);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
title: Text(
|
title: Text(
|
||||||
p.handle,
|
p.handle,
|
||||||
|
@ -318,8 +322,8 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
p.credentials is BasicCredentials
|
p.credentials is BasicCredentials
|
||||||
? 'Username/Password'
|
? usernamePasswordType
|
||||||
: 'OAuth Login',
|
: oauthType,
|
||||||
style: active
|
style: active
|
||||||
? const TextStyle(
|
? const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
@ -332,6 +336,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
context, 'Log out account?');
|
context, 'Log out account?');
|
||||||
if (confirm == true) {
|
if (confirm == true) {
|
||||||
await getIt<AccountsService>().signOut(p);
|
await getIt<AccountsService>().signOut(p);
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Sign out'),
|
child: const Text('Sign out'),
|
||||||
|
@ -349,8 +354,17 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _signIn(BuildContext context) async {
|
void _signIn(BuildContext context) async {
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
final valid = formKey.currentState?.validate() ?? false;
|
||||||
|
if (!valid) {
|
||||||
|
buildSnackbar(
|
||||||
|
context, 'Cannot login. Fix highlighted errors and try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ICredentials? creds;
|
ICredentials? creds;
|
||||||
|
if (existingProfile != null) {
|
||||||
|
creds = existingProfile?.credentials;
|
||||||
|
} else {
|
||||||
switch (authType) {
|
switch (authType) {
|
||||||
case usernamePasswordType:
|
case usernamePasswordType:
|
||||||
creds = BasicCredentials(
|
creds = BasicCredentials(
|
||||||
|
@ -365,6 +379,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
buildSnackbar(context, 'Unknown authorization type: $authType');
|
buildSnackbar(context, 'Unknown authorization type: $authType');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (creds == null) {
|
if (creds == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -383,4 +398,3 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
|
import '../routes.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
|
import '../services/status_service.dart';
|
||||||
|
|
||||||
class SplashScreen extends StatelessWidget {
|
class SplashScreen extends StatelessWidget {
|
||||||
const SplashScreen({super.key});
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final statusService = context.watch<StatusService>();
|
||||||
|
final accountsService = context.watch<AccountsService>();
|
||||||
|
if (!accountsService.initializing && !accountsService.loggedIn) {
|
||||||
|
context.pushNamed(ScreenPaths.signin);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -27,7 +37,13 @@ class SplashScreen extends StatelessWidget {
|
||||||
const CircularProgressIndicator(),
|
const CircularProgressIndicator(),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
const Text('Logging in accounts...'),
|
const Text('Logging in accounts...'),
|
||||||
|
const VerticalPadding(),
|
||||||
],
|
],
|
||||||
|
Text(
|
||||||
|
statusService.status,
|
||||||
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,11 +7,13 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../di_initialization.dart';
|
import '../di_initialization.dart';
|
||||||
import '../friendica_client/friendica_client.dart';
|
import '../friendica_client/friendica_client.dart';
|
||||||
|
import '../globals.dart';
|
||||||
import '../models/auth/credentials_intf.dart';
|
import '../models/auth/credentials_intf.dart';
|
||||||
import '../models/auth/profile.dart';
|
import '../models/auth/profile.dart';
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
import '../update_timer_initialization.dart';
|
import '../update_timer_initialization.dart';
|
||||||
import 'secrets_service.dart';
|
import 'secrets_service.dart';
|
||||||
|
import 'status_service.dart';
|
||||||
|
|
||||||
class AccountsService extends ChangeNotifier {
|
class AccountsService extends ChangeNotifier {
|
||||||
static final _logger = Logger('$AccountsService');
|
static final _logger = Logger('$AccountsService');
|
||||||
|
@ -80,6 +82,8 @@ class AccountsService extends ChangeNotifier {
|
||||||
final client =
|
final client =
|
||||||
ProfileClient(Profile.credentialsOnly(signedInCredentials));
|
ProfileClient(Profile.credentialsOnly(signedInCredentials));
|
||||||
credentialsCache = signedInCredentials;
|
credentialsCache = signedInCredentials;
|
||||||
|
getIt<StatusService>().setStatus(
|
||||||
|
'Getting user profile from ${signedInCredentials.serverName}');
|
||||||
return await client.getMyProfile();
|
return await client.getMyProfile();
|
||||||
}).andThenAsync((profileData) async {
|
}).andThenAsync((profileData) async {
|
||||||
final loginProfile = Profile(
|
final loginProfile = Profile(
|
||||||
|
@ -91,6 +95,8 @@ class AccountsService extends ChangeNotifier {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getIt<StatusService>()
|
||||||
|
.setStatus('Loaded user profile ${profileData.handle}');
|
||||||
if (_loggedInProfiles.isEmpty) {
|
if (_loggedInProfiles.isEmpty) {
|
||||||
await setActiveProfile(loginProfile,
|
await setActiveProfile(loginProfile,
|
||||||
withNotification: withNotification);
|
withNotification: withNotification);
|
||||||
|
@ -106,6 +112,7 @@ class AccountsService extends ChangeNotifier {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
|
getIt<StatusService>().setStatus('Error signing in: ${result.error}');
|
||||||
_logger.severe('Error signing in: ${result.error}');
|
_logger.severe('Error signing in: ${result.error}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
lib/services/status_service.dart
Normal file
16
lib/services/status_service.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class StatusService extends ChangeNotifier {
|
||||||
|
String _lastStatus = 'None';
|
||||||
|
DateTime _lastStatusTime = DateTime.now();
|
||||||
|
|
||||||
|
String get status => _lastStatus;
|
||||||
|
|
||||||
|
DateTime get statusTime => _lastStatusTime;
|
||||||
|
|
||||||
|
void setStatus(String status) {
|
||||||
|
_lastStatus = status;
|
||||||
|
_lastStatusTime = DateTime.now();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,12 @@ final _logger = Logger('UpdateTimer');
|
||||||
|
|
||||||
void setupUpdateTimers() {
|
void setupUpdateTimers() {
|
||||||
Timer.periodic(_timerRefresh, (_) async {
|
Timer.periodic(_timerRefresh, (_) async {
|
||||||
|
final service = getIt<AccountsService>();
|
||||||
|
if (!service.loggedIn) {
|
||||||
|
_logger
|
||||||
|
.info('Trying to do update when no logged in accounts. Skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
executeUpdatesForProfile(getIt<AccountsService>().currentProfile);
|
executeUpdatesForProfile(getIt<AccountsService>().currentProfile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import '../globals.dart';
|
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../friendica_client/paged_response.dart';
|
import '../friendica_client/paged_response.dart';
|
||||||
|
import '../globals.dart';
|
||||||
import '../models/exec_error.dart';
|
import '../models/exec_error.dart';
|
||||||
|
|
||||||
final _logger = Logger('NetworkUtils');
|
final _logger = Logger('NetworkUtils');
|
||||||
|
@ -15,16 +15,21 @@ http.Response requestTimeout() => http.Response('Client side timeout', 408);
|
||||||
FutureResult<PagedResponse<String>, ExecError> getUrl(
|
FutureResult<PagedResponse<String>, ExecError> getUrl(
|
||||||
Uri url, {
|
Uri url, {
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
|
Duration? timeout,
|
||||||
}) async {
|
}) async {
|
||||||
_logger.finer('GET: $url');
|
_logger.finer('GET: $url');
|
||||||
|
final requestHeaders = headers ?? {};
|
||||||
|
if (usePhpDebugging) {
|
||||||
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final request = http.get(
|
final request = http.get(
|
||||||
url,
|
url,
|
||||||
headers: headers,
|
headers: requestHeaders,
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await request.timeout(
|
final response = await request.timeout(
|
||||||
apiCallTimeout,
|
timeout ?? apiCallTimeout,
|
||||||
onTimeout: requestTimeout,
|
onTimeout: requestTimeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -47,17 +52,22 @@ FutureResult<String, ExecError> postUrl(
|
||||||
Uri url,
|
Uri url,
|
||||||
Map<String, dynamic> body, {
|
Map<String, dynamic> body, {
|
||||||
Map<String, String>? headers,
|
Map<String, String>? headers,
|
||||||
|
Duration? timeout,
|
||||||
}) async {
|
}) async {
|
||||||
_logger.finer('POST: $url \n Body: $body');
|
_logger.finer('POST: $url \n Body: $body');
|
||||||
|
final requestHeaders = headers ?? {};
|
||||||
|
if (usePhpDebugging) {
|
||||||
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final request = http.post(
|
final request = http.post(
|
||||||
url,
|
url,
|
||||||
headers: headers,
|
headers: requestHeaders,
|
||||||
body: jsonEncode(body),
|
body: jsonEncode(body),
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await request.timeout(
|
final response = await request.timeout(
|
||||||
apiCallTimeout,
|
timeout ?? apiCallTimeout,
|
||||||
onTimeout: requestTimeout,
|
onTimeout: requestTimeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: relatica
|
||||||
description: A mobile and desktop client for interacting with the Friendica social network
|
description: A mobile and desktop client for interacting with the Friendica social network
|
||||||
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 0.7.2+1
|
version: 0.8.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
Loading…
Reference in a new issue