Fixed visual bugs, added notifications on document upload success, enabled editing in inbox, added hints

This commit is contained in:
Anton Stubenbord
2023-01-11 18:28:42 +01:00
parent a4c4726c16
commit 4d7af3fffb
34 changed files with 1046 additions and 627 deletions

View File

@@ -0,0 +1,8 @@
enum OsErrorCodes {
serverUnreachable(101),
hostNotFound(7),
invalidClientCertConfig(318767212);
const OsErrorCodes(this.code);
final int code;
}

View File

@@ -11,22 +11,23 @@ class DioHttpErrorInterceptor extends Interceptor {
// try to parse contained error message, otherwise return response
final dynamic data = err.response?.data;
if (data is Map<String, dynamic>) {
_handlePaperlessValidationError(data, handler, err);
return _handlePaperlessValidationError(data, handler, err);
} else if (data is String) {
_handlePlainError(data, handler, err);
return _handlePlainError(data, handler, err);
}
} else if (err.error is SocketException) {
// Offline
handler.reject(
DioError(
error: const PaperlessServerException(ErrorCode.deviceOffline),
requestOptions: err.requestOptions,
type: DioErrorType.connectTimeout,
),
);
} else {
handler.reject(err);
final ex = err.error as SocketException;
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
return handler.reject(
DioError(
error: const PaperlessServerException(ErrorCode.deviceOffline),
requestOptions: err.requestOptions,
type: DioErrorType.connectTimeout,
),
);
}
}
return handler.reject(err);
}
void _handlePaperlessValidationError(
@@ -73,3 +74,11 @@ class DioHttpErrorInterceptor extends Interceptor {
}
}
}
enum _OsErrorCodes {
serverUnreachable(101),
hostNotFound(7);
const _OsErrorCodes(this.code);
final int code;
}

View File

@@ -0,0 +1,60 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:paperless_mobile/core/global/os_error_codes.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
class ServerReachabilityErrorInterceptor extends Interceptor {
static const _missingClientCertText = "No required SSL certificate was sent";
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
if (err.response?.statusCode == 400) {
final message = err.response?.data;
if (message is String && message.contains(_missingClientCertText)) {
return _rejectWithStatus(
ReachabilityStatus.missingClientCertificate,
err,
handler,
);
}
}
if (err.type == DioErrorType.connectTimeout) {
return _rejectWithStatus(
ReachabilityStatus.connectionTimeout,
err,
handler,
);
}
final error = err.error;
if (error is SocketException) {
final code = error.osError?.errorCode;
if (code == OsErrorCodes.serverUnreachable.code ||
code == OsErrorCodes.hostNotFound.code) {
return _rejectWithStatus(
ReachabilityStatus.unknownHost,
err,
handler,
);
}
}
return _rejectWithStatus(
ReachabilityStatus.notReachable,
err,
handler,
);
}
}
void _rejectWithStatus(
ReachabilityStatus reachabilityStatus,
DioError err,
ErrorInterceptorHandler handler,
) {
handler.reject(DioError(
error: reachabilityStatus,
requestOptions: err.requestOptions,
response: err.response,
type: DioErrorType.other,
));
}

View File

