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

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

View File

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

View File

@@ -11,22 +11,23 @@ class DioHttpErrorInterceptor extends Interceptor {
// try to parse contained error message, otherwise return response // 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;
}

View File

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

View File

@@ -26,7 +26,6 @@ class SessionManager {
(client) => client..badCertificateCallback = (cert, host, port) => true; (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,

View File

@@ -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;
} }
} }

View File

@@ -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,
],
); );
} }
} }

View File

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

View File

@@ -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,

View File

@@ -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),
), ),

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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,
), ),

View File

@@ -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();

View File

@@ -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();
} }
} }

View File

@@ -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);
} }

View 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,
};

View File

@@ -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);
},
),
); );
} }

View File

@@ -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,

View File

@@ -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);
}
},
); );
} }
} }

View File

@@ -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(

View File

@@ -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,
), ),

View File

@@ -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) {}, );
); },
}, ),
), ),
),
], ],
); );
}, },

View File

@@ -5,4 +5,5 @@ enum ReachabilityStatus {
unknownHost, unknownHost,
missingClientCertificate, missingClientCertificate,
invalidClientCertificateConfiguration, invalidClientCertificateConfiguration,
connectionTimeout;
} }

View File

@@ -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;

View File

@@ -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,
);
} }
} }

View File

@@ -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",
// ),
] ]
: [], : [],
), ),

View File

@@ -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';

View File

@@ -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": "",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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,
), ),
); );
} }

View File

@@ -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'