refactor: update AuthenticatedUser model to require id and remove password, enhance server URI handling in AddNewServer widget

This commit is contained in:
Dr-Blank 2024-10-04 02:31:51 -04:00
parent eda45efbce
commit fa815ae206
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
10 changed files with 297 additions and 72 deletions

View file

@ -7,7 +7,9 @@ import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/db/cache_manager.dart'; import 'package:vaani/db/cache_manager.dart';
import 'package:vaani/models/error_response.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/authenticated_user.dart';
import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/extensions/obfuscation.dart';
part 'api_provider.g.dart'; part 'api_provider.g.dart';
@ -49,6 +51,7 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
final apiSettings = ref.watch(apiSettingsProvider); final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser; final user = apiSettings.activeUser;
if (user == null) { if (user == null) {
_logger.severe('No active user can not provide authenticated api');
throw StateError('No active user'); throw StateError('No active user');
} }
return AudiobookshelfApi( return AudiobookshelfApi(
@ -97,17 +100,26 @@ class PersonalizedView extends _$PersonalizedView {
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
final apiSettings = ref.watch(apiSettingsProvider); final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser; final user = apiSettings.activeUser;
if (user == null) {
_logger.warning('no active user');
yield [];
return;
}
if (apiSettings.activeLibraryId == null) { if (apiSettings.activeLibraryId == null) {
// set it to default user library by logging in and getting the library id // set it to default user library by logging in and getting the library id
final login = final login = await ref.read(loginProvider().future);
await api.login(username: user!.username!, password: user.password!); if (login == null) {
_logger.shout('failed to login, not building personalized view');
yield [];
return;
}
ref.read(apiSettingsProvider.notifier).updateState( ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId), apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId),
); );
} }
// try to find in cache // try to find in cache
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}'; // final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}'; final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
final cachedRes = await apiResponseCacheManager.getFileFromMemory( final cachedRes = await apiResponseCacheManager.getFileFromMemory(
key, key,
) ?? ) ??
@ -127,7 +139,7 @@ class PersonalizedView extends _$PersonalizedView {
} }
} }
// ! exagerated delay // ! exaggerated delay
// await Future.delayed(const Duration(seconds: 2)); // await Future.delayed(const Duration(seconds: 2));
final res = await api.libraries final res = await api.libraries
.getPersonalized(libraryId: apiSettings.activeLibraryId!); .getPersonalized(libraryId: apiSettings.activeLibraryId!);
@ -151,6 +163,7 @@ class PersonalizedView extends _$PersonalizedView {
// method to force refresh the view and ignore the cache // method to force refresh the view and ignore the cache
Future<void> forceRefresh() async { Future<void> forceRefresh() async {
// clear the cache // clear the cache
// TODO: find a better way to clear the cache for only personalized view key
return apiResponseCacheManager.emptyCache(); return apiResponseCacheManager.emptyCache();
} }
} }
@ -173,6 +186,47 @@ FutureOr<User> me(
MeRef ref, MeRef ref,
) async { ) async {
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
final res = await api.me.getUser(); final errorResponseHandler = ErrorResponseHandler();
return res!; final res = await api.me.getUser(
responseErrorHandler: errorResponseHandler.storeError,
);
if (res == null) {
_logger.severe(
'me failed, got response: ${errorResponseHandler.response.obfuscate()}',
);
throw StateError('me failed');
}
return res;
}
@riverpod
FutureOr<LoginResponse?> login(
LoginRef ref, {
AuthenticatedUser? user,
}) async {
if (user == null) {
// try to get the user from settings
final apiSettings = ref.watch(apiSettingsProvider);
user = apiSettings.activeUser;
if (user == null) {
_logger.severe('no active user to login');
return null;
}
_logger.fine('no user provided, using active user: ${user.obfuscate()}');
}
final api = ref.watch(audiobookshelfApiProvider(user.server.serverUrl));
api.token = user.authToken;
var errorResponseHandler = ErrorResponseHandler();
_logger.fine('logging in with authenticated api');
final res = await api.misc.authorize(
responseErrorHandler: errorResponseHandler.storeError,
);
if (res == null) {
_logger.severe(
'login failed, got response: ${errorResponseHandler.response.obfuscate()}',
);
return null;
}
_logger.fine('login response: ${res.obfuscate()}');
return res;
} }