@@ -26,7 +26,6 @@ class SessionManager {
(client) => client..badCertificateCallback = (cert, host, port) => true;
dio.interceptors.addAll([
...interceptors,
DioHttpErrorInterceptor(),
PrettyDioLogger(
compact: true,
responseBody: false,

View File

@@ -1,8 +1,12 @@
import 'dart:developer';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:paperless_mobile/core/global/os_error_codes.dart';
import 'package:paperless_mobile/core/interceptor/server_reachability_error_interceptor.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
@@ -63,51 +67,30 @@ class ConnectivityStatusServiceImpl implements ConnectivityStatusService {
if (!RegExp(r"^https?://.*").hasMatch(serverAddress)) {
return ReachabilityStatus.unknown;
}
late SecurityContext context = SecurityContext();
try {
if (clientCertificate != null) {
context
..usePrivateKeyBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..useCertificateChainBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
)
..setTrustedCertificatesBytes(
clientCertificate.bytes,
password: clientCertificate.passphrase,
);
}
SessionManager manager =
SessionManager([ServerReachabilityErrorInterceptor()])
..updateSettings(clientCertificate: clientCertificate)
..client.options.connectTimeout = 5000
..client.options.receiveTimeout = 5000;
final adapter = DefaultHttpClientAdapter()
..onHttpClientCreate = (client) => HttpClient(context: context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
final Dio dio = Dio()..httpClientAdapter = adapter;
final response = await dio.get('$serverAddress/api/');
final response = await manager.client.get('$serverAddress/api/');
if (response.statusCode == 200) {
return ReachabilityStatus.reachable;
}
return ReachabilityStatus.notReachable;
} on DioError catch (error) {
if (error.error is String) {
if (error.response?.data is String) {
if ((error.response!.data as String)
.contains("No required SSL certificate was sent")) {
return ReachabilityStatus.missingClientCertificate;
}
}
if (error.type == DioErrorType.other &&
error.error is ReachabilityStatus) {
return error.error as ReachabilityStatus;
}
return ReachabilityStatus.notReachable;
} on TlsException catch (error) {
if (error.osError?.errorCode == 318767212) {
//INCORRECT_PASSWORD for certificate
final code = error.osError?.errorCode;
if (code == OsErrorCodes.invalidClientCertConfig.code) {
// Missing client cert passphrase
return ReachabilityStatus.invalidClientCertificateConfiguration;
}
return ReachabilityStatus.notReachable;
}
return ReachabilityStatus.notReachable;
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:shimmer/shimmer.dart';
class DocumentsListLoadingWidget extends StatelessWidget {
@@ -19,86 +20,69 @@ class DocumentsListLoadingWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: Column(
children: [
...above,
Expanded(
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final r = Random(index);
final tagCount = r.nextInt(tags.length + 1);
final correspondentLength = correspondentLengths[
r.nextInt(correspondentLengths.length - 1)];
final titleLength =
titleLengths[r.nextInt(titleLengths.length - 1)];
return ListTile(
isThreeLine: true,
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.white,
height: 50,
width: 35,
),
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
width: correspondentLength,
height: fontSize,
color: Colors.white,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
padding:
const EdgeInsets.symmetric(vertical: 2.0),
height: fontSize,
width: titleLength,
color: Colors.white,
),
Wrap(
spacing: 2.0,
children: List.generate(
tagCount,
(index) => InputChip(
label: Text(tags[r.nextInt(tags.length)]),
),
),
),
],
),
),
);
},
itemCount: 25,
return ListView(
children: <Widget>[
...above,
...List.generate(25, (idx) {
final r = Random(idx);
final tagCount = r.nextInt(tags.length + 1);
final correspondentLength =
correspondentLengths[r.nextInt(correspondentLengths.length - 1)];
final titleLength = titleLengths[r.nextInt(titleLengths.length - 1)];
return Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: ListTile(
contentPadding: const EdgeInsets.all(8),
dense: true,
isThreeLine: true,
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.white,
height: 50,
width: 35,
),
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
width: correspondentLength,
height: fontSize,
color: Colors.white,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
height: fontSize,
width: titleLength,
color: Colors.white,
),
),
...below,
],
Wrap(
spacing: 2.0,
children: List.generate(
tagCount,
(index) => InputChip(
label: Text(tags[r.nextInt(tags.length)]),
),
),
).paddedOnly(top: 4),
],
),
),
),
),
],
),
);
}).toList(),
...below,
],
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class HintCard extends StatelessWidget {
final String hintText;
final double elevation;
final VoidCallback onHintAcknowledged;
final bool show;
const HintCard({
super.key,
required this.hintText,
required this.onHintAcknowledged,
this.elevation = 1,
required this.show,
});
@override
Widget build(BuildContext context) {
return AnimatedCrossFade(
sizeCurve: Curves.elasticOut,
crossFadeState:
show ? CrossFadeState.showFirst : CrossFadeState.showSecond,
secondChild: const SizedBox.shrink(),
duration: const Duration(milliseconds: 500),
firstChild: Card(
elevation: elevation,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.tips_and_updates_outlined,
color: Theme.of(context).hintColor,
).padded(),
Align(
alignment: Alignment.center,
child: Text(
hintText,
softWrap: true,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
),
Align(
alignment: Alignment.bottomRight,
child: TextButton(
child: Text(S.of(context).genericAcknowledgeLabel),
onPressed: onHintAcknowledged,
),
),
],
).padded(),
).padded(),
);
}
}