mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 16:07:52 -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
|
// try to parse contained error message, otherwise return response
|
||||||
final dynamic data = err.response?.data;
|
final dynamic data = err.response?.data;
|
||||||
if (data is Map<String, dynamic>) {
|
if (data is Map<String, dynamic>) {
|
||||||
_handlePaperlessValidationError(data, handler, err);
|
return _handlePaperlessValidationError(data, handler, err);
|
||||||
} else if (data is String) {
|
} else if (data is String) {
|
||||||
_handlePlainError(data, handler, err);
|
return _handlePlainError(data, handler, err);
|
||||||
}
|
}
|
||||||
} else if (err.error is SocketException) {
|
} else if (err.error is SocketException) {
|
||||||
// Offline
|
final ex = err.error as SocketException;
|
||||||
handler.reject(
|
if (ex.osError?.errorCode == _OsErrorCodes.serverUnreachable.code) {
|
||||||
DioError(
|
return handler.reject(
|
||||||
error: const PaperlessServerException(ErrorCode.deviceOffline),
|
DioError(
|
||||||
requestOptions: err.requestOptions,
|
error: const PaperlessServerException(ErrorCode.deviceOffline),
|
||||||
type: DioErrorType.connectTimeout,
|
requestOptions: err.requestOptions,
|
||||||
),
|
type: DioErrorType.connectTimeout,
|
||||||
);
|
),
|
||||||
} else {
|
);
|
||||||
handler.reject(err);
|
}
|
||||||
}
|
}
|
||||||
|
return handler.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handlePaperlessValidationError(
|
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;
|
(client) => client..badCertificateCallback = (cert, host, port) => true;
|
||||||
dio.interceptors.addAll([
|
dio.interceptors.addAll([
|
||||||
...interceptors,
|
...interceptors,
|
||||||
DioHttpErrorInterceptor(),
|
|
||||||
PrettyDioLogger(
|
PrettyDioLogger(
|
||||||
compact: true,
|
compact: true,
|
||||||
responseBody: false,
|
responseBody: false,
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:dio/adapter.dart';
|
import 'package:dio/adapter.dart';
|
||||||
import 'package:dio/dio.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/client_certificate.dart';
|
||||||
import 'package:paperless_mobile/features/login/model/reachability_status.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)) {
|
if (!RegExp(r"^https?://.*").hasMatch(serverAddress)) {
|
||||||
return ReachabilityStatus.unknown;
|
return ReachabilityStatus.unknown;
|
||||||
}
|
}
|
||||||
late SecurityContext context = SecurityContext();
|
|
||||||
try {
|
try {
|
||||||
if (clientCertificate != null) {
|
SessionManager manager =
|
||||||
context
|
SessionManager([ServerReachabilityErrorInterceptor()])
|
||||||
..usePrivateKeyBytes(
|
..updateSettings(clientCertificate: clientCertificate)
|
||||||
clientCertificate.bytes,
|
..client.options.connectTimeout = 5000
|
||||||
password: clientCertificate.passphrase,
|
..client.options.receiveTimeout = 5000;
|
||||||
)
|
|
||||||
..useCertificateChainBytes(
|
|
||||||
clientCertificate.bytes,
|
|
||||||
password: clientCertificate.passphrase,
|
|
||||||
)
|
|
||||||
..setTrustedCertificatesBytes(
|
|
||||||
clientCertificate.bytes,
|
|
||||||
password: clientCertificate.passphrase,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final adapter = DefaultHttpClientAdapter()
|
final response = await manager.client.get('$serverAddress/api/');
|
||||||
..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/');
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return ReachabilityStatus.reachable;
|
return ReachabilityStatus.reachable;
|
||||||
}
|
}
|
||||||
return ReachabilityStatus.notReachable;
|
return ReachabilityStatus.notReachable;
|
||||||
} on DioError catch (error) {
|
} on DioError catch (error) {
|
||||||
if (error.error is String) {
|
if (error.type == DioErrorType.other &&
|
||||||
if (error.response?.data is String) {
|
error.error is ReachabilityStatus) {
|
||||||
if ((error.response!.data as String)
|
return error.error as ReachabilityStatus;
|
||||||
.contains("No required SSL certificate was sent")) {
|
|
||||||
return ReachabilityStatus.missingClientCertificate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ReachabilityStatus.notReachable;
|
|
||||||
} on TlsException catch (error) {
|
} on TlsException catch (error) {
|
||||||
if (error.osError?.errorCode == 318767212) {
|
final code = error.osError?.errorCode;
|
||||||
//INCORRECT_PASSWORD for certificate
|
if (code == OsErrorCodes.invalidClientCertConfig.code) {
|
||||||
|
// Missing client cert passphrase
|
||||||
return ReachabilityStatus.invalidClientCertificateConfiguration;
|
return ReachabilityStatus.invalidClientCertificateConfiguration;
|
||||||
}
|
}
|
||||||
return ReachabilityStatus.notReachable;
|
|
||||||
}
|
}
|
||||||
|
return ReachabilityStatus.notReachable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
|
||||||
class DocumentsListLoadingWidget extends StatelessWidget {
|
class DocumentsListLoadingWidget extends StatelessWidget {
|
||||||
@@ -19,86 +20,69 @@ class DocumentsListLoadingWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return ListView(
|
||||||
height: MediaQuery.of(context).size.height,
|
children: <Widget>[
|
||||||
width: double.infinity,
|
...above,
|
||||||
child: Column(
|
...List.generate(25, (idx) {
|
||||||
mainAxisSize: MainAxisSize.max,
|
final r = Random(idx);
|
||||||
children: <Widget>[
|
final tagCount = r.nextInt(tags.length + 1);
|
||||||
Expanded(
|
final correspondentLength =
|
||||||
child: Shimmer.fromColors(
|
correspondentLengths[r.nextInt(correspondentLengths.length - 1)];
|
||||||
baseColor: Theme.of(context).brightness == Brightness.light
|
final titleLength = titleLengths[r.nextInt(titleLengths.length - 1)];
|
||||||
? Colors.grey[300]!
|
return Shimmer.fromColors(
|
||||||
: Colors.grey[900]!,
|
baseColor: Theme.of(context).brightness == Brightness.light
|
||||||
highlightColor: Theme.of(context).brightness == Brightness.light
|
? Colors.grey[300]!
|
||||||
? Colors.grey[100]!
|
: Colors.grey[900]!,
|
||||||
: Colors.grey[600]!,
|
highlightColor: Theme.of(context).brightness == Brightness.light
|
||||||
child: Column(
|
? Colors.grey[100]!
|
||||||
children: [
|
: Colors.grey[600]!,
|
||||||
...above,
|
child: ListTile(
|
||||||
Expanded(
|
contentPadding: const EdgeInsets.all(8),
|
||||||
child: ListView.builder(
|
dense: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
isThreeLine: true,
|
||||||
itemBuilder: (context, index) {
|
leading: ClipRRect(
|
||||||
final r = Random(index);
|
borderRadius: BorderRadius.circular(8),
|
||||||
final tagCount = r.nextInt(tags.length + 1);
|
child: Container(
|
||||||
final correspondentLength = correspondentLengths[
|
color: Colors.white,
|
||||||
r.nextInt(correspondentLengths.length - 1)];
|
height: 50,
|
||||||
final titleLength =
|
width: 35,
|
||||||
titleLengths[r.nextInt(titleLengths.length - 1)];
|
),
|
||||||
return ListTile(
|
),
|
||||||
isThreeLine: true,
|
title: Container(
|
||||||
leading: ClipRRect(
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
borderRadius: BorderRadius.circular(8),
|
width: correspondentLength,
|
||||||
child: Container(
|
height: fontSize,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
height: 50,
|
),
|
||||||
width: 35,
|
subtitle: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
),
|
child: Column(
|
||||||
title: Container(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
width: correspondentLength,
|
children: [
|
||||||
height: fontSize,
|
Container(
|
||||||
color: Colors.white,
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
),
|
height: fontSize,
|
||||||
subtitle: Padding(
|
width: titleLength,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
color: Colors.white,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
Wrap(
|
||||||
...below,
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
||||||
@@ -77,7 +78,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
badgeColor: Theme.of(context).colorScheme.error,
|
badgeColor: Colors.red,
|
||||||
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -190,18 +191,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
children: [
|
children: [
|
||||||
_buildDocumentOverview(
|
_buildDocumentOverview(
|
||||||
state.document,
|
state.document,
|
||||||
widget.titleAndContentQueryString,
|
|
||||||
),
|
),
|
||||||
_buildDocumentContentView(
|
_buildDocumentContentView(
|
||||||
state.document,
|
state.document,
|
||||||
widget.titleAndContentQueryString,
|
|
||||||
state,
|
state,
|
||||||
),
|
),
|
||||||
_buildDocumentMetaDataView(
|
_buildDocumentMetaDataView(
|
||||||
state.document,
|
state.document,
|
||||||
),
|
),
|
||||||
].padded(),
|
],
|
||||||
);
|
).paddedSymmetrically(horizontal: 8);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -216,23 +215,34 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
Navigator.push<bool>(
|
Navigator.push<bool>(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => BlocProvider.value(
|
builder: (_) => MultiBlocProvider(
|
||||||
value: EditDocumentCubit(
|
providers: [
|
||||||
document,
|
BlocProvider.value(
|
||||||
documentsApi: context.read(),
|
value: EditDocumentCubit(
|
||||||
correspondentRepository: context.read(),
|
document,
|
||||||
documentTypeRepository: context.read(),
|
documentsApi: context.read(),
|
||||||
storagePathRepository: context.read(),
|
correspondentRepository: context.read(),
|
||||||
tagRepository: context.read(),
|
documentTypeRepository: context.read(),
|
||||||
),
|
storagePathRepository: context.read(),
|
||||||
|
tagRepository: context.read(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider<DocumentDetailsCubit>.value(
|
||||||
|
value: cubit,
|
||||||
|
),
|
||||||
|
],
|
||||||
child: BlocListener<EditDocumentCubit, EditDocumentState>(
|
child: BlocListener<EditDocumentCubit, EditDocumentState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous.document != current.document,
|
previous.document != current.document,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
cubit.replaceDocument(state.document);
|
cubit.replaceDocument(state.document);
|
||||||
},
|
},
|
||||||
child: DocumentEditPage(
|
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
suggestions: cubit.state.suggestions,
|
builder: (context, state) {
|
||||||
|
return DocumentEditPage(
|
||||||
|
suggestions: state.suggestions,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -273,8 +283,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
.documentArchiveSerialNumberPropertyLongLabel,
|
.documentArchiveSerialNumberPropertyLongLabel,
|
||||||
content: document.archiveSerialNumber != null
|
content: document.archiveSerialNumber != null
|
||||||
? Text(document.archiveSerialNumber.toString())
|
? Text(document.archiveSerialNumber.toString())
|
||||||
: OutlinedButton(
|
: TextButton.icon(
|
||||||
child: Text(S
|
icon: const Icon(Icons.archive),
|
||||||
|
label: Text(S
|
||||||
.of(context)
|
.of(context)
|
||||||
.documentDetailsPageAssignAsnButtonLabel),
|
.documentDetailsPageAssignAsnButtonLabel),
|
||||||
onPressed: widget.allowEdit
|
onPressed: widget.allowEdit
|
||||||
@@ -321,38 +332,46 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
|
|
||||||
Widget _buildDocumentContentView(
|
Widget _buildDocumentContentView(
|
||||||
DocumentModel document,
|
DocumentModel document,
|
||||||
String? match,
|
|
||||||
DocumentDetailsState state,
|
DocumentDetailsState state,
|
||||||
) {
|
) {
|
||||||
return ListView(
|
return SingleChildScrollView(
|
||||||
children: [
|
child: Column(
|
||||||
HighlightedText(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
text: (state.isFullContentLoaded
|
children: [
|
||||||
? state.fullContent
|
HighlightedText(
|
||||||
: document.content) ??
|
text: (state.isFullContentLoaded
|
||||||
"",
|
? state.fullContent
|
||||||
highlights: match == null ? [] : match.split(" "),
|
: document.content) ??
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
"",
|
||||||
caseSensitive: false,
|
highlights: widget.titleAndContentQueryString != null
|
||||||
),
|
? widget.titleAndContentQueryString!.split(" ")
|
||||||
if (!state.isFullContentLoaded && (document.content ?? '').isNotEmpty)
|
: [],
|
||||||
TextButton(
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
child: Text("Show full content ..."),
|
caseSensitive: false,
|
||||||
onPressed: () {
|
|
||||||
context.read<DocumentDetailsCubit>().loadFullContent();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
if (!state.isFullContentLoaded && (document.content ?? '').isNotEmpty)
|
||||||
).paddedOnly(top: 8);
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: TextButton(
|
||||||
|
child:
|
||||||
|
Text(S.of(context).documentDetailsPageLoadFullContentLabel),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<DocumentDetailsCubit>().loadFullContent();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padded(8).paddedOnly(top: 14),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentOverview(DocumentModel document, String? match) {
|
Widget _buildDocumentOverview(DocumentModel document) {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
_DetailsItem(
|
_DetailsItem(
|
||||||
content: HighlightedText(
|
content: HighlightedText(
|
||||||
text: document.title,
|
text: document.title,
|
||||||
highlights: match?.split(" ") ?? <String>[],
|
highlights: widget.titleAndContentQueryString?.split(" ") ?? [],
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
label: S.of(context).documentTitlePropertyLabel,
|
label: S.of(context).documentTitlePropertyLabel,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () => _onSubmit(state.document),
|
onPressed: () => _onSubmit(state.document),
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
label: Text(S.of(context).genericActionSaveLabel),
|
label: Text(S.of(context).genericActionUpdateLabel),
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(S.of(context).documentEditPageTitle),
|
title: Text(S.of(context).documentEditPageTitle),
|
||||||
@@ -75,31 +75,56 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
),
|
),
|
||||||
child: FormBuilder(
|
child: FormBuilder(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: ListView(children: [
|
child: ListView(
|
||||||
_buildTitleFormField(state.document.title).padded(),
|
children: [
|
||||||
_buildCreatedAtFormField(state.document.created).padded(),
|
_buildTitleFormField(state.document.title).padded(),
|
||||||
_buildDocumentTypeFormField(
|
_buildCreatedAtFormField(state.document.created).padded(),
|
||||||
state.document.documentType,
|
_buildDocumentTypeFormField(
|
||||||
state.documentTypes,
|
state.document.documentType,
|
||||||
).padded(),
|
state.documentTypes,
|
||||||
_buildCorrespondentFormField(
|
).padded(),
|
||||||
state.document.correspondent,
|
_buildCorrespondentFormField(
|
||||||
state.correspondents,
|
state.document.correspondent,
|
||||||
).padded(),
|
state.correspondents,
|
||||||
_buildStoragePathFormField(
|
).padded(),
|
||||||
state.document.storagePath,
|
_buildStoragePathFormField(
|
||||||
state.storagePaths,
|
state.document.storagePath,
|
||||||
).padded(),
|
state.storagePaths,
|
||||||
TagFormField(
|
).padded(),
|
||||||
initialValue:
|
TagFormField(
|
||||||
IdsTagsQuery.included(state.document.tags.toList()),
|
initialValue:
|
||||||
notAssignedSelectable: false,
|
IdsTagsQuery.included(state.document.tags.toList()),
|
||||||
anyAssignedSelectable: false,
|
notAssignedSelectable: false,
|
||||||
excludeAllowed: false,
|
anyAssignedSelectable: false,
|
||||||
name: fkTags,
|
excludeAllowed: false,
|
||||||
selectableOptions: state.tags,
|
name: fkTags,
|
||||||
).padded(),
|
selectableOptions: state.tags,
|
||||||
]),
|
suggestions: widget.suggestions.hasSuggestedTags
|
||||||
|
? _buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions: widget.suggestions.storagePaths,
|
||||||
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
|
label: Text(state.tags[itemData]!.name),
|
||||||
|
onPressed: () {
|
||||||
|
final currentTags = _formKey.currentState
|
||||||
|
?.fields[fkTags] as TagsQuery;
|
||||||
|
if (currentTags is IdsTagsQuery) {
|
||||||
|
_formKey.currentState?.fields[fkTags]
|
||||||
|
?.didChange((IdsTagsQuery.fromIds(
|
||||||
|
[...currentTags.ids, itemData])));
|
||||||
|
} else {
|
||||||
|
_formKey.currentState?.fields[fkTags]
|
||||||
|
?.didChange(
|
||||||
|
(IdsTagsQuery.fromIds([itemData])));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
).padded(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 64), // Prevent tags from being hidden by fab
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@@ -267,7 +292,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
_buildSuggestionsSkeleton<DateTime>(
|
_buildSuggestionsSkeleton<DateTime>(
|
||||||
suggestions: widget.suggestions.dates,
|
suggestions: widget.suggestions.dates,
|
||||||
itemBuilder: (context, itemData) => ActionChip(
|
itemBuilder: (context, itemData) => ActionChip(
|
||||||
label: Text(DateFormat.yMd().format(itemData)),
|
label: Text(DateFormat.yMMMd().format(itemData)),
|
||||||
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
||||||
?.didChange(itemData),
|
?.didChange(itemData),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty
|
|||||||
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/list/adaptive_documents_view.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/new_items_loading_widget.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/document_filter_panel.dart';
|
||||||
|
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/sort_documents_button.dart';
|
||||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||||
@@ -95,156 +96,196 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous != ConnectivityState.connected &&
|
!previous.isSuccess && current.isSuccess,
|
||||||
current == ConnectivityState.connected,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
try {
|
showSnackBar(
|
||||||
context.read<DocumentsCubit>().reload();
|
context,
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
S.of(context).documentsPageNewDocumentAvailableText,
|
||||||
showErrorMessage(context, error, stackTrace);
|
action: SnackBarActionConfig(
|
||||||
}
|
label: S
|
||||||
},
|
.of(context)
|
||||||
builder: (context, connectivityState) {
|
.documentUploadProcessingSuccessfulReloadActionText,
|
||||||
const linearProgressIndicatorHeight = 4.0;
|
onPressed: () {
|
||||||
return Scaffold(
|
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
||||||
drawer: BlocProvider.value(
|
context.read<DocumentsCubit>().reload();
|
||||||
value: context.read<AuthenticationCubit>(),
|
},
|
||||||
child: InfoDrawer(
|
|
||||||
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
appBar: PreferredSize(
|
duration: const Duration(seconds: 10),
|
||||||
preferredSize: const Size.fromHeight(
|
);
|
||||||
kToolbarHeight + linearProgressIndicatorHeight,
|
},
|
||||||
|
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous != ConnectivityState.connected &&
|
||||||
|
current == ConnectivityState.connected,
|
||||||
|
listener: (context, state) {
|
||||||
|
try {
|
||||||
|
context.read<DocumentsCubit>().reload();
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, connectivityState) {
|
||||||
|
const linearProgressIndicatorHeight = 4.0;
|
||||||
|
return Scaffold(
|
||||||
|
drawer: BlocProvider.value(
|
||||||
|
value: context.read<AuthenticationCubit>(),
|
||||||
|
child: InfoDrawer(
|
||||||
|
afterInboxClosed: () => context.read<DocumentsCubit>().reload(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
appBar: PreferredSize(
|
||||||
builder: (context, state) {
|
preferredSize: const Size.fromHeight(
|
||||||
return AppBar(
|
kToolbarHeight + linearProgressIndicatorHeight,
|
||||||
title: Text(
|
),
|
||||||
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
),
|
builder: (context, state) {
|
||||||
actions: [
|
if (state.selection.isEmpty) {
|
||||||
const SortDocumentsButton(),
|
return AppBar(
|
||||||
BlocBuilder<ApplicationSettingsCubit,
|
title: Text(
|
||||||
ApplicationSettingsState>(
|
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
|
||||||
builder: (context, settingsState) => IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
settingsState.preferredViewType == ViewType.grid
|
|
||||||
? Icons.list
|
|
||||||
: Icons.grid_view_rounded,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
// Reset saved view widget position as scroll offset will be reset anyway.
|
|
||||||
setState(() {
|
|
||||||
_offset = 0;
|
|
||||||
_last = 0;
|
|
||||||
});
|
|
||||||
final cubit =
|
|
||||||
context.read<ApplicationSettingsCubit>();
|
|
||||||
cubit.setViewType(
|
|
||||||
cubit.state.preferredViewType.toggle());
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
const SortDocumentsButton(),
|
||||||
|
BlocBuilder<ApplicationSettingsCubit,
|
||||||
|
ApplicationSettingsState>(
|
||||||
|
builder: (context, settingsState) => IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
settingsState.preferredViewType == ViewType.grid
|
||||||
|
? Icons.list
|
||||||
|
: Icons.grid_view_rounded,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// Reset saved view widget position as scroll offset will be reset anyway.
|
||||||
|
setState(() {
|
||||||
|
_offset = 0;
|
||||||
|
_last = 0;
|
||||||
|
});
|
||||||
|
final cubit =
|
||||||
|
context.read<ApplicationSettingsCubit>();
|
||||||
|
cubit.setViewType(
|
||||||
|
cubit.state.preferredViewType.toggle());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(
|
||||||
|
linearProgressIndicatorHeight),
|
||||||
|
child: state.isLoading
|
||||||
|
? const LinearProgressIndicator()
|
||||||
|
: const SizedBox(height: 4.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () =>
|
||||||
|
context.read<DocumentsCubit>().resetSelection(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'${state.selection.length} ${S.of(context).documentsSelectedText}'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () => _onDelete(context, state),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
||||||
|
return b.Badge(
|
||||||
|
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
||||||
|
showBadge: appliedFiltersCount > 0,
|
||||||
|
badgeContent: Text(
|
||||||
|
'$appliedFiltersCount',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
bottom: PreferredSize(
|
animationType: b.BadgeAnimationType.fade,
|
||||||
preferredSize:
|
badgeColor: Colors.red,
|
||||||
const Size.fromHeight(linearProgressIndicatorHeight),
|
child: FloatingActionButton(
|
||||||
child: state.isLoading
|
child: const Icon(Icons.filter_alt_outlined),
|
||||||
? const LinearProgressIndicator()
|
onPressed: _openDocumentFilter,
|
||||||
: const SizedBox(height: 4.0),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
resizeToAvoidBottomInset: true,
|
||||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
body: WillPopScope(
|
||||||
builder: (context, state) {
|
onWillPop: () async {
|
||||||
final appliedFiltersCount = state.filter.appliedFiltersCount;
|
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||||
return b.Badge(
|
context.read<DocumentsCubit>().resetSelection();
|
||||||
position: b.BadgePosition.topEnd(top: -12, end: -6),
|
}
|
||||||
showBadge: appliedFiltersCount > 0,
|
return false;
|
||||||
badgeContent: Text(
|
},
|
||||||
'$appliedFiltersCount',
|
child: RefreshIndicator(
|
||||||
style: const TextStyle(
|
onRefresh: _onRefresh,
|
||||||
color: Colors.white,
|
notificationPredicate: (_) => connectivityState.isConnected,
|
||||||
),
|
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
||||||
),
|
builder: (context, taskState) {
|
||||||
animationType: b.BadgeAnimationType.fade,
|
return Stack(
|
||||||
badgeColor: Theme.of(context).colorScheme.error,
|
children: [
|
||||||
child: FloatingActionButton(
|
_buildBody(connectivityState),
|
||||||
child: const Icon(Icons.filter_alt_outlined),
|
Positioned(
|
||||||
onPressed: _openDocumentFilter,
|
left: 0,
|
||||||
),
|
right: 0,
|
||||||
);
|
top: _offset,
|
||||||
},
|
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||||
),
|
builder: (context, state) {
|
||||||
resizeToAvoidBottomInset: true,
|
return ColoredBox(
|
||||||
body: WillPopScope(
|
color: Theme.of(context).colorScheme.background,
|
||||||
onWillPop: () async {
|
child: SavedViewSelectionWidget(
|
||||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
height: _savedViewWidgetHeight,
|
||||||
context.read<DocumentsCubit>().resetSelection();
|
currentFilter: state.filter,
|
||||||
}
|
enabled: state.selection.isEmpty &&
|
||||||
return false;
|
connectivityState.isConnected,
|
||||||
},
|
),
|
||||||
child: RefreshIndicator(
|
);
|
||||||
onRefresh: _onRefresh,
|
},
|
||||||
notificationPredicate: (_) => connectivityState.isConnected,
|
),
|
||||||
child: BlocBuilder<TaskStatusCubit, TaskStatusState>(
|
|
||||||
builder: (context, taskState) {
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
_buildBody(connectivityState),
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
top: _offset,
|
|
||||||
child: BlocBuilder<DocumentsCubit, DocumentsState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return ColoredBox(
|
|
||||||
color: Theme.of(context).colorScheme.background,
|
|
||||||
child: SavedViewSelectionWidget(
|
|
||||||
height: _savedViewWidgetHeight,
|
|
||||||
currentFilter: state.filter,
|
|
||||||
enabled: state.selection.isEmpty &&
|
|
||||||
connectivityState.isConnected,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
if (taskState.task != null &&
|
);
|
||||||
taskState.isSuccess &&
|
},
|
||||||
!taskState.task!.acknowledged)
|
),
|
||||||
_buildNewDocumentAvailableButton(context),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Align _buildNewDocumentAvailableButton(BuildContext context) {
|
void _onDelete(BuildContext context, DocumentsState documentsState) async {
|
||||||
return Align(
|
final shouldDelete = await showDialog<bool>(
|
||||||
alignment: Alignment.bottomLeft,
|
context: context,
|
||||||
child: FilledButton(
|
builder: (context) =>
|
||||||
style: ButtonStyle(
|
BulkDeleteConfirmationDialog(state: documentsState),
|
||||||
backgroundColor:
|
) ??
|
||||||
MaterialStatePropertyAll(Theme.of(context).colorScheme.error),
|
false;
|
||||||
),
|
if (shouldDelete) {
|
||||||
child: Text("New document available!"),
|
try {
|
||||||
onPressed: () {
|
await context
|
||||||
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
.read<DocumentsCubit>()
|
||||||
context.read<DocumentsCubit>().reload();
|
.bulkRemove(documentsState.selection);
|
||||||
},
|
showSnackBar(
|
||||||
).paddedOnly(bottom: 24, left: 24),
|
context,
|
||||||
);
|
S.of(context).documentsPageBulkDeleteSuccessfulText,
|
||||||
|
);
|
||||||
|
context.read<DocumentsCubit>().resetSelection();
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openDocumentFilter() async {
|
void _openDocumentFilter() async {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class AdaptiveDocumentsView extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(child: beforeItems),
|
SliverToBoxAdapter(child: beforeItems),
|
||||||
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
|
if (viewType == ViewType.list) _buildListView() else _buildGridView(),
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class DocumentListItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
trailing: Text("${document.id}"),
|
|
||||||
dense: true,
|
dense: true,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
onTap: () => _onTap(),
|
onTap: () => _onTap(),
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
|
|||||||
initialValue: label,
|
initialValue: label,
|
||||||
fromJsonT: fromJsonT,
|
fromJsonT: fromJsonT,
|
||||||
submitButtonConfig: SubmitButtonConfig<T>(
|
submitButtonConfig: SubmitButtonConfig<T>(
|
||||||
icon: const Icon(Icons.done),
|
icon: const Icon(Icons.save),
|
||||||
label: Text(S.of(context).genericActionUpdateLabel),
|
label: Text(S.of(context).genericActionUpdateLabel),
|
||||||
onSubmit: context.read<EditLabelCubit<T>>().update,
|
onSubmit: context.read<EditLabelCubit<T>>().update,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
|
|||||||
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
import 'package:paperless_mobile/util.dart';
|
import 'package:paperless_mobile/util.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:url_launcher/link.dart';
|
import 'package:url_launcher/link.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
class InfoDrawer extends StatefulWidget {
|
class InfoDrawer extends StatefulWidget {
|
||||||
final VoidCallback? afterInboxClosed;
|
final VoidCallback? afterInboxClosed;
|
||||||
@@ -115,151 +113,167 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
|||||||
),
|
),
|
||||||
child: Drawer(
|
child: Drawer(
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topRight: Radius.circular(16.0),
|
topRight: Radius.circular(16.0),
|
||||||
bottomRight: Radius.circular(16.0),
|
bottomRight: Radius.circular(16.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ListView(
|
child: Theme(
|
||||||
children: [
|
data: Theme.of(context).copyWith(
|
||||||
DrawerHeader(
|
listTileTheme: ListTileThemeData(
|
||||||
padding: const EdgeInsets.only(
|
tileColor: Colors.transparent,
|
||||||
top: 8,
|
|
||||||
left: 8,
|
|
||||||
bottom: 0,
|
|
||||||
right: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Image.asset(
|
|
||||||
'assets/logos/paperless_logo_white.png',
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
color:
|
|
||||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
).paddedOnly(right: 8.0),
|
|
||||||
Text(
|
|
||||||
S.of(context).appTitleText,
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.headlineSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: BlocBuilder<PaperlessServerInformationCubit,
|
|
||||||
PaperlessServerInformationState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
if (!state.isLoaded) {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
final info = state.information!;
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
dense: true,
|
|
||||||
title: Text(
|
|
||||||
S.of(context).appDrawerHeaderLoggedInAsText +
|
|
||||||
(info.username ?? '?'),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
state.information!.host ?? '',
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})',
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.bodySmall,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
isThreeLine: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
...[
|
),
|
||||||
ListTile(
|
child: ListView(
|
||||||
title: Text(S.of(context).bottomNavInboxPageLabel),
|
children: [
|
||||||
leading: const Icon(Icons.inbox),
|
DrawerHeader(
|
||||||
onTap: () => _onOpenInbox(),
|
padding: const EdgeInsets.only(
|
||||||
shape: listtTileShape,
|
top: 8,
|
||||||
),
|
left: 8,
|
||||||
ListTile(
|
bottom: 0,
|
||||||
leading: const Icon(Icons.settings),
|
right: 8,
|
||||||
shape: listtTileShape,
|
|
||||||
title: Text(
|
|
||||||
S.of(context).appDrawerSettingsLabel,
|
|
||||||
),
|
),
|
||||||
onTap: () => Navigator.of(context).push(
|
child: Column(
|
||||||
MaterialPageRoute(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
builder: (context) => BlocProvider.value(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
value: context.read<ApplicationSettingsCubit>(),
|
children: [
|
||||||
child: const SettingsPage(),
|
Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/logos/paperless_logo_white.png',
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
).paddedOnly(right: 8.0),
|
||||||
|
Text(
|
||||||
|
S.of(context).appTitleText,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headlineSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: BlocBuilder<PaperlessServerInformationCubit,
|
||||||
|
PaperlessServerInformationState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (!state.isLoaded) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
final info = state.information!;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
S
|
||||||
|
.of(context)
|
||||||
|
.appDrawerHeaderLoggedInAsText +
|
||||||
|
(info.username ?? '?'),
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.bodyMedium,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
state.information!.host ?? '',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyMedium,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isThreeLine: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
ListTile(
|
||||||
|
title: Text(S.of(context).bottomNavInboxPageLabel),
|
||||||
|
leading: const Icon(Icons.inbox),
|
||||||
|
onTap: () => _onOpenInbox(),
|
||||||
|
shape: listtTileShape,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
shape: listtTileShape,
|
||||||
|
title: Text(
|
||||||
|
S.of(context).appDrawerSettingsLabel,
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => BlocProvider.value(
|
||||||
|
value: context.read<ApplicationSettingsCubit>(),
|
||||||
|
child: const SettingsPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const Divider(
|
||||||
ListTile(
|
indent: 16,
|
||||||
leading: const Icon(Icons.bug_report),
|
endIndent: 16,
|
||||||
title: Text(S.of(context).appDrawerReportBugLabel),
|
),
|
||||||
onTap: () {
|
ListTile(
|
||||||
launchUrlString(
|
leading: const Icon(Icons.bug_report),
|
||||||
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
title: Text(S.of(context).appDrawerReportBugLabel),
|
||||||
},
|
onTap: () {
|
||||||
shape: listtTileShape,
|
launchUrlString(
|
||||||
),
|
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||||
ListTile(
|
},
|
||||||
title: Text(S.of(context).appDrawerAboutLabel),
|
shape: listtTileShape,
|
||||||
leading: Icon(Icons.info_outline_rounded),
|
),
|
||||||
onTap: _onShowAboutDialog,
|
ListTile(
|
||||||
shape: listtTileShape,
|
title: Text(S.of(context).appDrawerAboutLabel),
|
||||||
),
|
leading: Icon(Icons.info_outline_rounded),
|
||||||
ListTile(
|
onTap: _onShowAboutDialog,
|
||||||
leading: const Icon(Icons.logout),
|
shape: listtTileShape,
|
||||||
title: Text(S.of(context).appDrawerLogoutLabel),
|
),
|
||||||
shape: listtTileShape,
|
ListTile(
|
||||||
onTap: () {
|
leading: const Icon(Icons.logout),
|
||||||
_onLogout();
|
title: Text(S.of(context).appDrawerLogoutLabel),
|
||||||
},
|
shape: listtTileShape,
|
||||||
)
|
onTap: () {
|
||||||
|
_onLogout();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -295,11 +309,10 @@ class _InfoDrawerState extends State<InfoDrawer> {
|
|||||||
create: (context) => InboxCubit(
|
create: (context) => InboxCubit(
|
||||||
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
context.read<LabelRepository<Tag, TagRepositoryState>>(),
|
||||||
context.read<PaperlessDocumentsApi>(),
|
context.read<PaperlessDocumentsApi>(),
|
||||||
)..loadInbox(),
|
)..initializeInbox(),
|
||||||
child: const InboxPage(),
|
child: const InboxPage(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
maintainState: false,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
widget.afterInboxClosed?.call();
|
widget.afterInboxClosed?.call();
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
|
import 'package:paperless_mobile/features/inbox/bloc/state/inbox_state.dart';
|
||||||
|
|
||||||
class InboxCubit extends Cubit<InboxState> {
|
class InboxCubit extends HydratedCubit<InboxState> {
|
||||||
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
|
final LabelRepository<Tag, TagRepositoryState> _tagsRepository;
|
||||||
final PaperlessDocumentsApi _documentsApi;
|
final PaperlessDocumentsApi _documentsApi;
|
||||||
|
|
||||||
@@ -14,17 +15,20 @@ class InboxCubit extends Cubit<InboxState> {
|
|||||||
///
|
///
|
||||||
/// Fetches inbox tag ids and loads the inbox items (documents).
|
/// Fetches inbox tag ids and loads the inbox items (documents).
|
||||||
///
|
///
|
||||||
Future<void> loadInbox() async {
|
Future<void> initializeInbox() async {
|
||||||
|
if (state.isLoaded) return;
|
||||||
final inboxTags = await _tagsRepository.findAll().then(
|
final inboxTags = await _tagsRepository.findAll().then(
|
||||||
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
|
(tags) => tags.where((t) => t.isInboxTag ?? false).map((t) => t.id!),
|
||||||
);
|
);
|
||||||
if (inboxTags.isEmpty) {
|
if (inboxTags.isEmpty) {
|
||||||
// no inbox tags = no inbox items.
|
// no inbox tags = no inbox items.
|
||||||
return emit(const InboxState(
|
return emit(
|
||||||
isLoaded: true,
|
state.copyWith(
|
||||||
inboxItems: [],
|
isLoaded: true,
|
||||||
inboxTags: [],
|
inboxItems: [],
|
||||||
));
|
inboxTags: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final inboxDocuments = await _documentsApi
|
final inboxDocuments = await _documentsApi
|
||||||
.findAll(DocumentFilter(
|
.findAll(DocumentFilter(
|
||||||
@@ -32,7 +36,7 @@ class InboxCubit extends Cubit<InboxState> {
|
|||||||
sortField: SortField.added,
|
sortField: SortField.added,
|
||||||
))
|
))
|
||||||
.then((psr) => psr.results);
|
.then((psr) => psr.results);
|
||||||
final newState = InboxState(
|
final newState = state.copyWith(
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
inboxItems: inboxDocuments,
|
inboxItems: inboxDocuments,
|
||||||
inboxTags: inboxTags,
|
inboxTags: inboxTags,
|
||||||
@@ -57,9 +61,8 @@ class InboxCubit extends Cubit<InboxState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
emit(
|
emit(
|
||||||
InboxState(
|
state.copyWith(
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
inboxTags: state.inboxTags,
|
|
||||||
inboxItems: state.inboxItems.where((doc) => doc.id != document.id),
|
inboxItems: state.inboxItems.where((doc) => doc.id != document.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -79,14 +82,11 @@ class InboxCubit extends Cubit<InboxState> {
|
|||||||
overwriteTags: true,
|
overwriteTags: true,
|
||||||
);
|
);
|
||||||
await _documentsApi.update(updatedDoc);
|
await _documentsApi.update(updatedDoc);
|
||||||
emit(
|
emit(state.copyWith(
|
||||||
InboxState(
|
isLoaded: true,
|
||||||
isLoaded: true,
|
inboxItems: [...state.inboxItems, updatedDoc]
|
||||||
inboxItems: [...state.inboxItems, updatedDoc]
|
..sort((d1, d2) => d2.added.compareTo(d1.added)),
|
||||||
..sort((d1, d2) => d2.added.compareTo(d1.added)),
|
));
|
||||||
inboxTags: state.inboxTags,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -99,12 +99,40 @@ class InboxCubit extends Cubit<InboxState> {
|
|||||||
state.inboxTags,
|
state.inboxTags,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
emit(
|
emit(state.copyWith(
|
||||||
InboxState(
|
isLoaded: true,
|
||||||
isLoaded: true,
|
inboxItems: [],
|
||||||
inboxTags: state.inboxTags,
|
));
|
||||||
inboxItems: [],
|
}
|
||||||
),
|
|
||||||
);
|
void replaceUpdatedDocument(DocumentModel document) {
|
||||||
|
if (document.tags.any((id) => state.inboxTags.contains(id))) {
|
||||||
|
// If replaced document still has inbox tag assigned:
|
||||||
|
emit(state.copyWith(
|
||||||
|
inboxItems:
|
||||||
|
state.inboxItems.map((e) => e.id == document.id ? document : e),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Remove tag from inbox.
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
inboxItems:
|
||||||
|
state.inboxItems.where((element) => element.id != document.id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void acknowledgeHint() {
|
||||||
|
emit(state.copyWith(isHintAcknowledged: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
InboxState fromJson(Map<String, dynamic> json) {
|
||||||
|
return InboxState.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson(InboxState state) {
|
||||||
|
return state.toJson();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,54 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'inbox_state.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
class InboxState with EquatableMixin {
|
class InboxState with EquatableMixin {
|
||||||
|
@JsonKey(ignore: true)
|
||||||
final bool isLoaded;
|
final bool isLoaded;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
final Iterable<int> inboxTags;
|
final Iterable<int> inboxTags;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
final Iterable<DocumentModel> inboxItems;
|
final Iterable<DocumentModel> inboxItems;
|
||||||
|
|
||||||
|
final bool isHintAcknowledged;
|
||||||
|
|
||||||
const InboxState({
|
const InboxState({
|
||||||
this.isLoaded = false,
|
this.isLoaded = false,
|
||||||
this.inboxTags = const [],
|
this.inboxTags = const [],
|
||||||
this.inboxItems = const [],
|
this.inboxItems = const [],
|
||||||
|
this.isHintAcknowledged = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [isLoaded, inboxTags, inboxItems];
|
List<Object?> get props => [
|
||||||
|
isLoaded,
|
||||||
|
inboxTags,
|
||||||
|
inboxItems,
|
||||||
|
isHintAcknowledged,
|
||||||
|
];
|
||||||
|
|
||||||
|
InboxState copyWith({
|
||||||
|
bool? isLoaded,
|
||||||
|
Iterable<int>? inboxTags,
|
||||||
|
Iterable<DocumentModel>? inboxItems,
|
||||||
|
bool? isHintAcknowledged,
|
||||||
|
}) {
|
||||||
|
return InboxState(
|
||||||
|
isLoaded: isLoaded ?? this.isLoaded,
|
||||||
|
inboxItems: inboxItems ?? this.inboxItems,
|
||||||
|
inboxTags: inboxTags ?? this.inboxTags,
|
||||||
|
isHintAcknowledged: isHintAcknowledged ?? this.isHintAcknowledged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InboxState.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$InboxStateFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$InboxStateToJson(this);
|
||||||
}
|
}
|
||||||
|
|||||||
16
lib/features/inbox/bloc/state/inbox_state.g.dart
Normal file
16
lib/features/inbox/bloc/state/inbox_state.g.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'inbox_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
InboxState _$InboxStateFromJson(Map<String, dynamic> json) => InboxState(
|
||||||
|
isHintAcknowledged: json['isHintAcknowledged'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$InboxStateToJson(InboxState instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'isHintAcknowledged': instance.isHintAcknowledged,
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
|
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
|
||||||
@@ -113,7 +114,6 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
childCount: entry.value.length,
|
childCount: entry.value.length,
|
||||||
(context, index) => _buildListItem(
|
(context, index) => _buildListItem(
|
||||||
context,
|
|
||||||
entry.value[index],
|
entry.value[index],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -124,7 +124,7 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () => context.read<InboxCubit>().loadInbox(),
|
onRefresh: () => context.read<InboxCubit>().initializeInbox(),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -132,11 +132,12 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Text(
|
child: HintCard(
|
||||||
S.of(context).inboxPageUsageHintText,
|
show: !state.isHintAcknowledged,
|
||||||
textAlign: TextAlign.center,
|
hintText: S.of(context).inboxPageUsageHintText,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
onHintAcknowledged: () =>
|
||||||
).padded(),
|
context.read<InboxCubit>().acknowledgeHint(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
...slivers
|
...slivers
|
||||||
],
|
],
|
||||||
@@ -150,7 +151,7 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildListItem(BuildContext context, DocumentModel doc) {
|
Widget _buildListItem(DocumentModel doc) {
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
direction: DismissDirection.endToStart,
|
direction: DismissDirection.endToStart,
|
||||||
background: Row(
|
background: Row(
|
||||||
@@ -170,7 +171,12 @@ class _InboxPageState extends State<InboxPage> {
|
|||||||
).padded(),
|
).padded(),
|
||||||
confirmDismiss: (_) => _onItemDismissed(doc),
|
confirmDismiss: (_) => _onItemDismissed(doc),
|
||||||
key: UniqueKey(),
|
key: UniqueKey(),
|
||||||
child: InboxItem(document: doc),
|
child: InboxItem(
|
||||||
|
document: doc,
|
||||||
|
onDocumentUpdated: (document) {
|
||||||
|
context.read<InboxCubit>().replaceUpdatedDocument(document);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class InboxEmptyWidget extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
key: _emptyStateRefreshIndicatorKey,
|
key: _emptyStateRefreshIndicatorKey,
|
||||||
onRefresh: () => context.read<InboxCubit>().loadInbox(),
|
onRefresh: () => context.read<InboxCubit>().initializeInbox(),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro
|
|||||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||||
|
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||||
|
|
||||||
class InboxItem extends StatelessWidget {
|
class InboxItem extends StatelessWidget {
|
||||||
static const _a4AspectRatio = 1 / 1.4142;
|
static const _a4AspectRatio = 1 / 1.4142;
|
||||||
|
final void Function(DocumentModel model) onDocumentUpdated;
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
|
|
||||||
const InboxItem({
|
const InboxItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.document,
|
required this.document,
|
||||||
|
required this.onDocumentUpdated,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -45,23 +47,27 @@ class InboxItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () => Navigator.push(
|
onTap: () async {
|
||||||
context,
|
final returnedDocument = await Navigator.push<DocumentModel?>(
|
||||||
MaterialPageRoute(
|
context,
|
||||||
builder: (context) => BlocProvider(
|
MaterialPageRoute(
|
||||||
create: (context) => DocumentDetailsCubit(
|
builder: (context) => BlocProvider(
|
||||||
context.read<PaperlessDocumentsApi>(),
|
create: (context) => DocumentDetailsCubit(
|
||||||
document,
|
context.read<PaperlessDocumentsApi>(),
|
||||||
),
|
document,
|
||||||
child: const LabelRepositoriesProvider(
|
),
|
||||||
child: DocumentDetailsPage(
|
child: const LabelRepositoriesProvider(
|
||||||
allowEdit: false,
|
child: DocumentDetailsPage(
|
||||||
isLabelClickable: false,
|
isLabelClickable: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
if (returnedDocument != null) {
|
||||||
|
onDocumentUpdated(returnedDocument);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class _StoragePathAutofillFormBuilderFieldState
|
|||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.start,
|
alignment: WrapAlignment.start,
|
||||||
spacing: 8.0,
|
spacing: 4.0,
|
||||||
|
runSpacing: 4.0,
|
||||||
children: [
|
children: [
|
||||||
InputChip(
|
InputChip(
|
||||||
label: Text(
|
label: Text(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
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:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
@@ -5,6 +7,7 @@ import 'package:flutter_typeahead/flutter_typeahead.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ class TagFormField extends StatefulWidget {
|
|||||||
final bool anyAssignedSelectable;
|
final bool anyAssignedSelectable;
|
||||||
final bool excludeAllowed;
|
final bool excludeAllowed;
|
||||||
final Map<int, Tag> selectableOptions;
|
final Map<int, Tag> selectableOptions;
|
||||||
|
final Widget? suggestions;
|
||||||
|
|
||||||
const TagFormField({
|
const TagFormField({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -26,6 +30,7 @@ class TagFormField extends StatefulWidget {
|
|||||||
this.anyAssignedSelectable = true,
|
this.anyAssignedSelectable = true,
|
||||||
this.excludeAllowed = true,
|
this.excludeAllowed = true,
|
||||||
required this.selectableOptions,
|
required this.selectableOptions,
|
||||||
|
this.suggestions,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -37,7 +42,7 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
static const _anyAssignedId = -2;
|
static const _anyAssignedId = -2;
|
||||||
|
|
||||||
late final TextEditingController _textEditingController;
|
late final TextEditingController _textEditingController;
|
||||||
bool _showCreationSuffixIcon = true;
|
bool _showCreationSuffixIcon = false;
|
||||||
bool _showClearSuffixIcon = false;
|
bool _showClearSuffixIcon = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -46,14 +51,19 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
_textEditingController = TextEditingController()
|
_textEditingController = TextEditingController()
|
||||||
..addListener(() {
|
..addListener(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showCreationSuffixIcon = widget.selectableOptions.values
|
_showCreationSuffixIcon = widget.selectableOptions.values.where(
|
||||||
.where(
|
(item) {
|
||||||
(item) => item.name.toLowerCase().startsWith(
|
log(item.name
|
||||||
_textEditingController.text.toLowerCase(),
|
.toLowerCase()
|
||||||
),
|
.startsWith(
|
||||||
|
_textEditingController.text.toLowerCase(),
|
||||||
)
|
)
|
||||||
.isEmpty ||
|
.toString());
|
||||||
_textEditingController.text.isEmpty;
|
return item.name.toLowerCase().startsWith(
|
||||||
|
_textEditingController.text.toLowerCase(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).isEmpty;
|
||||||
});
|
});
|
||||||
setState(
|
setState(
|
||||||
() => _showClearSuffixIcon = _textEditingController.text.isNotEmpty,
|
() => _showClearSuffixIcon = _textEditingController.text.isNotEmpty,
|
||||||
@@ -124,23 +134,33 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
getImmediateSuggestions: true,
|
getImmediateSuggestions: true,
|
||||||
animationStart: 1,
|
animationStart: 1,
|
||||||
itemBuilder: (context, data) {
|
itemBuilder: (context, data) {
|
||||||
if (data == _onlyNotAssignedId) {
|
late String? title;
|
||||||
return ListTile(
|
switch (data) {
|
||||||
title: Text(S.of(context).labelNotAssignedText),
|
case _onlyNotAssignedId:
|
||||||
);
|
title = S.of(context).labelNotAssignedText;
|
||||||
} else if (data == _anyAssignedId) {
|
break;
|
||||||
return ListTile(
|
case _anyAssignedId:
|
||||||
title: Text(S.of(context).labelAnyAssignedText),
|
title = S.of(context).labelAnyAssignedText;
|
||||||
);
|
break;
|
||||||
|
default:
|
||||||
|
title = widget.selectableOptions[data]?.name;
|
||||||
}
|
}
|
||||||
final tag = widget.selectableOptions[data]!;
|
|
||||||
|
final tag = widget.selectableOptions[data];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(
|
dense: true,
|
||||||
Icons.circle,
|
shape: RoundedRectangleBorder(
|
||||||
color: tag.color,
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
|
style: ListTileStyle.list,
|
||||||
|
leading: data != _onlyNotAssignedId && data != _anyAssignedId
|
||||||
|
? Icon(
|
||||||
|
Icons.circle,
|
||||||
|
color: tag?.color,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: Text(
|
title: Text(
|
||||||
tag.name,
|
title ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onBackground),
|
color: Theme.of(context).colorScheme.onBackground),
|
||||||
),
|
),
|
||||||
@@ -165,10 +185,11 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
direction: AxisDirection.up,
|
direction: AxisDirection.up,
|
||||||
),
|
),
|
||||||
if (field.value is OnlyNotAssignedTagsQuery) ...[
|
if (field.value is OnlyNotAssignedTagsQuery) ...[
|
||||||
_buildNotAssignedTag(field)
|
_buildNotAssignedTag(field).padded()
|
||||||
] else if (field.value is AnyAssignedTagsQuery) ...[
|
] else if (field.value is AnyAssignedTagsQuery) ...[
|
||||||
_buildAnyAssignedTag(field)
|
_buildAnyAssignedTag(field).padded()
|
||||||
] else ...[
|
] else ...[
|
||||||
|
if (widget.suggestions != null) widget.suggestions!,
|
||||||
// field.value is IdsTagsQuery
|
// field.value is IdsTagsQuery
|
||||||
Wrap(
|
Wrap(
|
||||||
alignment: WrapAlignment.start,
|
alignment: WrapAlignment.start,
|
||||||
@@ -183,7 +204,7 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
).padded(),
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -266,6 +287,7 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
tag.name,
|
tag.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: tag.textColor,
|
color: tag.textColor,
|
||||||
|
decorationColor: tag.textColor,
|
||||||
decoration: !isIncludedTag ? TextDecoration.lineThrough : null,
|
decoration: !isIncludedTag ? TextDecoration.lineThrough : null,
|
||||||
decorationThickness: 2.0,
|
decorationThickness: 2.0,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
),
|
),
|
||||||
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
body: BlocBuilder<LinkedDocumentsCubit, LinkedDocumentsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (!state.isLoaded) {
|
|
||||||
return const DocumentsListLoadingWidget();
|
|
||||||
}
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -35,37 +32,41 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
Expanded(
|
if (!state.isLoaded)
|
||||||
child: ListView.builder(
|
Expanded(child: const DocumentsListLoadingWidget())
|
||||||
itemBuilder: (context, index) {
|
else
|
||||||
return DocumentListItem(
|
Expanded(
|
||||||
isLabelClickable: false,
|
child: ListView.builder(
|
||||||
document: state.documents!.results.elementAt(index),
|
itemCount: state.documents?.results.length,
|
||||||
onTap: (doc) {
|
itemBuilder: (context, index) {
|
||||||
Navigator.push(
|
return DocumentListItem(
|
||||||
context,
|
isLabelClickable: false,
|
||||||
MaterialPageRoute(
|
document: state.documents!.results.elementAt(index),
|
||||||
builder: (context) => BlocProvider(
|
onTap: (doc) {
|
||||||
create: (context) => DocumentDetailsCubit(
|
Navigator.push(
|
||||||
context.read<PaperlessDocumentsApi>(),
|
context,
|
||||||
state.documents!.results.elementAt(index),
|
MaterialPageRoute(
|
||||||
),
|
builder: (context) => BlocProvider(
|
||||||
child: const DocumentDetailsPage(
|
create: (context) => DocumentDetailsCubit(
|
||||||
isLabelClickable: false,
|
context.read<PaperlessDocumentsApi>(),
|
||||||
allowEdit: false,
|
state.documents!.results.elementAt(index),
|
||||||
|
),
|
||||||
|
child: const DocumentDetailsPage(
|
||||||
|
isLabelClickable: false,
|
||||||
|
allowEdit: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
isSelected: false,
|
||||||
isSelected: false,
|
isAtLeastOneSelected: false,
|
||||||
isAtLeastOneSelected: false,
|
isTagSelectedPredicate: (_) => false,
|
||||||
isTagSelectedPredicate: (_) => false,
|
onTagSelected: (int tag) {},
|
||||||
onTagSelected: (int tag) {},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ enum ReachabilityStatus {
|
|||||||
unknownHost,
|
unknownHost,
|
||||||
missingClientCertificate,
|
missingClientCertificate,
|
||||||
invalidClientCertificateConfiguration,
|
invalidClientCertificateConfiguration,
|
||||||
|
connectionTimeout;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ class ServerAddressFormField extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
||||||
|
bool _canClear = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_textEditingController.addListener(() {
|
||||||
|
if (_textEditingController.text.isNotEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_canClear = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final TextEditingController _textEditingController = TextEditingController();
|
final TextEditingController _textEditingController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -25,12 +39,30 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
|
|||||||
key: const ValueKey('login-server-address'),
|
key: const ValueKey('login-server-address'),
|
||||||
controller: _textEditingController,
|
controller: _textEditingController,
|
||||||
name: ServerAddressFormField.fkServerAddress,
|
name: ServerAddressFormField.fkServerAddress,
|
||||||
validator: FormBuilderValidators.required(
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
errorText: S.of(context).loginPageServerUrlValidatorMessageRequiredText,
|
validator: FormBuilderValidators.compose([
|
||||||
),
|
FormBuilderValidators.required(
|
||||||
|
errorText:
|
||||||
|
S.of(context).loginPageServerUrlValidatorMessageRequiredText,
|
||||||
|
),
|
||||||
|
FormBuilderValidators.match(
|
||||||
|
r"https?://.*",
|
||||||
|
errorText:
|
||||||
|
S.of(context).loginPageServerUrlValidatorMessageMissingSchemeText,
|
||||||
|
)
|
||||||
|
]),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "http://192.168.1.50:8000",
|
hintText: "http://192.168.1.50:8000",
|
||||||
labelText: S.of(context).loginPageServerUrlFieldLabel,
|
labelText: S.of(context).loginPageServerUrlFieldLabel,
|
||||||
|
suffixIcon: _canClear
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(Icons.clear),
|
||||||
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
onPressed: () {
|
||||||
|
_textEditingController.clear();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
|
|||||||
@@ -24,28 +24,38 @@ class ServerConnectionPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
||||||
|
bool _isCheckingConnection = false;
|
||||||
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
ReachabilityStatus _reachabilityStatus = ReachabilityStatus.unknown;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
toolbarHeight: kToolbarHeight - 4,
|
||||||
title: Text(S.of(context).loginPageTitle),
|
title: Text(S.of(context).loginPageTitle),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
child: _isCheckingConnection
|
||||||
|
? const LinearProgressIndicator()
|
||||||
|
: const SizedBox(height: 4.0),
|
||||||
|
preferredSize: const Size.fromHeight(4.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: true,
|
resizeToAvoidBottomInset: true,
|
||||||
body: Column(
|
body: SingleChildScrollView(
|
||||||
children: [
|
child: Column(
|
||||||
ServerAddressFormField(
|
children: [
|
||||||
onDone: (address) {
|
ServerAddressFormField(
|
||||||
_updateReachability();
|
onDone: (address) {
|
||||||
},
|
_updateReachability(address);
|
||||||
).padded(),
|
},
|
||||||
ClientCertificateFormField(
|
).padded(),
|
||||||
onChanged: (_) => _updateReachability(),
|
ClientCertificateFormField(
|
||||||
).padded(),
|
onChanged: (_) => _updateReachability(),
|
||||||
_buildStatusIndicator(),
|
).padded(),
|
||||||
],
|
_buildStatusIndicator(),
|
||||||
).padded(),
|
],
|
||||||
|
).padded(),
|
||||||
|
),
|
||||||
bottomNavigationBar: BottomAppBar(
|
bottomNavigationBar: BottomAppBar(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@@ -62,20 +72,30 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateReachability() async {
|
Future<void> _updateReachability([String? address]) async {
|
||||||
|
setState(() {
|
||||||
|
_isCheckingConnection = true;
|
||||||
|
});
|
||||||
final status = await context
|
final status = await context
|
||||||
.read<ConnectivityStatusService>()
|
.read<ConnectivityStatusService>()
|
||||||
.isPaperlessServerReachable(
|
.isPaperlessServerReachable(
|
||||||
widget.formBuilderKey.currentState!
|
address ??
|
||||||
.getRawValue(ServerAddressFormField.fkServerAddress),
|
widget.formBuilderKey.currentState!
|
||||||
|
.getRawValue(ServerAddressFormField.fkServerAddress),
|
||||||
widget.formBuilderKey.currentState?.getRawValue(
|
widget.formBuilderKey.currentState?.getRawValue(
|
||||||
ClientCertificateFormField.fkClientCertificate,
|
ClientCertificateFormField.fkClientCertificate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() => _reachabilityStatus = status);
|
setState(() {
|
||||||
|
_isCheckingConnection = false;
|
||||||
|
_reachabilityStatus = status;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatusIndicator() {
|
Widget _buildStatusIndicator() {
|
||||||
|
if (_isCheckingConnection) {
|
||||||
|
return const ListTile();
|
||||||
|
}
|
||||||
Color errorColor = Theme.of(context).colorScheme.error;
|
Color errorColor = Theme.of(context).colorScheme.error;
|
||||||
switch (_reachabilityStatus) {
|
switch (_reachabilityStatus) {
|
||||||
case ReachabilityStatus.unknown:
|
case ReachabilityStatus.unknown:
|
||||||
@@ -112,6 +132,12 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
|
|||||||
.loginPageReachabilityInvalidClientCertificateConfigurationText,
|
.loginPageReachabilityInvalidClientCertificateConfigurationText,
|
||||||
errorColor,
|
errorColor,
|
||||||
);
|
);
|
||||||
|
case ReachabilityStatus.connectionTimeout:
|
||||||
|
return _buildIconText(
|
||||||
|
Icons.close,
|
||||||
|
S.of(context).loginPageReachabilityConnectionTimeoutText,
|
||||||
|
errorColor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,15 +91,16 @@ class LocalNotificationService {
|
|||||||
progress: progress,
|
progress: progress,
|
||||||
actions: status == TaskStatus.success
|
actions: status == TaskStatus.success
|
||||||
? [
|
? [
|
||||||
AndroidNotificationAction(
|
//TODO: Implement once moved to new routing
|
||||||
NotificationResponseAction.openCreatedDocument.name,
|
// AndroidNotificationAction(
|
||||||
"Open",
|
// NotificationResponseAction.openCreatedDocument.name,
|
||||||
showsUserInterface: true,
|
// "Open",
|
||||||
),
|
// showsUserInterface: true,
|
||||||
AndroidNotificationAction(
|
// ),
|
||||||
NotificationResponseAction.acknowledgeCreatedDocument.name,
|
// AndroidNotificationAction(
|
||||||
"Acknowledge",
|
// NotificationResponseAction.acknowledgeCreatedDocument.name,
|
||||||
),
|
// "Acknowledge",
|
||||||
|
// ),
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:paperless_mobile/core/repository/state/impl/document_type_reposi
|
|||||||
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/core/store/local_vault.dart';
|
import 'package:paperless_mobile/core/store/local_vault.dart';
|
||||||
|
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
|
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||||
|
|||||||
@@ -60,6 +60,8 @@
|
|||||||
"@documentDeleteSuccessMessage": {},
|
"@documentDeleteSuccessMessage": {},
|
||||||
"documentDetailsPageAssignAsnButtonLabel": "Přiřadit",
|
"documentDetailsPageAssignAsnButtonLabel": "Přiřadit",
|
||||||
"@documentDetailsPageAssignAsnButtonLabel": {},
|
"@documentDetailsPageAssignAsnButtonLabel": {},
|
||||||
|
"documentDetailsPageLoadFullContentLabel": "",
|
||||||
|
"@documentDetailsPageLoadFullContentLabel": {},
|
||||||
"documentDetailsPageSimilarDocumentsLabel": "Podobné dokumenty",
|
"documentDetailsPageSimilarDocumentsLabel": "Podobné dokumenty",
|
||||||
"@documentDetailsPageSimilarDocumentsLabel": {},
|
"@documentDetailsPageSimilarDocumentsLabel": {},
|
||||||
"documentDetailsPageTabContentLabel": "Obsah",
|
"documentDetailsPageTabContentLabel": "Obsah",
|
||||||
@@ -154,6 +156,8 @@
|
|||||||
"@documentsPageEmptyStateNothingHereText": {},
|
"@documentsPageEmptyStateNothingHereText": {},
|
||||||
"documentsPageEmptyStateOopsText": "Ajaj.",
|
"documentsPageEmptyStateOopsText": "Ajaj.",
|
||||||
"@documentsPageEmptyStateOopsText": {},
|
"@documentsPageEmptyStateOopsText": {},
|
||||||
|
"documentsPageNewDocumentAvailableText": "",
|
||||||
|
"@documentsPageNewDocumentAvailableText": {},
|
||||||
"documentsPageOrderByLabel": "Řadit dle",
|
"documentsPageOrderByLabel": "Řadit dle",
|
||||||
"@documentsPageOrderByLabel": {},
|
"@documentsPageOrderByLabel": {},
|
||||||
"documentsPageSelectionBulkDeleteDialogContinueText": "Tuto akci nelze vrátit zpět. Opravdu chcete pokračovat?",
|
"documentsPageSelectionBulkDeleteDialogContinueText": "Tuto akci nelze vrátit zpět. Opravdu chcete pokračovat?",
|
||||||
@@ -336,6 +340,8 @@
|
|||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"genericAcknowledgeLabel": "",
|
||||||
|
"@genericAcknowledgeLabel": {},
|
||||||
"genericActionCancelLabel": "Zrušit",
|
"genericActionCancelLabel": "Zrušit",
|
||||||
"@genericActionCancelLabel": {},
|
"@genericActionCancelLabel": {},
|
||||||
"genericActionCreateLabel": "Vytvořit",
|
"genericActionCreateLabel": "Vytvořit",
|
||||||
@@ -442,6 +448,8 @@
|
|||||||
"@loginPagePasswordFieldLabel": {},
|
"@loginPagePasswordFieldLabel": {},
|
||||||
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
|
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
|
||||||
"@loginPagePasswordValidatorMessageText": {},
|
"@loginPagePasswordValidatorMessageText": {},
|
||||||
|
"loginPageReachabilityConnectionTimeoutText": "",
|
||||||
|
"@loginPageReachabilityConnectionTimeoutText": {},
|
||||||
"loginPageReachabilityInvalidClientCertificateConfigurationText": "",
|
"loginPageReachabilityInvalidClientCertificateConfigurationText": "",
|
||||||
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
||||||
"loginPageReachabilityMissingClientCertificateText": "",
|
"loginPageReachabilityMissingClientCertificateText": "",
|
||||||
@@ -456,6 +464,8 @@
|
|||||||
"@loginPageServerUrlFieldLabel": {},
|
"@loginPageServerUrlFieldLabel": {},
|
||||||
"loginPageServerUrlValidatorMessageInvalidAddressText": "",
|
"loginPageServerUrlValidatorMessageInvalidAddressText": "",
|
||||||
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
||||||
|
"loginPageServerUrlValidatorMessageMissingSchemeText": "",
|
||||||
|
"@loginPageServerUrlValidatorMessageMissingSchemeText": {},
|
||||||
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
|
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
|
||||||
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
||||||
"loginPageSignInButtonLabel": "",
|
"loginPageSignInButtonLabel": "",
|
||||||
|
|||||||
@@ -60,6 +60,8 @@
|
|||||||
"@documentDeleteSuccessMessage": {},
|
"@documentDeleteSuccessMessage": {},
|
||||||
"documentDetailsPageAssignAsnButtonLabel": "Zuweisen",
|
"documentDetailsPageAssignAsnButtonLabel": "Zuweisen",
|
||||||
"@documentDetailsPageAssignAsnButtonLabel": {},
|
"@documentDetailsPageAssignAsnButtonLabel": {},
|
||||||
|
"documentDetailsPageLoadFullContentLabel": "Lade gesamten Inhalt",
|
||||||
|
"@documentDetailsPageLoadFullContentLabel": {},
|
||||||
"documentDetailsPageSimilarDocumentsLabel": "Similar Documents",
|
"documentDetailsPageSimilarDocumentsLabel": "Similar Documents",
|
||||||
"@documentDetailsPageSimilarDocumentsLabel": {},
|
"@documentDetailsPageSimilarDocumentsLabel": {},
|
||||||
"documentDetailsPageTabContentLabel": "Inhalt",
|
"documentDetailsPageTabContentLabel": "Inhalt",
|
||||||
@@ -154,6 +156,8 @@
|
|||||||
"@documentsPageEmptyStateNothingHereText": {},
|
"@documentsPageEmptyStateNothingHereText": {},
|
||||||
"documentsPageEmptyStateOopsText": "Ups.",
|
"documentsPageEmptyStateOopsText": "Ups.",
|
||||||
"@documentsPageEmptyStateOopsText": {},
|
"@documentsPageEmptyStateOopsText": {},
|
||||||
|
"documentsPageNewDocumentAvailableText": "Neues Dokument verfügbar!",
|
||||||
|
"@documentsPageNewDocumentAvailableText": {},
|
||||||
"documentsPageOrderByLabel": "Sortiere nach",
|
"documentsPageOrderByLabel": "Sortiere nach",
|
||||||
"@documentsPageOrderByLabel": {},
|
"@documentsPageOrderByLabel": {},
|
||||||
"documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchtest Du trotzdem fortfahren?",
|
"documentsPageSelectionBulkDeleteDialogContinueText": "Diese Aktion ist unwiderruflich. Möchtest Du trotzdem fortfahren?",
|
||||||
@@ -336,6 +340,8 @@
|
|||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"genericAcknowledgeLabel": "Verstanden!",
|
||||||
|
"@genericAcknowledgeLabel": {},
|
||||||
"genericActionCancelLabel": "Abbrechen",
|
"genericActionCancelLabel": "Abbrechen",
|
||||||
"@genericActionCancelLabel": {},
|
"@genericActionCancelLabel": {},
|
||||||
"genericActionCreateLabel": "Erstellen",
|
"genericActionCreateLabel": "Erstellen",
|
||||||
@@ -372,7 +378,7 @@
|
|||||||
"@inboxPageNoNewDocumentsText": {},
|
"@inboxPageNoNewDocumentsText": {},
|
||||||
"inboxPageTodayText": "Heute",
|
"inboxPageTodayText": "Heute",
|
||||||
"@inboxPageTodayText": {},
|
"@inboxPageTodayText": {},
|
||||||
"inboxPageUndoRemoveText": "UNDO",
|
"inboxPageUndoRemoveText": "Undo",
|
||||||
"@inboxPageUndoRemoveText": {},
|
"@inboxPageUndoRemoveText": {},
|
||||||
"inboxPageUnseenText": "ungesehen",
|
"inboxPageUnseenText": "ungesehen",
|
||||||
"@inboxPageUnseenText": {},
|
"@inboxPageUnseenText": {},
|
||||||
@@ -442,6 +448,8 @@
|
|||||||
"@loginPagePasswordFieldLabel": {},
|
"@loginPagePasswordFieldLabel": {},
|
||||||
"loginPagePasswordValidatorMessageText": "Passwort darf nicht leer sein.",
|
"loginPagePasswordValidatorMessageText": "Passwort darf nicht leer sein.",
|
||||||
"@loginPagePasswordValidatorMessageText": {},
|
"@loginPagePasswordValidatorMessageText": {},
|
||||||
|
"loginPageReachabilityConnectionTimeoutText": "Zeitüberschreitung der Verbindung.",
|
||||||
|
"@loginPageReachabilityConnectionTimeoutText": {},
|
||||||
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Inkorrekte oder fehlende Zertifikatspassphrase.",
|
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Inkorrekte oder fehlende Zertifikatspassphrase.",
|
||||||
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
||||||
"loginPageReachabilityMissingClientCertificateText": "Ein Client-Zertifikat wurde erwartet aber nicht gesendet. Bitte stelle ein Zertifikat zur Verfügung.",
|
"loginPageReachabilityMissingClientCertificateText": "Ein Client-Zertifikat wurde erwartet aber nicht gesendet. Bitte stelle ein Zertifikat zur Verfügung.",
|
||||||
@@ -450,12 +458,14 @@
|
|||||||
"@loginPageReachabilityNotReachableText": {},
|
"@loginPageReachabilityNotReachableText": {},
|
||||||
"loginPageReachabilitySuccessText": "Verbindung erfolgreich hergestellt.",
|
"loginPageReachabilitySuccessText": "Verbindung erfolgreich hergestellt.",
|
||||||
"@loginPageReachabilitySuccessText": {},
|
"@loginPageReachabilitySuccessText": {},
|
||||||
"loginPageReachabilityUnresolvedHostText": "Der Host konnte nicht aufgelöst werden. Bitte überprüfe die Server-Adresse.",
|
"loginPageReachabilityUnresolvedHostText": "Der Host konnte nicht aufgelöst werden. Bitte überprüfe die Server-Adresse und deine Internetverbindung.",
|
||||||
"@loginPageReachabilityUnresolvedHostText": {},
|
"@loginPageReachabilityUnresolvedHostText": {},
|
||||||
"loginPageServerUrlFieldLabel": "Server-Adresse",
|
"loginPageServerUrlFieldLabel": "Server-Adresse",
|
||||||
"@loginPageServerUrlFieldLabel": {},
|
"@loginPageServerUrlFieldLabel": {},
|
||||||
"loginPageServerUrlValidatorMessageInvalidAddressText": "Ungültige Adresse.",
|
"loginPageServerUrlValidatorMessageInvalidAddressText": "Ungültige Adresse.",
|
||||||
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
||||||
|
"loginPageServerUrlValidatorMessageMissingSchemeText": "Server-Adresse muss ein Schema enthalten.",
|
||||||
|
"@loginPageServerUrlValidatorMessageMissingSchemeText": {},
|
||||||
"loginPageServerUrlValidatorMessageRequiredText": "Server-Addresse darf nicht leer sein.",
|
"loginPageServerUrlValidatorMessageRequiredText": "Server-Addresse darf nicht leer sein.",
|
||||||
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
||||||
"loginPageSignInButtonLabel": "Anmelden",
|
"loginPageSignInButtonLabel": "Anmelden",
|
||||||
|
|||||||
@@ -60,6 +60,8 @@
|
|||||||
"@documentDeleteSuccessMessage": {},
|
"@documentDeleteSuccessMessage": {},
|
||||||
"documentDetailsPageAssignAsnButtonLabel": "Assign",
|
"documentDetailsPageAssignAsnButtonLabel": "Assign",
|
||||||
"@documentDetailsPageAssignAsnButtonLabel": {},
|
"@documentDetailsPageAssignAsnButtonLabel": {},
|
||||||
|
"documentDetailsPageLoadFullContentLabel": "Load full content",
|
||||||
|
"@documentDetailsPageLoadFullContentLabel": {},
|
||||||
"documentDetailsPageSimilarDocumentsLabel": "Similar Documents",
|
"documentDetailsPageSimilarDocumentsLabel": "Similar Documents",
|
||||||
"@documentDetailsPageSimilarDocumentsLabel": {},
|
"@documentDetailsPageSimilarDocumentsLabel": {},
|
||||||
"documentDetailsPageTabContentLabel": "Content",
|
"documentDetailsPageTabContentLabel": "Content",
|
||||||
@@ -154,6 +156,8 @@
|
|||||||
"@documentsPageEmptyStateNothingHereText": {},
|
"@documentsPageEmptyStateNothingHereText": {},
|
||||||
"documentsPageEmptyStateOopsText": "Oops.",
|
"documentsPageEmptyStateOopsText": "Oops.",
|
||||||
"@documentsPageEmptyStateOopsText": {},
|
"@documentsPageEmptyStateOopsText": {},
|
||||||
|
"documentsPageNewDocumentAvailableText": "New document available!",
|
||||||
|
"@documentsPageNewDocumentAvailableText": {},
|
||||||
"documentsPageOrderByLabel": "Order By",
|
"documentsPageOrderByLabel": "Order By",
|
||||||
"@documentsPageOrderByLabel": {},
|
"@documentsPageOrderByLabel": {},
|
||||||
"documentsPageSelectionBulkDeleteDialogContinueText": "This action is irreversible. Do you wish to proceed anyway?",
|
"documentsPageSelectionBulkDeleteDialogContinueText": "This action is irreversible. Do you wish to proceed anyway?",
|
||||||
@@ -336,6 +340,8 @@
|
|||||||
"count": {}
|
"count": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"genericAcknowledgeLabel": "Got it!",
|
||||||
|
"@genericAcknowledgeLabel": {},
|
||||||
"genericActionCancelLabel": "Cancel",
|
"genericActionCancelLabel": "Cancel",
|
||||||
"@genericActionCancelLabel": {},
|
"@genericActionCancelLabel": {},
|
||||||
"genericActionCreateLabel": "Create",
|
"genericActionCreateLabel": "Create",
|
||||||
@@ -372,7 +378,7 @@
|
|||||||
"@inboxPageNoNewDocumentsText": {},
|
"@inboxPageNoNewDocumentsText": {},
|
||||||
"inboxPageTodayText": "Today",
|
"inboxPageTodayText": "Today",
|
||||||
"@inboxPageTodayText": {},
|
"@inboxPageTodayText": {},
|
||||||
"inboxPageUndoRemoveText": "UNDO",
|
"inboxPageUndoRemoveText": "Undo",
|
||||||
"@inboxPageUndoRemoveText": {},
|
"@inboxPageUndoRemoveText": {},
|
||||||
"inboxPageUnseenText": "unseen",
|
"inboxPageUnseenText": "unseen",
|
||||||
"@inboxPageUnseenText": {},
|
"@inboxPageUnseenText": {},
|
||||||
@@ -442,6 +448,8 @@
|
|||||||
"@loginPagePasswordFieldLabel": {},
|
"@loginPagePasswordFieldLabel": {},
|
||||||
"loginPagePasswordValidatorMessageText": "Password must not be empty.",
|
"loginPagePasswordValidatorMessageText": "Password must not be empty.",
|
||||||
"@loginPagePasswordValidatorMessageText": {},
|
"@loginPagePasswordValidatorMessageText": {},
|
||||||
|
"loginPageReachabilityConnectionTimeoutText": "Connection timed out.",
|
||||||
|
"@loginPageReachabilityConnectionTimeoutText": {},
|
||||||
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
|
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
|
||||||
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
|
||||||
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
|
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
|
||||||
@@ -450,12 +458,14 @@
|
|||||||
"@loginPageReachabilityNotReachableText": {},
|
"@loginPageReachabilityNotReachableText": {},
|
||||||
"loginPageReachabilitySuccessText": "Connection successfully established.",
|
"loginPageReachabilitySuccessText": "Connection successfully established.",
|
||||||
"@loginPageReachabilitySuccessText": {},
|
"@loginPageReachabilitySuccessText": {},
|
||||||
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.",
|
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address and your internet connection. ",
|
||||||
"@loginPageReachabilityUnresolvedHostText": {},
|
"@loginPageReachabilityUnresolvedHostText": {},
|
||||||
"loginPageServerUrlFieldLabel": "Server Address",
|
"loginPageServerUrlFieldLabel": "Server Address",
|
||||||
"@loginPageServerUrlFieldLabel": {},
|
"@loginPageServerUrlFieldLabel": {},
|
||||||
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
|
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
|
||||||
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
|
||||||
|
"loginPageServerUrlValidatorMessageMissingSchemeText": "Server address must include a scheme.",
|
||||||
|
"@loginPageServerUrlValidatorMessageMissingSchemeText": {},
|
||||||
"loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.",
|
"loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.",
|
||||||
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
"@loginPageServerUrlValidatorMessageRequiredText": {},
|
||||||
"loginPageSignInButtonLabel": "Sign In",
|
"loginPageSignInButtonLabel": "Sign In",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:paperless_api/paperless_api.dart';
|
|||||||
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
|
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
import 'package:paperless_mobile/core/bloc/paperless_server_information_cubit.dart';
|
||||||
|
import 'package:paperless_mobile/core/interceptor/dio_http_error_interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||||
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
|
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
|
||||||
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
|
import 'package:paperless_mobile/core/repository/impl/document_type_repository_impl.dart';
|
||||||
@@ -76,7 +77,10 @@ void main() async {
|
|||||||
appSettingsCubit.state.preferredLocaleSubtag,
|
appSettingsCubit.state.preferredLocaleSubtag,
|
||||||
);
|
);
|
||||||
// Manages security context, required for self signed client certificates
|
// Manages security context, required for self signed client certificates
|
||||||
final sessionManager = SessionManager([languageHeaderInterceptor]);
|
final sessionManager = SessionManager([
|
||||||
|
DioHttpErrorInterceptor(),
|
||||||
|
languageHeaderInterceptor,
|
||||||
|
]);
|
||||||
|
|
||||||
// Initialize Paperless APIs
|
// Initialize Paperless APIs
|
||||||
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
|
final authApi = PaperlessAuthenticationApiImpl(sessionManager.client);
|
||||||
@@ -219,6 +223,9 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
chipTheme: ChipThemeData(
|
chipTheme: ChipThemeData(
|
||||||
backgroundColor: Colors.lightGreen[50],
|
backgroundColor: Colors.lightGreen[50],
|
||||||
),
|
),
|
||||||
|
listTileTheme: const ListTileThemeData(
|
||||||
|
tileColor: Colors.transparent,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final _darkTheme = ThemeData(
|
final _darkTheme = ThemeData(
|
||||||
@@ -241,6 +248,9 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
|
|||||||
chipTheme: ChipThemeData(
|
chipTheme: ChipThemeData(
|
||||||
backgroundColor: Colors.green[900],
|
backgroundColor: Colors.green[900],
|
||||||
),
|
),
|
||||||
|
listTileTheme: const ListTileThemeData(
|
||||||
|
tileColor: Colors.transparent,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -33,30 +33,32 @@ void showSnackBar(
|
|||||||
String message, {
|
String message, {
|
||||||
String? details,
|
String? details,
|
||||||
SnackBarActionConfig? action,
|
SnackBarActionConfig? action,
|
||||||
|
Duration duration = const Duration(seconds: 5),
|
||||||
}) {
|
}) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
..hideCurrentSnackBar()
|
..hideCurrentSnackBar()
|
||||||
..showSnackBar(
|
..showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: RichText(
|
content: (details != null)
|
||||||
maxLines: 5,
|
? RichText(
|
||||||
text: TextSpan(
|
maxLines: 5,
|
||||||
text: message,
|
text: TextSpan(
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
text: message,
|
||||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
|
),
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: "\n$details",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
children: <TextSpan>[
|
)
|
||||||
if (details != null)
|
: Text(message),
|
||||||
TextSpan(
|
|
||||||
text: "\n$details",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
action: action != null
|
action: action != null
|
||||||
? SnackBarAction(
|
? SnackBarAction(
|
||||||
label: action.label,
|
label: action.label,
|
||||||
@@ -64,7 +66,7 @@ void showSnackBar(
|
|||||||
textColor: Theme.of(context).colorScheme.onInverseSurface,
|
textColor: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
duration: const Duration(seconds: 5),
|
duration: duration,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.4.1+12
|
version: 1.5.0+13
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0-35.0.dev <4.0.0'
|
sdk: '>=3.0.0-35.0.dev <4.0.0'
|
||||||
|
|||||||
Reference in New Issue
Block a user