View file

@ -168,7 +168,7 @@ class _AudiobookshelfApiProviderElement
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl; Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
} }
String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14'; String _$authenticatedApiHash() => r'e662465f01ab1a6384db4738a3ae49b5fab48a4f';
/// get the api instance for the authenticated user /// get the api instance for the authenticated user
/// ///
@ -507,7 +507,7 @@ final fetchContinueListeningProvider =
typedef FetchContinueListeningRef typedef FetchContinueListeningRef
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>; = AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
String _$meHash() => r'bdc664c4fd867ad13018fa769ce7a6913248c44f'; String _$meHash() => r'da5f40b8063b0c0a6651fdcc4ac2d192d0dc7df6';
/// See also [me]. /// See also [me].
@ProviderFor(me) @ProviderFor(me)
@ -521,7 +521,134 @@ final meProvider = AutoDisposeFutureProvider<User>.internal(
); );
typedef MeRef = AutoDisposeFutureProviderRef<User>; typedef MeRef = AutoDisposeFutureProviderRef<User>;
String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9'; String _$loginHash() => r'eb1c4fcef1818dce994846c1adb8eca8f6ec9259';
/// See also [login].
@ProviderFor(login)
const loginProvider = LoginFamily();
/// See also [login].
class LoginFamily extends Family<AsyncValue<LoginResponse?>> {
/// See also [login].
const LoginFamily();
/// See also [login].
LoginProvider call({
AuthenticatedUser? user,
}) {
return LoginProvider(
user: user,
);
}
@override
LoginProvider getProviderOverride(
covariant LoginProvider provider,
) {
return call(
user: provider.user,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'loginProvider';
}
/// See also [login].
class LoginProvider extends AutoDisposeFutureProvider<LoginResponse?> {
/// See also [login].
LoginProvider({
AuthenticatedUser? user,
}) : this._internal(
(ref) => login(
ref as LoginRef,
user: user,
),
from: loginProvider,
name: r'loginProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$loginHash,
dependencies: LoginFamily._dependencies,
allTransitiveDependencies: LoginFamily._allTransitiveDependencies,
user: user,
);
LoginProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.user,
}) : super.internal();
final AuthenticatedUser? user;
@override
Override overrideWith(
FutureOr<LoginResponse?> Function(LoginRef provider) create,
) {
return ProviderOverride(
origin: this,
override: LoginProvider._internal(
(ref) => create(ref as LoginRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
user: user,
),
);
}
@override
AutoDisposeFutureProviderElement<LoginResponse?> createElement() {
return _LoginProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is LoginProvider && other.user == user;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, user.hashCode);
return _SystemHash.finish(hash);
}
}
mixin LoginRef on AutoDisposeFutureProviderRef<LoginResponse?> {
/// The parameter `user` of this provider.
AuthenticatedUser? get user;
}
class _LoginProviderElement
extends AutoDisposeFutureProviderElement<LoginResponse?> with LoginRef {
_LoginProviderElement(super.provider);
@override
AuthenticatedUser? get user => (origin as LoginProvider).user;
}
String _$personalizedViewHash() => r'65c0bc60e312d290498ab488496495114d407ccb';
/// fetch the personalized view /// fetch the personalized view
/// ///

View file

@ -17,7 +17,7 @@ class OnboardingSinglePage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider); final apiSettings = ref.watch(apiSettingsProvider);
final serverUriController = useTextEditingController( final serverUriController = useTextEditingController(
text: apiSettings.activeServer?.serverUrl.toString() ?? '', text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://',
); );
var audiobookshelfUri = makeBaseUrl(serverUriController.text); var audiobookshelfUri = makeBaseUrl(serverUriController.text);

View file

