mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 20:07:55 -06:00
Fixed visual bugs, added notifications on document upload success, enabled editing in inbox, added hints
This commit is contained in:
8
lib/core/global/os_error_codes.dart
Normal file
8
lib/core/global/os_error_codes.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
enum OsErrorCodes {
|
||||
serverUnreachable(101),
|
||||
hostNotFound(7),
|
||||
invalidClientCertConfig(318767212);
|
||||
|
||||
const OsErrorCodes(this.code);
|
||||
final int code;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
@@ -26,7 +26,6 @@ class SessionManager {
|
||||
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||
dio.interceptors.addAll([
|
||||
...interceptors,
|
||||
DioHttpErrorInterceptor(),
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
responseBody: false,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
58
lib/core/widgets/hint_card.dart
Normal file
58
lib/core/widgets/hint_card.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user