@ -76,7 +76,6 @@ class UserLoginWithPassword extends HookConsumerWidget {
final authenticatedUser = model.AuthenticatedUser( final authenticatedUser = model.AuthenticatedUser(
server: addServer(), server: addServer(),
id: success.user.id, id: success.user.id,
password: password,
username: username, username: username,
authToken: api.token!, authToken: api.token!,
); );

View file

@ -7,14 +7,18 @@ final _logger = Logger('ErrorResponse');
class ErrorResponseHandler { class ErrorResponseHandler {
String? name; String? name;
http.Response _response; http.Response _response;
bool logRawResponse;
ErrorResponseHandler({ ErrorResponseHandler({
this.name, this.name,
http.Response? response, http.Response? response,
this.logRawResponse = false,
}) : _response = response ?? http.Response('', 418); }) : _response = response ?? http.Response('', 418);
void storeError(http.Response response, [Object? error]) { void storeError(http.Response response, [Object? error]) {
_logger.fine('for $name got response: ${response.obfuscate()}'); if (logRawResponse) {
_logger.fine('for $name got response: ${response.obfuscate()}');
}
_response = response; _response = response;
} }

View file

@ -10,9 +10,8 @@ class AuthenticatedUser with _$AuthenticatedUser {
const factory AuthenticatedUser({ const factory AuthenticatedUser({
required AudiobookShelfServer server, required AudiobookShelfServer server,
required String authToken, required String authToken,
String? id, required String id,
String? username, String? username,
String? password,
}) = _AuthenticatedUser; }) = _AuthenticatedUser;
factory AuthenticatedUser.fromJson(Map<String, dynamic> json) => factory AuthenticatedUser.fromJson(Map<String, dynamic> json) =>

View file

@ -22,9 +22,8 @@ AuthenticatedUser _$AuthenticatedUserFromJson(Map<String, dynamic> json) {
mixin _$AuthenticatedUser { mixin _$AuthenticatedUser {
AudiobookShelfServer get server => throw _privateConstructorUsedError; AudiobookShelfServer get server => throw _privateConstructorUsedError;
String get authToken => throw _privateConstructorUsedError; String get authToken => throw _privateConstructorUsedError;
String? get id => throw _privateConstructorUsedError; String get id => throw _privateConstructorUsedError;
String? get username => throw _privateConstructorUsedError; String? get username => throw _privateConstructorUsedError;
String? get password => throw _privateConstructorUsedError;
/// Serializes this AuthenticatedUser to a JSON map. /// Serializes this AuthenticatedUser to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -45,9 +44,8 @@ abstract class $AuthenticatedUserCopyWith<$Res> {
$Res call( $Res call(
{AudiobookShelfServer server, {AudiobookShelfServer server,
String authToken, String authToken,
String? id, String id,
String? username, String? username});
String? password});
$AudiobookShelfServerCopyWith<$Res> get server; $AudiobookShelfServerCopyWith<$Res> get server;
} }
@ -69,9 +67,8 @@ class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
$Res call({ $Res call({
Object? server = null, Object? server = null,
Object? authToken = null, Object? authToken = null,
Object? id = freezed, Object? id = null,
Object? username = freezed, Object? username = freezed,
Object? password = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
server: null == server server: null == server
@ -82,18 +79,14 @@ class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser>
? _value.authToken ? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable : authToken // ignore: cast_nullable_to_non_nullable
as String, as String,
id: freezed == id id: null == id
? _value.id ? _value.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String?, as String,
username: freezed == username username: freezed == username
? _value.username ? _value.username
: username // ignore: cast_nullable_to_non_nullable : username // ignore: cast_nullable_to_non_nullable
as String?, as String?,
password: freezed == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val); ) as $Val);
} }
@ -119,9 +112,8 @@ abstract class _$$AuthenticatedUserImplCopyWith<$Res>
$Res call( $Res call(
{AudiobookShelfServer server, {AudiobookShelfServer server,
String authToken, String authToken,
String? id, String id,
String? username, String? username});
String? password});
@override @override
$AudiobookShelfServerCopyWith<$Res> get server; $AudiobookShelfServerCopyWith<$Res> get server;
@ -142,9 +134,8 @@ class __$$AuthenticatedUserImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? server = null, Object? server = null,
Object? authToken = null, Object? authToken = null,
Object? id = freezed, Object? id = null,
Object? username = freezed, Object? username = freezed,
Object? password = freezed,
}) { }) {
return _then(_$AuthenticatedUserImpl( return _then(_$AuthenticatedUserImpl(
server: null == server server: null == server
@ -155,18 +146,14 @@ class __$$AuthenticatedUserImplCopyWithImpl<$Res>
? _value.authToken ? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable : authToken // ignore: cast_nullable_to_non_nullable
as String, as String,
id: freezed == id id: null == id
? _value.id ? _value.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String?, as String,
username: freezed == username username: freezed == username
? _value.username ? _value.username
: username // ignore: cast_nullable_to_non_nullable : username // ignore: cast_nullable_to_non_nullable
as String?, as String?,
password: freezed == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String?,
)); ));
} }
} }
@ -177,9 +164,8 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser {
const _$AuthenticatedUserImpl( const _$AuthenticatedUserImpl(
{required this.server, {required this.server,
required this.authToken, required this.authToken,
this.id, required this.id,
this.username, this.username});
this.password});
factory _$AuthenticatedUserImpl.fromJson(Map<String, dynamic> json) => factory _$AuthenticatedUserImpl.fromJson(Map<String, dynamic> json) =>
_$$AuthenticatedUserImplFromJson(json); _$$AuthenticatedUserImplFromJson(json);
@ -189,15 +175,13 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser {
@override @override
final String authToken; final String authToken;
@override @override
final String? id; final String id;
@override @override
final String? username; final String? username;
@override
final String? password;
@override @override
String toString() { String toString() {
return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username, password: $password)'; return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username)';
} }
@override @override
@ -210,15 +194,12 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser {
other.authToken == authToken) && other.authToken == authToken) &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.username, username) || (identical(other.username, username) ||
other.username == username) && other.username == username));
(identical(other.password, password) ||
other.password == password));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => int get hashCode => Object.hash(runtimeType, server, authToken, id, username);
Object.hash(runtimeType, server, authToken, id, username, password);
/// Create a copy of AuthenticatedUser /// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -241,9 +222,8 @@ abstract class _AuthenticatedUser implements AuthenticatedUser {
const factory _AuthenticatedUser( const factory _AuthenticatedUser(
{required final AudiobookShelfServer server, {required final AudiobookShelfServer server,
required final String authToken, required final String authToken,
final String? id, required final String id,
final String? username, final String? username}) = _$AuthenticatedUserImpl;
final String? password}) = _$AuthenticatedUserImpl;
factory _AuthenticatedUser.fromJson(Map<String, dynamic> json) = factory _AuthenticatedUser.fromJson(Map<String, dynamic> json) =
_$AuthenticatedUserImpl.fromJson; _$AuthenticatedUserImpl.fromJson;
@ -253,11 +233,9 @@ abstract class _AuthenticatedUser implements AuthenticatedUser {
@override @override
String get authToken; String get authToken;
@override @override
String? get id; String get id;
@override @override
String? get username; String? get username;
@override
String? get password;
/// Create a copy of AuthenticatedUser /// Create a copy of AuthenticatedUser
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View file

@ -12,9 +12,8 @@ _$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
server: server:
AudiobookShelfServer.fromJson(json['server'] as Map<String, dynamic>), AudiobookShelfServer.fromJson(json['server'] as Map<String, dynamic>),
authToken: json['authToken'] as String, authToken: json['authToken'] as String,
id: json['id'] as String?, id: json['id'] as String,
username: json['username'] as String?, username: json['username'] as String?,
password: json['password'] as String?,
); );
Map<String, dynamic> _$$AuthenticatedUserImplToJson( Map<String, dynamic> _$$AuthenticatedUserImplToJson(
@ -24,5 +23,4 @@ Map<String, dynamic> _$$AuthenticatedUserImplToJson(
'authToken': instance.authToken, 'authToken': instance.authToken,
'id': instance.id, 'id': instance.id,
'username': instance.username, 'username': instance.username,
'password': instance.password,
}; };

View file

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:vaani/settings/models/api_settings.dart'; import 'package:vaani/settings/models/api_settings.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'; import 'package:vaani/settings/models/audiobookshelf_server.dart';
import 'package:vaani/settings/models/authenticated_user.dart'; import 'package:vaani/settings/models/authenticated_user.dart';
@ -67,7 +68,6 @@ extension ObfuscateAuthenticatedUser on AuthenticatedUser {
return this; return this;
} }
return copyWith( return copyWith(
password: password == null ? null : 'passwordObfuscated',
username: username == null ? null : 'usernameObfuscated', username: username == null ? null : 'usernameObfuscated',
authToken: 'authTokenObfuscated', authToken: 'authTokenObfuscated',
server: server.obfuscate(), server: server.obfuscate(),
@ -116,10 +116,54 @@ extension ObfuscateResponse on http.Response {
return this; return this;
} }
return http.Response( return http.Response(
body, obfuscateBody(),
statusCode, statusCode,
headers: headers, headers: headers,
request: request?.obfuscate(), request: request?.obfuscate(),
); );
} }
String obfuscateBody() {
if (!kReleaseMode) {
return body;
}
// replace any email addresses with emailObfuscated
// replace any phone numbers with phoneObfuscated
// replace any urls with urlObfuscated
// replace any tokens with tokenObfuscated
// token regex is `"token": "..."`
return body
.replaceAll(
RegExp(r'(\b\w+@\w+\.\w+\b)|'
r'(\b\d{3}-\d{3}-\d{4}\b)|'
r'(\bhttps?://\S+\b)'),
'obfuscated',
)
.replaceAll(
RegExp(r'"?token"?:?\s*"[^"]+"'),
'"token": "tokenObfuscated"',
);
}
}
extension ObfuscateLoginResponse on shelfsdk.LoginResponse {
shelfsdk.LoginResponse obfuscate() {
if (!kReleaseMode) {
return this;
}
return copyWith(
user: user.obfuscate(),
);
}
}
extension ObfuscateUser on shelfsdk.User {
shelfsdk.User obfuscate() {
if (!kReleaseMode) {
return this;
}
return shelfsdk.User.fromJson(
toJson()..['token'] = 'tokenObfuscated',
);
}
} }

View file

@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/main.dart';
final httpUrlRegExp = RegExp('https?://');
class AddNewServer extends HookConsumerWidget { class AddNewServer extends HookConsumerWidget {
const AddNewServer({ const AddNewServer({
@ -25,7 +28,8 @@ class AddNewServer extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final myController = controller ?? useTextEditingController(); final myController =
controller ?? useTextEditingController(text: 'https://');
var newServerURI = useValueListenable(myController); var newServerURI = useValueListenable(myController);
final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text)); final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text));
bool isServerAliveValue = isServerAlive.when( bool isServerAliveValue = isServerAlive.when(
@ -34,15 +38,33 @@ class AddNewServer extends HookConsumerWidget {
error: (error, _) => false, error: (error, _) => false,
); );
Uri parsedUri = Uri.parse('');
try {
parsedUri = Uri.parse(newServerURI.text);
} on FormatException {
// prepend https:// if not present
if (!newServerURI.text.startsWith(httpUrlRegExp)) {
myController.text = 'https://${newServerURI.text}';
parsedUri = Uri.parse(myController.text);
}
} catch (e) {
// do nothing
appLogger.severe('Error parsing URI: $e');
}
final canSubmit = !readOnly &&
(isServerAliveValue || (allowEmpty && newServerURI.text.isEmpty));
return TextFormField( return TextFormField(
readOnly: readOnly, readOnly: readOnly,
controller: controller, controller: controller,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
autofillHints: const [AutofillHints.url], autofillHints: const [AutofillHints.url],
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
onFieldSubmitted: (_) { onFieldSubmitted: canSubmit
onPressed?.call(); ? (_) {
}, onPressed?.call();
}
: null,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Server URI', labelText: 'Server URI',
labelStyle: TextStyle( labelStyle: TextStyle(
@ -50,8 +72,8 @@ class AddNewServer extends HookConsumerWidget {
), ),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixText: prefixText:
myController.text.startsWith(RegExp('https?://')) ? '' : 'https://', myController.text.startsWith(httpUrlRegExp) ? '' : 'https://',
prefixIcon: ServerAliveIcon(server: Uri.parse(newServerURI.text)), prefixIcon: ServerAliveIcon(server: parsedUri),
// add server button // add server button
suffixIcon: onPressed == null suffixIcon: onPressed == null
@ -65,10 +87,10 @@ class AddNewServer extends HookConsumerWidget {
focusColor: Theme.of(context).colorScheme.onSurface, focusColor: Theme.of(context).colorScheme.onSurface,
// should be enabled when // should be enabled when
onPressed: !readOnly && onPressed: canSubmit
(isServerAliveValue || ? () {
(allowEmpty && newServerURI.text.isEmpty)) onPressed?.call();
? onPressed }
: null, // disable button if server is not alive : null, // disable button if server is not alive
), ),
), ),