Fixed bugs, added serialization for documents state

This commit is contained in:
Anton Stubenbord
2023-01-06 18:22:43 +01:00
parent 738ef99bc5
commit 23bcb355b1
76 changed files with 1333 additions and 595 deletions

View File

@@ -4,4 +4,8 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
</manifest>

View File

@@ -5,7 +5,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
@@ -39,5 +39,9 @@
</application>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
</manifest>

View File

@@ -47,7 +47,7 @@ class DioHttpErrorInterceptor extends Interceptor {
errorMessages.putIfAbsent(entry.key, () => entry.value.toString());
}
}
return handler.reject(
handler.reject(
DioError(
error: errorMessages,
requestOptions: err.requestOptions,

View File

@@ -1,2 +1,8 @@
typedef JSON = Map<String, dynamic>;
typedef PaperlessValidationErrors = Map<String, String>;
typedef PaperlessLocalizedErrorMessage = String;
extension ValidationErrorsUtils on PaperlessValidationErrors {
bool get hasFieldUnspecificError => containsKey("non_field_errors");
String? get fieldUnspecificError => this['non_field_errors'];
}

View File

@@ -22,6 +22,7 @@ class DocumentUploadPreparationPage extends StatefulWidget {
final Uint8List fileBytes;
final String? title;
final String? filename;
final String? fileExtension;
final void Function(DocumentModel)? onSuccessfullyConsumed;
const DocumentUploadPreparationPage({
@@ -30,6 +31,7 @@ class DocumentUploadPreparationPage extends StatefulWidget {
this.title,
this.filename,
this.onSuccessfullyConsumed,
this.fileExtension,
}) : super(key: key);
@override
@@ -119,7 +121,7 @@ class _DocumentUploadPreparationPageState
name: fkFileName,
decoration: InputDecoration(
labelText: S.of(context).documentUploadFileNameLabel,
suffixText: ".pdf",
suffixText: widget.fileExtension,
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _formKey.currentState?.fields[fkFileName]

View File

@@ -1,13 +1,13 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:json_annotation/json_annotation.dart';
import 'dart:developer';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
class DocumentsCubit extends Cubit<DocumentsState> {
class DocumentsCubit extends HydratedCubit<DocumentsState> {
final PaperlessDocumentsApi _api;
DocumentsCubit(this._api) : super(DocumentsState.initial);
DocumentsCubit(this._api) : super(const DocumentsState());
Future<void> bulkRemove(List<DocumentModel> documents) async {
await _api.bulkAction(
@@ -40,42 +40,85 @@ class DocumentsCubit extends Cubit<DocumentsState> {
}
Future<void> load() async {
final result = await _api.find(state.filter);
emit(DocumentsState(
isLoaded: true,
value: [...state.value, result],
filter: state.filter,
));
emit(state.copyWith(isLoading: true));
try {
final result = await _api.find(state.filter);
emit(state.copyWith(
isLoading: false,
hasLoaded: true,
value: [...state.value, result],
));
} catch (err) {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
Future<void> reload() async {
if (state.currentPageNumber >= 5) {
return _bulkReloadDocuments();
emit(state.copyWith(isLoading: true));
try {
if (state.currentPageNumber >= 5) {
return _bulkReloadDocuments();
}
var newPages = <PagedSearchResult<DocumentModel>>[];
for (final page in state.value) {
final result =
await _api.find(state.filter.copyWith(page: page.pageKey));
newPages.add(result);
}
emit(DocumentsState(
hasLoaded: true,
value: newPages,
filter: state.filter,
isLoading: false,
));
} catch (err) {
emit(state.copyWith(isLoading: false));
rethrow;
}
var newPages = <PagedSearchResult>[];
for (final page in state.value) {
final result = await _api.find(state.filter.copyWith(page: page.pageKey));
newPages.add(result);
}
emit(DocumentsState(isLoaded: true, value: newPages, filter: state.filter));
}
Future<void> _bulkReloadDocuments() async {
final result = await _api
.find(state.filter.copyWith(page: 1, pageSize: state.documents.length));
emit(DocumentsState(isLoaded: true, value: [result], filter: state.filter));
emit(state.copyWith(isLoading: true));
try {
final result = await _api.find(
state.filter.copyWith(
page: 1,
pageSize: state.documents.length,
),
);
emit(DocumentsState(
hasLoaded: true,
value: [result],
filter: state.filter,
isLoading: false,
));
} catch (err) {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
Future<void> loadMore() async {
if (state.isLastPageLoaded) {
return;
}
emit(state.copyWith(isLoading: true));
final newFilter = state.filter.copyWith(page: state.filter.page + 1);
final result = await _api.find(newFilter);
emit(
DocumentsState(
isLoaded: true, value: [...state.value, result], filter: newFilter),
);
try {
final result = await _api.find(newFilter);
emit(
DocumentsState(
hasLoaded: true,
value: [...state.value, result],
filter: newFilter,
isLoading: false,
),
);
} catch (err) {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
///
@@ -84,8 +127,21 @@ class DocumentsCubit extends Cubit<DocumentsState> {
Future<void> updateFilter({
final DocumentFilter filter = DocumentFilter.initial,
}) async {
final result = await _api.find(filter.copyWith(page: 1));
emit(DocumentsState(filter: filter, value: [result], isLoaded: true));
try {
emit(state.copyWith(isLoading: true));
final result = await _api.find(filter.copyWith(page: 1));
emit(
DocumentsState(
filter: filter,
value: [result],
hasLoaded: true,
isLoading: false,
),
);
} catch (err) {
emit(state.copyWith(isLoading: false));
rethrow;
}
}
Future<void> resetFilter() {
@@ -125,6 +181,17 @@ class DocumentsCubit extends Cubit<DocumentsState> {
}
void reset() {
emit(DocumentsState.initial);
emit(const DocumentsState());
}
@override
DocumentsState? fromJson(Map<String, dynamic> json) {
log(json['filter'].toString());
return DocumentsState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(DocumentsState state) {
return state.toJson();
}
}

View File

@@ -1,30 +1,27 @@
import 'dart:developer';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
@JsonSerializable()
class DocumentsState extends Equatable {
final bool isLoaded;
final bool isLoading;
final bool hasLoaded;
final DocumentFilter filter;
final List<PagedSearchResult> value;
final List<PagedSearchResult<DocumentModel>> value;
@JsonKey(ignore: true)
final List<DocumentModel> selection;
const DocumentsState({
required this.isLoaded,
required this.value,
required this.filter,
this.hasLoaded = false,
this.isLoading = false,
this.value = const [],
this.filter = const DocumentFilter(),
this.selection = const [],
});
static const DocumentsState initial = DocumentsState(
isLoaded: false,
value: [],
filter: DocumentFilter.initial,
selection: [],
);
int get currentPageNumber {
return filter.page;
}
@@ -41,7 +38,7 @@ class DocumentsState extends Equatable {
}
bool get isLastPageLoaded {
if (!isLoaded) {
if (!hasLoaded) {
return false;
}
if (value.isNotEmpty) {
@@ -51,7 +48,7 @@ class DocumentsState extends Equatable {
}
int inferPageCount({required int pageSize}) {
if (!isLoaded) {
if (!hasLoaded) {
return 100000;
}
if (value.isEmpty) {
@@ -67,13 +64,15 @@ class DocumentsState extends Equatable {
DocumentsState copyWith({
bool overwrite = false,
bool? isLoaded,
List<PagedSearchResult>? value,
bool? hasLoaded,
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
List<DocumentModel>? selection,
}) {
return DocumentsState(
isLoaded: isLoaded ?? this.isLoaded,
hasLoaded: hasLoaded ?? this.hasLoaded,
isLoading: isLoading ?? this.isLoading,
value: value ?? this.value,
filter: filter ?? this.filter,
selection: selection ?? this.selection,
@@ -81,5 +80,28 @@ class DocumentsState extends Equatable {
}
@override
List<Object?> get props => [isLoaded, filter, value, selection];
List<Object?> get props => [hasLoaded, filter, value, selection, isLoading];
Map<String, dynamic> toJson() {
final json = {
'hasLoaded': hasLoaded,
'isLoading': isLoading,
'filter': filter.toJson(),
'value':
value.map((e) => e.toJson(DocumentModelJsonConverter())).toList(),
};
return json;
}
factory DocumentsState.fromJson(Map<String, dynamic> json) {
return DocumentsState(
hasLoaded: json['hasLoaded'],
isLoading: json['isLoading'],
value: (json['value'] as List<dynamic>)
.map((e) =>
PagedSearchResult.fromJson(e, DocumentModelJsonConverter()))
.toList(),
filter: DocumentFilter.fromJson(json['filter']),
);
}
}

View File

@@ -35,6 +35,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
final _pagingController = PagingController<int, DocumentModel>(
firstPageKey: 1,
);
final _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@override
void initState() {
@@ -78,8 +79,8 @@ class _DocumentsPageState extends State<DocumentsPage> {
builder: (context, state) {
final appliedFiltersCount = state.filter.appliedFiltersCount;
return Badge.count(
alignment: const AlignmentDirectional(44,
-4), //TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
//TODO: Wait for stable version of m3, then use AlignmentDirectional.topEnd
alignment: const AlignmentDirectional(44, -4),
isLabelVisible: appliedFiltersCount > 0,
count: state.filter.appliedFiltersCount,
backgroundColor: Colors.red,
@@ -177,7 +178,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
break;
}
if (state.isLoaded && state.documents.isEmpty) {
if (state.hasLoaded && state.documents.isEmpty) {
child = SliverToBoxAdapter(
child: DocumentsEmptyState(
state: state,
@@ -190,6 +191,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
}
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _onRefresh,
notificationPredicate: (_) => isConnected,
child: CustomScrollView(
@@ -369,10 +371,10 @@ class _DocumentsPageState extends State<DocumentsPage> {
Future<void> _onRefresh() async {
try {
context.read<DocumentsCubit>().updateCurrentFilter(
await context.read<DocumentsCubit>().updateCurrentFilter(
(filter) => filter.copyWith(page: 1),
);
context.read<SavedViewCubit>().reload();
await context.read<SavedViewCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}

View File

@@ -28,10 +28,13 @@ class TextQueryFormField extends StatelessWidget {
prefixIcon: const Icon(Icons.search_outlined),
labelText: _buildLabelText(context, field.value!.queryType),
suffixIcon: PopupMenuButton<QueryType>(
enabled: !onlyExtendedQueryAllowed,
color: onlyExtendedQueryAllowed
? Theme.of(context).disabledColor
icon: onlyExtendedQueryAllowed
? Icon(
Icons.more_vert,
color: Theme.of(context).disabledColor,
)
: null,
enabled: !onlyExtendedQueryAllowed,
itemBuilder: (context) => [
PopupMenuItem(
child: ListTile(
@@ -59,7 +62,6 @@ class TextQueryFormField extends StatelessWidget {
onSelected: (selection) {
field.didChange(field.value?.copyWith(queryType: selection));
},
child: const Icon(Icons.more_vert),
),
),
onChanged: (value) {

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
@@ -36,8 +38,15 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
return BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, documentsState) {
final hasSelection = documentsState.selection.isNotEmpty;
// final PreferredSize? loadingWidget = documentsState.isLoading
// ? const PreferredSize(
// child: LinearProgressIndicator(),
// preferredSize: Size.fromHeight(4.0),
// )
// : null;
if (hasSelection) {
return SliverAppBar(
// bottom: loadingWidget,
expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true,
floating: true,
@@ -62,6 +71,7 @@ class _DocumentsPageAppBarState extends State<DocumentsPageAppBar> {
);
} else {
return SliverAppBar(
// bottom: loadingWidget,
expandedHeight: kToolbarHeight + flexibleAreaHeight,
snap: true,
floating: true,

View File

@@ -1,10 +1,19 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:paperless_api/paperless_api.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/global/constants.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/saved_view_repository.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/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/view/pages/documents_page.dart';
import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart';
@@ -13,8 +22,12 @@ import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@@ -31,6 +44,126 @@ class _HomePageState extends State<HomePage> {
void initState() {
super.initState();
_initializeData(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles();
});
}
void _listenForReceivedFiles() async {
if (ShareIntentQueue.instance.hasUnhandledFiles) {
Fluttertoast.showToast(msg: "Sync: Has unhandled files!");
await _handleReceivedFile(ShareIntentQueue.instance.pop()!);
Fluttertoast.showToast(msg: "Sync: File handled!");
}
ShareIntentQueue.instance.addListener(() async {
final queue = ShareIntentQueue.instance;
while (queue.hasUnhandledFiles) {
Fluttertoast.showToast(msg: "Async: Has unhandled files!");
final file = queue.pop()!;
await _handleReceivedFile(file);
Fluttertoast.showToast(msg: "Async: File handled!");
}
});
}
bool _isFileTypeSupported(SharedMediaFile file) {
return supportedFileExtensions.contains(
file.path.split('.').last.toLowerCase(),
);
}
Future<void> _handleReceivedFile(SharedMediaFile file) async {
final isGranted = await askForPermission(Permission.storage);
if (!isGranted) {
return;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Received File."),
content: Column(
children: [
Text("Path: ${file.path}"),
Text("Type: ${file.type.name}"),
Text("Exists: ${File(file.path).existsSync()}"),
FutureBuilder<bool>(
future: Permission.storage.isGranted,
builder: (context, snapshot) =>
Text("Has storage permission: ${snapshot.data}"),
)
],
),
));
SharedMediaFile mediaFile;
if (Platform.isIOS) {
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
mediaFile = SharedMediaFile(
file.path.replaceAll('file://', ''),
file.thumbnail,
file.duration,
file.type,
);
} else {
mediaFile = file;
}
if (!_isFileTypeSupported(mediaFile)) {
Fluttertoast.showToast(
msg: translateError(context, ErrorCode.unsupportedFileFormat),
);
if (Platform.isAndroid) {
// As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines.
await SystemNavigator.pop();
}
return;
}
final filename = extractFilenameFromPath(mediaFile.path);
try {
if (File(mediaFile.path).existsSync()) {
final bytes = File(mediaFile.path).readAsBytesSync();
final success = await Navigator.push<bool>(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: DocumentUploadCubit(
localVault: context.read(),
documentApi: context.read(),
tagRepository: context.read(),
correspondentRepository: context.read(),
documentTypeRepository: context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
filename: filename,
),
),
),
) ??
false;
if (success) {
await Fluttertoast.showToast(
msg: S.of(context).documentUploadSuccessText,
);
SystemNavigator.pop();
}
}
} catch (e, stackTrace) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Column(
children: [
Text(
e.toString(),
),
Text(stackTrace.toString()),
],
),
),
);
}
}
@override

View File

@@ -22,8 +22,7 @@ class InboxPage extends StatefulWidget {
}
class _InboxPageState extends State<InboxPage> {
final GlobalKey<RefreshIndicatorState> _emptyStateRefreshIndicatorKey =
GlobalKey();
final _emptyStateRefreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@override
void initState() {

View File

@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/security/authentication_aware_dio_manager.dart';
@@ -34,7 +35,6 @@ class AuthenticationCubit extends Cubit<AuthenticationState>
username: credentials.username!,
password: credentials.password!,
);
_dioWrapper.updateSettings(
baseUrl: serverUrl,
clientCertificate: clientCertificate,

View File

@@ -2,16 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/server_connection_page.dart';
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'widgets/never_scrollable_scroll_behavior.dart';
import 'widgets/server_login_page.dart';
import 'widgets/login_pages/server_login_page.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@@ -29,14 +30,6 @@ class _LoginPageState extends State<LoginPage> {
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false, // appBar: AppBar(
// title: Text(S.of(context).loginPageTitle),
// bottom: _isLoginLoading
// ? const PreferredSize(
// preferredSize: Size(double.infinity, 4),
// child: LinearProgressIndicator(),
// )
// : null,
// ),
body: FormBuilder(
key: _formKey,
child: PageView(
@@ -47,8 +40,9 @@ class _LoginPageState extends State<LoginPage> {
formBuilderKey: _formKey,
onContinue: () {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut);
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
ServerLoginPage(
@@ -58,58 +52,10 @@ class _LoginPageState extends State<LoginPage> {
],
),
),
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: FormBuilder(
// key: _formKey,
// child: ListView(
// children: [
// const ServerAddressFormField().padded(),
// const UserCredentialsFormField(),
// Align(
// alignment: Alignment.centerLeft,
// child: Padding(
// padding: const EdgeInsets.only(top: 16.0),
// child: Text(
// S.of(context).loginPageAdvancedLabel,
// style: Theme.of(context).textTheme.bodyLarge,
// ).padded(),
// ),
// ),
// const ClientCertificateFormField(),
// LayoutBuilder(builder: (context, constraints) {
// return Padding(
// padding: const EdgeInsets.all(8.0),
// child: SizedBox(
// width: constraints.maxWidth,
// child: _buildLoginButton(),
// ),
// );
// }),
// ],
// ),
// ),
// ),
);
}
Widget _buildLoginButton() {
return ElevatedButton(
key: const ValueKey('login-login-button'),
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(
Theme.of(context).colorScheme.primaryContainer,
),
elevation: const MaterialStatePropertyAll(0),
),
onPressed: _login,
child: Text(
S.of(context).loginPageLoginButtonLabel,
),
);
}
void _login() async {
Future<void> _login() async {
FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value;
@@ -122,11 +68,15 @@ class _LoginPageState extends State<LoginPage> {
);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on Map<String, dynamic> catch (error, stackTrace) {
showGenericError(context, error.values.first, stackTrace);
} on PaperlessValidationErrors catch (error, stackTrace) {
if (error.hasFieldUnspecificError) {
showLocalizedError(context, error.fieldUnspecificError!);
} else {
showGenericError(context, error.values.first, stackTrace);
}
} catch (unknownError, stackTrace) {
showGenericError(context, unknownError.toString(), stackTrace);
} finally {}
}
}
}
}

View File

@@ -5,8 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/view/widgets/password_text_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:permission_handler/permission_handler.dart';
import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget {
static const fkClientCertificate = 'clientCertificate';
@@ -24,7 +27,6 @@ class ClientCertificateFormField extends StatefulWidget {
class _ClientCertificateFormFieldState
extends State<ClientCertificateFormField> {
RestorableString? _selectedFilePath;
File? _selectedFile;
@override
Widget build(BuildContext context) {
@@ -105,7 +107,9 @@ class _ClientCertificateFormFieldState
}
Future<void> _onSelectFile(FormFieldState<ClientCertificate?> field) async {
FilePickerResult? result = await FilePicker.platform.pickFiles();
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: false,
);
if (result != null && result.files.single.path != null) {
File file = File(result.files.single.path!);
setState(() {

View File

@@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';
class ServerAddressFormField extends StatefulWidget {
static const String fkServerAddress = "serverAddress";

View File

@@ -3,7 +3,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/user_credentials.model.dart';
import 'package:paperless_mobile/features/login/view/widgets/password_text_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/obscured_input_text_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class UserCredentialsFormField extends StatefulWidget {

View File

@@ -4,8 +4,8 @@ import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/view/widgets/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/client_certificate_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';
@@ -28,7 +28,6 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
@override
Widget build(BuildContext context) {
final logoHeight = MediaQuery.of(context).size.width / 2;
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).loginPageTitle),
@@ -52,7 +51,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
child: Text("Continue"),
child: Text(S.of(context).loginPageContinueLabel),
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
? widget.onContinue
: null,
@@ -84,31 +83,33 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
case ReachabilityStatus.reachable:
return _buildIconText(
Icons.done,
"Connection established.",
S.of(context).loginPageReachabilitySuccessText,
Colors.green,
);
case ReachabilityStatus.notReachable:
return _buildIconText(
Icons.close,
"Could not establish a connection to the server.",
S.of(context).loginPageReachabilityNotReachableText,
errorColor,
);
case ReachabilityStatus.unknownHost:
return _buildIconText(
Icons.close,
"Host could not be resolved.",
S.of(context).loginPageReachabilityUnresolvedHostText,
errorColor,
);
case ReachabilityStatus.missingClientCertificate:
return _buildIconText(
Icons.close,
"A client certificate was expected but not sent. Please provide a certificate.",
S.of(context).loginPageReachabilityMissingClientCertificateText,
errorColor,
);
case ReachabilityStatus.invalidClientCertificateConfiguration:
return _buildIconText(
Icons.close,
"Incorrect or missing client certificate passphrase.",
S
.of(context)
.loginPageReachabilityInvalidClientCertificateConfigurationText,
errorColor,
);
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_credentials_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class ServerLoginPage extends StatefulWidget {
final Future<void> Function() onDone;
final GlobalKey<FormBuilderState> formBuilderKey;
const ServerLoginPage({
super.key,
required this.onDone,
required this.formBuilderKey,
});
@override
State<ServerLoginPage> createState() => _ServerLoginPageState();
}
class _ServerLoginPageState extends State<ServerLoginPage> {
bool _isLoginLoading = false;
@override
Widget build(BuildContext context) {
final serverAddress = (widget.formBuilderKey.currentState
?.getRawValue(ServerAddressFormField.fkServerAddress)
as String?)
?.replaceAll(RegExp(r'https?://'), '') ??
'';
return Scaffold(
appBar: AppBar(
title: Text(S.of(context).loginPageSignInTitle),
bottom: _isLoginLoading
? const PreferredSize(
preferredSize: Size.fromHeight(4.0),
child: LinearProgressIndicator(),
)
: null,
),
body: ListView(
children: [
Text(S.of(context).loginPageSignInToPrefixText(serverAddress))
.padded(),
const UserCredentialsFormField(),
],
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: () async {
setState(() => _isLoginLoading = true);
await widget.onDone();
setState(() => _isLoginLoading = false);
},
child: Text(S.of(context).loginPageSignInButtonLabel),
)
],
),
),
);
}
}

View File

@@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/view/widgets/server_address_form_field.dart';
import 'package:paperless_mobile/features/login/view/widgets/user_credentials_form_field.dart';
class ServerLoginPage extends StatefulWidget {
final VoidCallback onDone;
final GlobalKey<FormBuilderState> formBuilderKey;
const ServerLoginPage({
super.key,
required this.onDone,
required this.formBuilderKey,
});
@override
State<ServerLoginPage> createState() => _ServerLoginPageState();
}
class _ServerLoginPageState extends State<ServerLoginPage> {
@override
Widget build(BuildContext context) {
final serverAddress = (widget.formBuilderKey.currentState
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
?.replaceAll(RegExp(r'https?://'), '');
return Scaffold(
appBar: AppBar(
title: Text("Sign In"),
),
body: ListView(
children: [
Text("Sign in to $serverAddress").padded(),
UserCredentialsFormField(),
],
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: widget.onDone,
child: Text("Sign In"),
)
],
),
),
);
}
}

View File

@@ -26,8 +26,8 @@ import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
class ScannerPage extends StatefulWidget {
const ScannerPage({Key? key}) : super(key: key);
@@ -70,8 +70,10 @@ class _ScannerPageState extends State<ScannerPage>
? () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes:
_buildDocumentFromImageFiles(state).save(),
documentBytes: _assembleFileBytes(
state,
forcePdf: true,
).then((file) => file.bytes),
),
),
)
@@ -106,7 +108,10 @@ class _ScannerPageState extends State<ScannerPage>
}
void _openDocumentScanner(BuildContext context) async {
await _requestCameraPermissions();
final isGranted = await askForPermission(Permission.camera);
if (!isGranted) {
return;
}
final file = await FileService.allocateTemporaryFile(
PaperlessDirectoryType.scans,
extension: 'jpeg',
@@ -130,10 +135,9 @@ class _ScannerPageState extends State<ScannerPage>
}
void _onPrepareDocumentUpload(BuildContext context) async {
final doc = _buildDocumentFromImageFiles(
final file = await _assembleFileBytes(
context.read<DocumentScannerCubit>().state,
);
final bytes = await doc.save();
final uploaded = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LabelRepositoriesProvider(
@@ -148,7 +152,8 @@ class _ScannerPageState extends State<ScannerPage>
tagRepository: context.read<LabelRepository<Tag>>(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
fileBytes: file.bytes,
fileExtension: file.extension,
),
),
),
@@ -229,24 +234,17 @@ class _ScannerPageState extends State<ScannerPage>
}
}
Future<void> _requestCameraPermissions() async {
final hasPermission = await Permission.camera.isGranted;
if (!hasPermission) {
await Permission.camera.request();
}
}
void _onUploadFromFilesystem() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: supportedFileExtensions,
withData: true,
allowMultiple: false,
);
if (result?.files.single.path != null) {
File file = File(result!.files.single.path!);
if (!supportedFileExtensions.contains(
file.path.split('.').last.toLowerCase(),
)) {
if (!supportedFileExtensions
.contains(file.path.split('.').last.toLowerCase())) {
showErrorMessage(
context,
const PaperlessServerException(ErrorCode.unsupportedFileFormat),
@@ -254,14 +252,7 @@ class _ScannerPageState extends State<ScannerPage>
return;
}
final filename = extractFilenameFromPath(file.path);
final mimeType = lookupMimeType(file.path) ?? '';
late Uint8List fileBytes;
if (mimeType.startsWith('image')) {
fileBytes = await _buildDocumentFromImageFiles([file]).save();
} else {
// pdf
fileBytes = file.readAsBytesSync();
}
final extension = p.extension(file.path);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LabelRepositoriesProvider(
@@ -276,8 +267,9 @@ class _ScannerPageState extends State<ScannerPage>
tagRepository: context.read<LabelRepository<Tag>>(),
),
child: DocumentUploadPreparationPage(
fileBytes: fileBytes,
fileBytes: file.readAsBytesSync(),
filename: filename,
fileExtension: extension,
),
),
),
@@ -286,18 +278,38 @@ class _ScannerPageState extends State<ScannerPage>
}
}
pw.Document _buildDocumentFromImageFiles(List<File> files) {
///
/// Returns the file bytes of either a single file or multiple images concatenated into a single pdf.
///
Future<AssembledFile> _assembleFileBytes(
final List<File> files, {
bool forcePdf = false,
}) async {
assert(files.isNotEmpty);
if (files.length == 1 && !forcePdf) {
final ext = p.extension(files.first.path);
return AssembledFile(ext, files.first.readAsBytesSync());
}
final doc = pw.Document();
for (final file in files) {
final img = pw.MemoryImage(file.readAsBytesSync());
doc.addPage(
pw.Page(
pageFormat:
PdfPageFormat(img.width!.toDouble(), img.height!.toDouble()),
pageFormat: PdfPageFormat(
img.width!.toDouble(),
img.height!.toDouble(),
),
build: (context) => pw.Image(img),
),
);
}
return doc;
return AssembledFile('.pdf', await doc.save());
}
}
class AssembledFile {
final String extension;
final Uint8List bytes;
AssembledFile(this.extension, this.bytes);
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/util.dart';
import 'package:permission_handler/permission_handler.dart';
typedef OnImageScannedCallback = void Function(File);
@@ -23,14 +24,13 @@ class _ScannerWidgetState extends State<ScannerWidget> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Scan document")),
body: FutureBuilder<PermissionStatus>(
future: Permission.camera.request(),
builder:
(BuildContext context, AsyncSnapshot<PermissionStatus> snapshot) {
body: FutureBuilder<bool>(
future: askForPermission(Permission.camera),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.data!.isGranted) {
if (snapshot.data!) {
return Container();
}
return const Center(

View File

@@ -0,0 +1,36 @@
import 'dart:collection';
import 'dart:developer';
import 'package:flutter/widgets.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:rxdart/rxdart.dart';
class ShareIntentQueue extends ChangeNotifier {
final Queue<SharedMediaFile> _queue = Queue();
ShareIntentQueue._();
static final instance = ShareIntentQueue._();
void add(SharedMediaFile file) {
_queue.add(file);
notifyListeners();
}
void addAll(Iterable<SharedMediaFile> files) {
_queue.addAll(files);
notifyListeners();
}
SharedMediaFile? pop() {
if (hasUnhandledFiles) {
return _queue.removeFirst();
// Don't notify listeners, only when new item is added.
} else {
return null;
}
}
bool get hasUnhandledFiles => _queue.isNotEmpty;
}

View File

@@ -428,6 +428,8 @@
"@loginPageClientCertificateSettingLabel": {},
"loginPageClientCertificateSettingSelectFileText": "Vybrat soubor...",
"@loginPageClientCertificateSettingSelectFileText": {},
"loginPageContinueLabel": "Continue",
"@loginPageContinueLabel": {},
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Chybná nebo chybějící heslová fráze certifikátu.",
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
"loginPageLoginButtonLabel": "Připojit",
@@ -436,12 +438,32 @@
"@loginPagePasswordFieldLabel": {},
"loginPagePasswordValidatorMessageText": "Heslo nesmí být prázdné.",
"@loginPagePasswordValidatorMessageText": {},
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
"@loginPageReachabilityMissingClientCertificateText": {},
"loginPageReachabilityNotReachableText": "Could not establish a connection to the server.",
"@loginPageReachabilityNotReachableText": {},
"loginPageReachabilitySuccessText": "Connection successfully established.",
"@loginPageReachabilitySuccessText": {},
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.",
"@loginPageReachabilityUnresolvedHostText": {},
"loginPageServerUrlFieldLabel": "'Adresa serveru",
"@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Adresa serveru nesmí být prázdná.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageSignInButtonLabel": "Sign In",
"@loginPageSignInButtonLabel": {},
"loginPageSignInTitle": "Sign In",
"@loginPageSignInTitle": {},
"loginPageSignInToPrefixText": "Sign in to {serverAddress}",
"@loginPageSignInToPrefixText": {
"placeholders": {
"serverAddress": {}
}
},
"loginPageTitle": "Propojit s Paperless",
"@loginPageTitle": {},
"loginPageUsernameLabel": "Jméno uživatele",

View File

@@ -428,6 +428,8 @@
"@loginPageClientCertificateSettingLabel": {},
"loginPageClientCertificateSettingSelectFileText": "Datei auswählen...",
"@loginPageClientCertificateSettingSelectFileText": {},
"loginPageContinueLabel": "Fortfahren",
"@loginPageContinueLabel": {},
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Falsche oder fehlende Zertifikatspassphrase.",
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
"loginPageLoginButtonLabel": "Verbinden",
@@ -436,12 +438,32 @@
"@loginPagePasswordFieldLabel": {},
"loginPagePasswordValidatorMessageText": "Passwort darf nicht leer sein.",
"@loginPagePasswordValidatorMessageText": {},
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Inkorrekte oder fehlende Zertifikatspassphrase.",
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
"loginPageReachabilityMissingClientCertificateText": "Ein Client-Zertifikat wurde erwartet aber nicht gesendet. Bitte stelle ein Zertifikat zur Verfügung.",
"@loginPageReachabilityMissingClientCertificateText": {},
"loginPageReachabilityNotReachableText": "Es konnte keine Verbindung zum Server hergestellt werden.",
"@loginPageReachabilityNotReachableText": {},
"loginPageReachabilitySuccessText": "Verbindung erfolgreich hergestellt.",
"@loginPageReachabilitySuccessText": {},
"loginPageReachabilityUnresolvedHostText": "Der Host konnte nicht aufgelöst werden. Bitte überprüfe die Server-Adresse.",
"@loginPageReachabilityUnresolvedHostText": {},
"loginPageServerUrlFieldLabel": "Server-Adresse",
"@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageInvalidAddressText": "Ungültige Adresse.",
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Server-Addresse darf nicht leer sein.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageSignInButtonLabel": "Anmelden",
"@loginPageSignInButtonLabel": {},
"loginPageSignInTitle": "Anmelden",
"@loginPageSignInTitle": {},
"loginPageSignInToPrefixText": "Bei {serverAddress} anmelden",
"@loginPageSignInToPrefixText": {
"placeholders": {
"serverAddress": {}
}
},
"loginPageTitle": "Mit Paperless verbinden",
"@loginPageTitle": {},
"loginPageUsernameLabel": "Nutzername",

View File

@@ -428,6 +428,8 @@
"@loginPageClientCertificateSettingLabel": {},
"loginPageClientCertificateSettingSelectFileText": "Select file...",
"@loginPageClientCertificateSettingSelectFileText": {},
"loginPageContinueLabel": "Continue",
"@loginPageContinueLabel": {},
"loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": "Incorrect or missing certificate passphrase.",
"@loginPageIncorrectOrMissingCertificatePassphraseErrorMessageText": {},
"loginPageLoginButtonLabel": "Connect",
@@ -436,12 +438,32 @@
"@loginPagePasswordFieldLabel": {},
"loginPagePasswordValidatorMessageText": "Password must not be empty.",
"@loginPagePasswordValidatorMessageText": {},
"loginPageReachabilityInvalidClientCertificateConfigurationText": "Incorrect or missing client certificate passphrase.",
"@loginPageReachabilityInvalidClientCertificateConfigurationText": {},
"loginPageReachabilityMissingClientCertificateText": "A client certificate was expected but not sent. Please provide a certificate.",
"@loginPageReachabilityMissingClientCertificateText": {},
"loginPageReachabilityNotReachableText": "Could not establish a connection to the server.",
"@loginPageReachabilityNotReachableText": {},
"loginPageReachabilitySuccessText": "Connection successfully established.",
"@loginPageReachabilitySuccessText": {},
"loginPageReachabilityUnresolvedHostText": "Host could not be resolved. Please check the server address.",
"@loginPageReachabilityUnresolvedHostText": {},
"loginPageServerUrlFieldLabel": "Server Address",
"@loginPageServerUrlFieldLabel": {},
"loginPageServerUrlValidatorMessageInvalidAddressText": "Invalid address.",
"@loginPageServerUrlValidatorMessageInvalidAddressText": {},
"loginPageServerUrlValidatorMessageRequiredText": "Server address must not be empty.",
"@loginPageServerUrlValidatorMessageRequiredText": {},
"loginPageSignInButtonLabel": "Sign In",
"@loginPageSignInButtonLabel": {},
"loginPageSignInTitle": "Sign In",
"@loginPageSignInTitle": {},
"loginPageSignInToPrefixText": "Sign in to {serverAddress}",
"@loginPageSignInToPrefixText": {
"placeholders": {
"serverAddress": {}
}
},
"loginPageTitle": "Connect to Paperless",
"@loginPageTitle": {},
"loginPageUsernameLabel": "Username",

View File

@@ -1,14 +1,10 @@
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart';
@@ -18,10 +14,8 @@ import 'package:paperless_api/paperless_api.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/paperless_server_information_cubit.dart';
import 'package:paperless_mobile/core/global/constants.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/logic/error_code_localization_mapper.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/saved_view_repository_impl.dart';
@@ -35,8 +29,6 @@ import 'package:paperless_mobile/core/service/dio_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/features/app_intro/application_intro_slideshow.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/home/view/home_page.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
@@ -45,8 +37,8 @@ import 'package:paperless_mobile/features/login/services/authentication_service.
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:provider/provider.dart';
@@ -136,7 +128,6 @@ void main() async {
//Update language header in interceptor on language change.
appSettingsCubit.stream.listen((event) => languageHeaderInterceptor
.preferredLocaleSubtag = event.preferredLocaleSubtag);
runApp(
MultiProvider(
providers: [
@@ -292,67 +283,6 @@ class AuthenticationWrapper extends StatefulWidget {
}
class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
bool isFileTypeSupported(SharedMediaFile file) {
return supportedFileExtensions.contains(
file.path.split('.').last.toLowerCase(),
);
}
void handleReceivedFiles(List<SharedMediaFile> files) async {
if (files.isEmpty) {
return;
}
late final SharedMediaFile file;
if (Platform.isIOS) {
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
file = SharedMediaFile(
files.first.path.replaceAll('file://', ''),
files.first.thumbnail,
files.first.duration,
files.first.type,
);
} else {
file = files.first;
}
if (!isFileTypeSupported(file)) {
Fluttertoast.showToast(
msg: translateError(context, ErrorCode.unsupportedFileFormat),
);
if (Platform.isAndroid) {
// As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines.
await SystemNavigator.pop();
}
return;
}
final filename = extractFilenameFromPath(file.path);
final bytes = File(file.path).readAsBytesSync();
final success = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: DocumentUploadCubit(
localVault: context.read(),
documentApi: context.read(),
tagRepository: context.read(),
correspondentRepository: context.read(),
documentTypeRepository: context.read(),
),
child: DocumentUploadPreparationPage(
fileBytes: bytes,
filename: filename,
),
),
),
);
if (success) {
Fluttertoast.showToast(
msg: S.of(context).documentUploadSuccessText,
);
SystemNavigator.pop();
}
}
@override
void didChangeDependencies() {
FlutterNativeSplash.remove();
@@ -364,18 +294,17 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
super.initState();
initializeDateFormatting();
// For sharing files coming from outside the app while the app is still opened
ReceiveSharingIntent.getMediaStream().listen(handleReceivedFiles);
ReceiveSharingIntent.getMediaStream()
.listen(ShareIntentQueue.instance.addAll);
// For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then(handleReceivedFiles);
ReceiveSharingIntent.getInitialMedia()
.then(ShareIntentQueue.instance.addAll);
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: true,
left: false,
right: false,
bottom: false,
child: BlocConsumer<AuthenticationCubit, AuthenticationState>(
listener: (context, authState) {
final bool showIntroSlider =

View File

@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:ui';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -11,6 +13,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/logic/error_code_localization_mapper.dart';
import 'package:paperless_mobile/core/service/github_issue_service.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:permission_handler/permission_handler.dart';
final dateFormat = DateFormat("yyyy-MM-dd");
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
@@ -90,6 +93,15 @@ void showGenericError(
);
}
void showLocalizedError(
BuildContext context,
String localizedMessage, [
StackTrace? stackTrace,
]) {
showSnackBar(context, localizedMessage);
log(localizedMessage, stackTrace: stackTrace);
}
void showErrorMessage(
BuildContext context,
PaperlessServerException error, [
@@ -156,3 +168,14 @@ Future<void> loadImage(ImageProvider provider) {
stream.addListener(listener);
return completer.future;
}
Future<bool> askForPermission(Permission permission) async {
final status = await permission.request();
log("Permission requested, new status is $status");
// If user has permanently declined permission, open settings.
if (status == PermissionStatus.permanentlyDenied) {
await openAppSettings();
}
return status == PermissionStatus.granted;
}

View File

@@ -2,3 +2,4 @@ library paperless_api;
export 'src/models/models.dart';
export 'src/modules/modules.dart';
export 'src/converters/converters.dart';

View File

@@ -0,0 +1,3 @@
export 'document_model_json_converter.dart';
export 'similar_document_model_json_converter.dart';
export 'date_range_query_json_converter.dart';

View File

@@ -0,0 +1,32 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/models.dart';
import 'package:paperless_api/src/models/query_parameters/date_range_queries/absolute_date_range_query.dart';
import 'package:paperless_api/src/models/query_parameters/date_range_queries/relative_date_range_query.dart';
class DateRangeQueryJsonConverter
extends JsonConverter<DateRangeQuery, Map<String, dynamic>> {
const DateRangeQueryJsonConverter();
@override
DateRangeQuery fromJson(Map<String, dynamic> json) {
final type = json['type'];
final data = json['data'];
switch (json['type'] as String) {
case 'UnsetDateRangeQuery':
return const UnsetDateRangeQuery();
case 'AbsoluteDateRangeQuery':
return AbsoluteDateRangeQuery.fromJson(data);
case 'RelativeDateRangeQuery':
return RelativeDateRangeQuery.fromJson(data);
default:
throw Exception('Error parsing DateRangeQuery: Unknown type $type');
}
}
@override
Map<String, dynamic> toJson(DateRangeQuery object) {
return {
'type': object.runtimeType.toString(),
'data': object.toJson(),
};
}
}

View File

@@ -0,0 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/document_model.dart';
class DocumentModelJsonConverter
extends JsonConverter<DocumentModel, Map<String, dynamic>> {
@override
DocumentModel fromJson(Map<String, dynamic> json) {
return DocumentModel.fromJson(json);
}
@override
Map<String, dynamic> toJson(DocumentModel object) {
return object.toJson();
}
}

View File

@@ -1,21 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/models.dart';
class IdQueryParameterJsonConverter
extends JsonConverter<IdQueryParameter, Map<String, dynamic>> {
const IdQueryParameterJsonConverter();
static const _idKey = "id";
static const _assignmentStatusKey = 'assignmentStatus';
@override
IdQueryParameter fromJson(Map<String, dynamic> json) {
return IdQueryParameter(json[_assignmentStatusKey], json[_idKey]);
}
@override
Map<String, dynamic> toJson(IdQueryParameter object) {
return {
_idKey: object.id,
_assignmentStatusKey: object.assignmentStatus,
};
}
}

View File

@@ -0,0 +1,15 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
class SimilarDocumentModelJsonConverter
extends JsonConverter<SimilarDocumentModel, Map<String, dynamic>> {
@override
SimilarDocumentModel fromJson(Map<String, dynamic> json) {
return SimilarDocumentModel.fromJson(json);
}
@override
Map<String, dynamic> toJson(SimilarDocumentModel object) {
return object.toJson();
}
}

View File

@@ -0,0 +1,34 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/models/query_parameters/tags_query/any_assigned_tags_query.dart';
import 'package:paperless_api/src/models/query_parameters/tags_query/ids_tags_query.dart';
import 'package:paperless_api/src/models/query_parameters/tags_query/only_not_assigned_tags_query.dart';
import '../models/query_parameters/tags_query/tags_query.dart';
class TagsQueryJsonConverter
extends JsonConverter<TagsQuery, Map<String, dynamic>> {
const TagsQueryJsonConverter();
@override
TagsQuery fromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
final data = json['data'] as Map<String, dynamic>;
switch (type) {
case 'OnlyNotAssignedTagsQuery':
return const OnlyNotAssignedTagsQuery();
case 'AnyAssignedTagsQuery':
return AnyAssignedTagsQuery.fromJson(data);
case 'IdsTagsQuery':
return IdsTagsQuery.fromJson(data);
default:
throw Exception('Error parsing TagsQuery: Unknown type $type');
}
}
@override
Map<String, dynamic> toJson(TagsQuery object) {
return {
'type': object.runtimeType.toString(),
'data': object.toJson(),
};
}
}

View File

@@ -1,10 +1,14 @@
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:collection/collection.dart';
import 'package:paperless_api/src/converters/tags_query_json_converter.dart';
@JsonSerializable()
part 'document_filter.g.dart';
@TagsQueryJsonConverter()
@DateRangeQueryJsonConverter()
@JsonSerializable(explicitToJson: true)
class DocumentFilter extends Equatable {
static const DocumentFilter initial = DocumentFilter();
@@ -73,7 +77,7 @@ class DocumentFilter extends Equatable {
key,
entries.length == 1
? entries.first.value
: entries.map((e) => e.value).toList(),
: entries.map((e) => e.value).join(","),
),
);
return queryParams;
@@ -150,4 +154,9 @@ class DocumentFilter extends Equatable {
modified,
query,
];
factory DocumentFilter.fromJson(Map<String, dynamic> json) =>
_$DocumentFilterFromJson(json);
Map<String, dynamic> toJson() => _$DocumentFilterToJson(this);
}

View File

@@ -0,0 +1,83 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'document_filter.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DocumentFilter _$DocumentFilterFromJson(Map<String, dynamic> json) =>
DocumentFilter(
documentType: json['documentType'] == null
? const IdQueryParameter.unset()
: IdQueryParameter.fromJson(
json['documentType'] as Map<String, dynamic>),
correspondent: json['correspondent'] == null
? const IdQueryParameter.unset()
: IdQueryParameter.fromJson(
json['correspondent'] as Map<String, dynamic>),
storagePath: json['storagePath'] == null
? const IdQueryParameter.unset()
: IdQueryParameter.fromJson(
json['storagePath'] as Map<String, dynamic>),
asnQuery: json['asnQuery'] == null
? const IdQueryParameter.unset()
: IdQueryParameter.fromJson(json['asnQuery'] as Map<String, dynamic>),
tags: json['tags'] == null
? const IdsTagsQuery()
: const TagsQueryJsonConverter()
.fromJson(json['tags'] as Map<String, dynamic>),
sortField: $enumDecodeNullable(_$SortFieldEnumMap, json['sortField']) ??
SortField.created,
sortOrder: $enumDecodeNullable(_$SortOrderEnumMap, json['sortOrder']) ??
SortOrder.descending,
page: json['page'] as int? ?? 1,
pageSize: json['pageSize'] as int? ?? 25,
query: json['query'] == null
? const TextQuery()
: TextQuery.fromJson(json['query'] as Map<String, dynamic>),
added: json['added'] == null
? const UnsetDateRangeQuery()
: const DateRangeQueryJsonConverter()
.fromJson(json['added'] as Map<String, dynamic>),
created: json['created'] == null
? const UnsetDateRangeQuery()
: const DateRangeQueryJsonConverter()
.fromJson(json['created'] as Map<String, dynamic>),
modified: json['modified'] == null
? const UnsetDateRangeQuery()
: const DateRangeQueryJsonConverter()
.fromJson(json['modified'] as Map<String, dynamic>),
);
Map<String, dynamic> _$DocumentFilterToJson(DocumentFilter instance) =>
<String, dynamic>{
'pageSize': instance.pageSize,
'page': instance.page,
'documentType': instance.documentType.toJson(),
'correspondent': instance.correspondent.toJson(),
'storagePath': instance.storagePath.toJson(),
'asnQuery': instance.asnQuery.toJson(),
'tags': const TagsQueryJsonConverter().toJson(instance.tags),
'sortField': _$SortFieldEnumMap[instance.sortField]!,
'sortOrder': _$SortOrderEnumMap[instance.sortOrder]!,
'created': const DateRangeQueryJsonConverter().toJson(instance.created),
'added': const DateRangeQueryJsonConverter().toJson(instance.added),
'modified': const DateRangeQueryJsonConverter().toJson(instance.modified),
'query': instance.query.toJson(),
};
const _$SortFieldEnumMap = {
SortField.archiveSerialNumber: 'archiveSerialNumber',
SortField.correspondentName: 'correspondentName',
SortField.title: 'title',
SortField.documentType: 'documentType',
SortField.created: 'created',
SortField.added: 'added',
SortField.modified: 'modified',
};
const _$SortOrderEnumMap = {
SortOrder.ascending: 'ascending',
SortOrder.descending: 'descending',
};

View File

@@ -1,8 +1,17 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
import 'query_parameters/tags_query/any_assigned_tags_query.dart';
import 'query_parameters/tags_query/exclude_tag_id_query.dart';
import 'query_parameters/tags_query/ids_tags_query.dart';
import 'query_parameters/tags_query/include_tag_id_query.dart';
import 'query_parameters/tags_query/only_not_assigned_tags_query.dart';
part 'filter_rule_model.g.dart';
@JsonSerializable()
class FilterRule with EquatableMixin {
static const int titleRule = 0;
static const int asnRule = 2;
@@ -35,22 +44,12 @@ class FilterRule with EquatableMixin {
static const String _lastNDateRangeQueryRegex =
r"(?<field>created|added|modified):\[-?(?<n>\d+) (?<unit>day|week|month|year) to now\]";
@JsonKey(name: 'rule_type')
final int ruleType;
final String? value;
FilterRule(this.ruleType, this.value);
FilterRule.fromJson(Map<String, dynamic> json)
: ruleType = json['rule_type'],
value = json['value'];
Map<String, dynamic> toJson() {
return {
'rule_type': ruleType,
'value': value,
};
}
DocumentFilter applyToFilter(final DocumentFilter filter) {
//TODO: Check in profiling mode if this is inefficient enough to cause stutters...
switch (ruleType) {
@@ -368,4 +367,9 @@ class FilterRule with EquatableMixin {
@override
List<Object?> get props => [ruleType, value];
Map<String, dynamic> toJson() => _$FilterRuleToJson(this);
factory FilterRule.fromJson(Map<String, dynamic> json) =>
_$FilterRuleFromJson(json);
}

View File

@@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'filter_rule_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
FilterRule _$FilterRuleFromJson(Map<String, dynamic> json) => FilterRule(
json['rule_type'] as int,
json['value'] as String?,
);
Map<String, dynamic> _$FilterRuleToJson(FilterRule instance) =>
<String, dynamic>{
'rule_type': instance.ruleType,
'value': instance.value,
};

View File

@@ -8,8 +8,8 @@ export 'query_parameters/id_query_parameter.dart';
export 'query_parameters/query_type.dart';
export 'query_parameters/sort_field.dart';
export 'query_parameters/sort_order.dart';
export 'query_parameters/tags_query.dart';
export 'query_parameters/date_range_query.dart';
export 'query_parameters/tags_query/tags_queries.dart';
export 'query_parameters/date_range_queries/date_range_queries.dart';
export 'query_parameters/text_query.dart';
export 'bulk_edit_model.dart';
export 'document_filter.dart';

View File

@@ -4,15 +4,13 @@ import 'package:paperless_api/src/models/document_model.dart';
const pageRegex = r".*page=(\d+).*";
//Todo: make this an interface and delegate serialization to implementations
class PagedSearchResultJsonSerializer<T> {
final Map<String, dynamic> json;
final T Function(Map<String, dynamic>) fromJson;
JsonConverter<T, Map<String, dynamic>> converter;
PagedSearchResultJsonSerializer(this.json, this.fromJson);
PagedSearchResultJsonSerializer(this.json, this.converter);
}
@JsonSerializable()
class PagedSearchResult<T> extends Equatable {
/// Total number of available items
final int count;
@@ -54,18 +52,34 @@ class PagedSearchResult<T> extends Equatable {
required this.results,
});
factory PagedSearchResult.fromJson(
PagedSearchResultJsonSerializer<T> serializer) {
factory PagedSearchResult.fromJson(Map<String, dynamic> json,
JsonConverter<T, Map<String, dynamic>> converter) {
return PagedSearchResult(
count: serializer.json['count'],
next: serializer.json['next'],
previous: serializer.json['previous'],
results: List<Map<String, dynamic>>.from(serializer.json['results'])
.map<T>(serializer.fromJson)
count: json['count'],
next: json['next'],
previous: json['previous'],
results: List<Map<String, dynamic>>.from(json['results'])
.map<T>(converter.fromJson)
.toList(),
);
}
Map<String, dynamic> toJson(
JsonConverter<T, Map<String, dynamic>> converter) {
return {
'count': count,
'next': next,
'previous': previous,
'results': results.map((e) => converter.toJson(e)).toList()
};
}
factory PagedSearchResult.fromJsonSingleParam(
PagedSearchResultJsonSerializer<T> serializer,
) {
return PagedSearchResult.fromJson(serializer.json, serializer.converter);
}
PagedSearchResult copyWith({
int? count,
String? next,

View File

@@ -0,0 +1,51 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/constants.dart';
import 'date_range_query.dart';
import 'date_range_query_field.dart';
part 'absolute_date_range_query.g.dart';
@JsonSerializable()
class AbsoluteDateRangeQuery extends DateRangeQuery {
final DateTime? after;
final DateTime? before;
const AbsoluteDateRangeQuery({this.after, this.before});
@override
List<Object?> get props => [after, before];
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) {
final Map<String, String> params = {};
// Add/subtract one day in the following because paperless uses gt/lt not gte/lte
if (after != null) {
params.putIfAbsent('${field.name}__date__gt',
() => apiDateFormat.format(after!.subtract(const Duration(days: 1))));
}
if (before != null) {
params.putIfAbsent('${field.name}__date__lt',
() => apiDateFormat.format(before!.add(const Duration(days: 1))));
}
return params;
}
AbsoluteDateRangeQuery copyWith({
DateTime? before,
DateTime? after,
}) {
return AbsoluteDateRangeQuery(
before: before ?? this.before,
after: after ?? this.after,
);
}
factory AbsoluteDateRangeQuery.fromJson(json) =>
_$AbsoluteDateRangeQueryFromJson(json);
@override
Map<String, dynamic> toJson() => _$AbsoluteDateRangeQueryToJson(this);
}

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'absolute_date_range_query.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AbsoluteDateRangeQuery _$AbsoluteDateRangeQueryFromJson(
Map<String, dynamic> json) =>
AbsoluteDateRangeQuery(
after: json['after'] == null
? null
: DateTime.parse(json['after'] as String),
before: json['before'] == null
? null
: DateTime.parse(json['before'] as String),
);
Map<String, dynamic> _$AbsoluteDateRangeQueryToJson(
AbsoluteDateRangeQuery instance) =>
<String, dynamic>{
'after': instance.after?.toIso8601String(),
'before': instance.before?.toIso8601String(),
};

View File

@@ -0,0 +1,6 @@
export 'date_range_query.dart';
export 'unset_date_range_query.dart';
export 'absolute_date_range_query.dart';
export 'relative_date_range_query.dart';
export 'date_range_unit.dart';
export 'date_range_query_field.dart';

View File

@@ -0,0 +1,10 @@
import 'package:equatable/equatable.dart';
import 'date_range_query_field.dart';
abstract class DateRangeQuery extends Equatable {
const DateRangeQuery();
Map<String, String> toQueryParameter(DateRangeQueryField field);
Map<String, dynamic> toJson();
}

View File

@@ -0,0 +1,5 @@
enum DateRangeQueryField {
created,
added,
modified;
}

View File

@@ -0,0 +1,6 @@
enum DateRangeUnit {
day,
week,
month,
year;
}

View File

@@ -0,0 +1,43 @@
import 'package:json_annotation/json_annotation.dart';
import 'date_range_query.dart';
import 'date_range_query_field.dart';
import 'date_range_unit.dart';
part 'relative_date_range_query.g.dart';
@JsonSerializable()
class RelativeDateRangeQuery extends DateRangeQuery {
final int offset;
final DateRangeUnit unit;
const RelativeDateRangeQuery([
this.offset = 1,
this.unit = DateRangeUnit.day,
]);
@override
List<Object?> get props => [offset, unit];
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) {
return {
'query': '${field.name}:[-$offset ${unit.name} to now]',
};
}
RelativeDateRangeQuery copyWith({
int? offset,
DateRangeUnit? unit,
}) {
return RelativeDateRangeQuery(
offset ?? this.offset,
unit ?? this.unit,
);
}
@override
Map<String, dynamic> toJson() => _$RelativeDateRangeQueryToJson(this);
factory RelativeDateRangeQuery.fromJson(Map<String, dynamic> json) =>
_$RelativeDateRangeQueryFromJson(json);
}

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'relative_date_range_query.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
RelativeDateRangeQuery _$RelativeDateRangeQueryFromJson(
Map<String, dynamic> json) =>
RelativeDateRangeQuery(
json['offset'] as int? ?? 1,
$enumDecodeNullable(_$DateRangeUnitEnumMap, json['unit']) ??
DateRangeUnit.day,
);
Map<String, dynamic> _$RelativeDateRangeQueryToJson(
RelativeDateRangeQuery instance) =>
<String, dynamic>{
'offset': instance.offset,
'unit': _$DateRangeUnitEnumMap[instance.unit]!,
};
const _$DateRangeUnitEnumMap = {
DateRangeUnit.day: 'day',
DateRangeUnit.week: 'week',
DateRangeUnit.month: 'month',
DateRangeUnit.year: 'year',
};

View File

@@ -0,0 +1,17 @@
import 'package:paperless_api/src/models/query_parameters/date_range_queries/date_range_query_field.dart';
import 'date_range_query.dart';
class UnsetDateRangeQuery extends DateRangeQuery {
const UnsetDateRangeQuery();
@override
List<Object?> get props => [];
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) => const {};
@override
Map<String, dynamic> toJson() {
return {};
}
}

View File

@@ -1,96 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:paperless_api/src/constants.dart';
abstract class DateRangeQuery extends Equatable {
const DateRangeQuery();
Map<String, String> toQueryParameter(DateRangeQueryField field);
}
class UnsetDateRangeQuery extends DateRangeQuery {
const UnsetDateRangeQuery();
@override
List<Object?> get props => [];
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) => const {};
}
class AbsoluteDateRangeQuery extends DateRangeQuery {
final DateTime? after;
final DateTime? before;
const AbsoluteDateRangeQuery({this.after, this.before});
@override
List<Object?> get props => [after, before];
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) {
final Map<String, String> params = {};
// Add/subtract one day in the following because paperless uses gt/lt not gte/lte
if (after != null) {
params.putIfAbsent('${field.name}__date__gt',
() => apiDateFormat.format(after!.subtract(const Duration(days: 1))));
}
if (before != null) {
params.putIfAbsent('${field.name}__date__lt',
() => apiDateFormat.format(before!.add(const Duration(days: 1))));
}
return params;
}
AbsoluteDateRangeQuery copyWith({
DateTime? before,
DateTime? after,
}) {
return AbsoluteDateRangeQuery(
before: before ?? this.before,
after: after ?? this.after,
);
}
}
class RelativeDateRangeQuery extends DateRangeQuery {
final int offset;
final DateRangeUnit unit;
const RelativeDateRangeQuery([
this.offset = 1,
this.unit = DateRangeUnit.day,
]);
@override
List<Object?> get props => [offset, unit];
@override
Map<String, String> toQueryParameter(DateRangeQueryField field) {
return {
'query': '${field.name}:[-$offset ${unit.name} to now]',
};
}
RelativeDateRangeQuery copyWith({
int? offset,
DateRangeUnit? unit,
}) {
return RelativeDateRangeQuery(
offset ?? this.offset,
unit ?? this.unit,
);
}
}
enum DateRangeUnit {
day,
week,
month,
year;
}
enum DateRangeQueryField {
created,
added,
modified;
}

View File

@@ -1,49 +1,43 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/src/converters/id_query_parameter_json_converter.dart';
part 'id_query_parameter.g.dart';
@IdQueryParameterJsonConverter()
@JsonSerializable()
class IdQueryParameter extends Equatable {
final int? _assignmentStatus;
final int? _id;
final int? assignmentStatus;
final int? id;
@Deprecated("Use named constructors, this is only meant for code generation")
const IdQueryParameter(this._assignmentStatus, this._id);
const IdQueryParameter(this.assignmentStatus, this.id);
const IdQueryParameter.notAssigned()
: _assignmentStatus = 1,
_id = null;
: assignmentStatus = 1,
id = null;
const IdQueryParameter.anyAssigned()
: _assignmentStatus = 0,
_id = null;
: assignmentStatus = 0,
id = null;
const IdQueryParameter.fromId(int? id)
: _assignmentStatus = null,
_id = id;
: assignmentStatus = null,
id = id;
const IdQueryParameter.unset() : this.fromId(null);
bool get isUnset => _id == null && _assignmentStatus == null;
bool get isUnset => id == null && assignmentStatus == null;
bool get isSet => _id != null && _assignmentStatus == null;
bool get isSet => id != null && assignmentStatus == null;
bool get onlyNotAssigned => _assignmentStatus == 1;
bool get onlyNotAssigned => assignmentStatus == 1;
bool get onlyAssigned => _assignmentStatus == 0;
int? get id => _id;
@visibleForTesting
int? get assignmentStatus => _assignmentStatus;
bool get onlyAssigned => assignmentStatus == 0;
Map<String, String> toQueryParameter(String field) {
final Map<String, String> params = {};
if (onlyNotAssigned || onlyAssigned) {
params.putIfAbsent(
'${field}__isnull', () => _assignmentStatus!.toString());
'${field}__isnull', () => assignmentStatus!.toString());
}
if (isSet) {
params.putIfAbsent("${field}__id", () => id!.toString());
@@ -52,5 +46,10 @@ class IdQueryParameter extends Equatable {
}
@override
List<Object?> get props => [_assignmentStatus, _id];
List<Object?> get props => [assignmentStatus, id];
Map<String, dynamic> toJson() => _$IdQueryParameterToJson(this);
factory IdQueryParameter.fromJson(Map<String, dynamic> json) =>
_$IdQueryParameterFromJson(json);
}

View File

@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'id_query_parameter.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
IdQueryParameter _$IdQueryParameterFromJson(Map<String, dynamic> json) =>
IdQueryParameter(
json['assignmentStatus'] as int?,
json['id'] as int?,
);
Map<String, dynamic> _$IdQueryParameterToJson(IdQueryParameter instance) =>
<String, dynamic>{
'assignmentStatus': instance.assignmentStatus,
'id': instance.id,
};

View File

@@ -0,0 +1,11 @@
import 'package:json_annotation/json_annotation.dart';
@JsonEnum()
enum TagsQueryType {
notAssigned,
anyAssigned,
ids,
id,
include,
exclude;
}

View File

@@ -1,3 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
@JsonEnum()
enum SortField {
archiveSerialNumber("archive_serial_number"),
correspondentName("correspondent__name"),

View File

@@ -1,3 +1,6 @@
import 'package:json_annotation/json_annotation.dart';
@JsonEnum()
enum SortOrder {
ascending(""),
descending("-");

View File

@@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
import 'tags_query.dart';
part 'any_assigned_tags_query.g.dart';
@JsonSerializable(explicitToJson: true)
class AnyAssignedTagsQuery extends TagsQuery {
final Iterable<int> tagIds;
const AnyAssignedTagsQuery({
this.tagIds = const [],
});
@override
Map<String, String> toQueryParameter() {
if (tagIds.isEmpty) {
return {'is_tagged': '1'};
}
return {'tags__id__in': tagIds.join(',')};
}
@override
List<Object?> get props => [tagIds];
@override
Map<String, dynamic> toJson() => _$AnyAssignedTagsQueryToJson(this);
factory AnyAssignedTagsQuery.fromJson(Map<String, dynamic> json) =>
_$AnyAssignedTagsQueryFromJson(json);
}

View File

@@ -0,0 +1,20 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'any_assigned_tags_query.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AnyAssignedTagsQuery _$AnyAssignedTagsQueryFromJson(
Map<String, dynamic> json) =>
AnyAssignedTagsQuery(
tagIds:
(json['tagIds'] as List<dynamic>?)?.map((e) => e as int) ?? const [],
);
Map<String, dynamic> _$AnyAssignedTagsQueryToJson(
AnyAssignedTagsQuery instance) =>
<String, dynamic>{
'tagIds': instance.tagIds.toList(),
};

View File

@@ -0,0 +1,15 @@
import 'package:paperless_api/src/models/query_parameters/tags_query/tag_id_query.dart';
import 'include_tag_id_query.dart';
class ExcludeTagIdQuery extends TagIdQuery {
const ExcludeTagIdQuery(super.id);
@override
String get methodName => 'exclude';
@override
TagIdQuery toggle() {
return IncludeTagIdQuery(id);
}
}

View File

@@ -1,40 +1,13 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
abstract class TagsQuery extends Equatable {
const TagsQuery();
Map<String, String> toQueryParameter();
}
import 'exclude_tag_id_query.dart';
import 'include_tag_id_query.dart';
import 'tag_id_query.dart';
import 'tags_query.dart';
class OnlyNotAssignedTagsQuery extends TagsQuery {
const OnlyNotAssignedTagsQuery();
@override
Map<String, String> toQueryParameter() {
return {'is_tagged': '0'};
}
@override
List<Object?> get props => [];
}
class AnyAssignedTagsQuery extends TagsQuery {
final Iterable<int> tagIds;
const AnyAssignedTagsQuery({
this.tagIds = const [],
});
@override
Map<String, String> toQueryParameter() {
if (tagIds.isEmpty) {
return {'is_tagged': '1'};
}
return {'tags__id__in': tagIds.join(',')};
}
@override
List<Object?> get props => [tagIds];
}
part 'ids_tags_query.g.dart';
@JsonSerializable(explicitToJson: true)
class IdsTagsQuery extends TagsQuery {
final Iterable<TagIdQuery> _idQueries;
@@ -102,41 +75,10 @@ class IdsTagsQuery extends TagsQuery {
@override
List<Object?> get props => [_idQueries];
}
abstract class TagIdQuery extends Equatable {
final int id;
const TagIdQuery(this.id);
String get methodName;
@override
List<Object?> get props => [id, methodName];
TagIdQuery toggle();
}
class IncludeTagIdQuery extends TagIdQuery {
const IncludeTagIdQuery(super.id);
@override
String get methodName => 'include';
@override
TagIdQuery toggle() {
return ExcludeTagIdQuery(id);
}
}
class ExcludeTagIdQuery extends TagIdQuery {
const ExcludeTagIdQuery(super.id);
@override
String get methodName => 'exclude';
@override
TagIdQuery toggle() {
return IncludeTagIdQuery(id);
}
@override
Map<String, dynamic> toJson() => _$IdsTagsQueryToJson(this);
factory IdsTagsQuery.fromJson(Map<String, dynamic> json) =>
_$IdsTagsQueryFromJson(json);
}

View File

@@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'ids_tags_query.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
IdsTagsQuery _$IdsTagsQueryFromJson(Map<String, dynamic> json) =>
IdsTagsQuery();
Map<String, dynamic> _$IdsTagsQueryToJson(IdsTagsQuery instance) =>
<String, dynamic>{};

View File

@@ -0,0 +1,15 @@
import 'package:paperless_api/src/models/query_parameters/tags_query/tag_id_query.dart';
import 'exclude_tag_id_query.dart';
class IncludeTagIdQuery extends TagIdQuery {
const IncludeTagIdQuery(super.id);
@override
String get methodName => 'include';
@override
TagIdQuery toggle() {
return ExcludeTagIdQuery(id);
}
}

View File

@@ -0,0 +1,17 @@
import 'tags_query.dart';
class OnlyNotAssignedTagsQuery extends TagsQuery {
const OnlyNotAssignedTagsQuery();
@override
Map<String, String> toQueryParameter() {
return {'is_tagged': '0'};
}
@override
List<Object?> get props => [];
@override
Map<String, dynamic> toJson() {
return {};
}
}

View File

@@ -0,0 +1,14 @@
import 'package:equatable/equatable.dart';
abstract class TagIdQuery extends Equatable {
final int id;
const TagIdQuery(this.id);
String get methodName;
@override
List<Object?> get props => [id, methodName];
TagIdQuery toggle();
}

View File

@@ -0,0 +1,7 @@
export 'any_assigned_tags_query.dart';
export 'ids_tags_query.dart';
export 'tags_query.dart';
export 'exclude_tag_id_query.dart';
export 'include_tag_id_query.dart';
export 'only_not_assigned_tags_query.dart';
export 'tag_id_query.dart';

View File

@@ -0,0 +1,7 @@
import 'package:equatable/equatable.dart';
abstract class TagsQuery extends Equatable {
const TagsQuery();
Map<String, String> toQueryParameter();
Map<String, dynamic> toJson();
}

View File

@@ -1,5 +1,10 @@
import 'package:json_annotation/json_annotation.dart';
import 'query_type.dart';
part 'text_query.g.dart';
@JsonSerializable()
class TextQuery {
final QueryType queryType;
final String? queryText;
@@ -51,4 +56,9 @@ class TextQuery {
}
return null;
}
Map<String, dynamic> toJson() => _$TextQueryToJson(this);
factory TextQuery.fromJson(Map<String, dynamic> json) =>
_$TextQueryFromJson(json);
}

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'text_query.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TextQuery _$TextQueryFromJson(Map<String, dynamic> json) => TextQuery(
queryType: $enumDecodeNullable(_$QueryTypeEnumMap, json['queryType']) ??
QueryType.titleAndContent,
queryText: json['queryText'] as String?,
);
Map<String, dynamic> _$TextQueryToJson(TextQuery instance) => <String, dynamic>{
'queryType': _$QueryTypeEnumMap[instance.queryType]!,
'queryText': instance.queryText,
};
const _$QueryTypeEnumMap = {
QueryType.title: 'title',
QueryType.titleAndContent: 'titleAndContent',
QueryType.extended: 'extended',
QueryType.asn: 'asn',
};

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:paperless_api/src/models/paperless_server_exception.dart';
import 'package:paperless_api/src/modules/authentication_api/authentication_api.dart';
@@ -24,10 +22,10 @@ class PaperlessAuthenticationApiImpl implements PaperlessAuthenticationApi {
},
);
} on DioError catch (error) {
if (error.error is PaperlessServerException) {
if (error.error is PaperlessServerException ||
error.error is Map<String, String>) {
throw error.error;
} else {
log(error.message);
throw PaperlessServerException(
ErrorCode.authenticationFailed,
details: error.message,

View File

@@ -1,10 +1,11 @@
import 'dart:convert';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/constants.dart';
import 'package:paperless_api/src/converters/document_model_json_converter.dart';
import 'package:paperless_api/src/converters/similar_document_model_json_converter.dart';
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
final Dio client;
@@ -28,9 +29,8 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
'document',
MultipartFile.fromBytes(documentBytes, filename: filename),
),
);
formData.fields.add(MapEntry('title', title));
)
..fields.add(MapEntry('title', title));
if (createdAt != null) {
formData.fields.add(MapEntry('created', apiDateFormat.format(createdAt)));
}
@@ -85,10 +85,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
);
if (response.statusCode == 200) {
return compute(
PagedSearchResult.fromJson,
PagedSearchResult.fromJsonSingleParam,
PagedSearchResultJsonSerializer<DocumentModel>(
response.data,
DocumentModel.fromJson,
DocumentModelJsonConverter(),
),
);
} else {
@@ -254,10 +254,10 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
await client.get("/api/documents/?more_like=$docId&pageSize=10");
if (response.statusCode == 200) {
return (await compute(
PagedSearchResult<SimilarDocumentModel>.fromJson,
PagedSearchResult<SimilarDocumentModel>.fromJsonSingleParam,
PagedSearchResultJsonSerializer(
response.data,
SimilarDocumentModel.fromJson,
SimilarDocumentModelJsonConverter(),
),
))
.results;

View File

@@ -18,6 +18,7 @@ dependencies:
json_annotation: ^4.7.0
intl: ^0.17.0
dio: ^4.0.6
collection: ^1.17.0
dev_dependencies:
flutter_test:

View File

@@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/models/query_parameters/tags_query/include_tag_id_query.dart';
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
void main() {

View File

@@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "8c7478991c7bbde2c1e18034ac697723176a5d3e7e0ca06c7f9aed69b6f388d7"
sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201"
url: "https://pub.dev"
source: hosted
version: "51.0.0"
version: "52.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "120fe7ce25377ba616bb210e7584983b163861f45d6ec446744d507e3943881b"
sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4
url: "https://pub.dev"
source: hosted
version: "5.3.1"
version: "5.4.0"
archive:
dependency: transitive
description:
@@ -189,10 +189,10 @@ packages:
dependency: transitive
description:
name: checked_yaml
sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659
sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.0.2"
clock:
dependency: transitive
description:
@@ -205,10 +205,10 @@ packages:
dependency: transitive
description:
name: code_builder
sha256: "02ce3596b459c666530f045ad6f96209474e8fee6e4855940a3cee65fb872ec5"
sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe"
url: "https://pub.dev"
source: hosted
version: "4.3.0"
version: "4.4.0"
collection:
dependency: "direct main"
description:
@@ -330,7 +330,7 @@ packages:
source: hosted
version: "3.2.2"
device_info_plus:
dependency: transitive
dependency: "direct main"
description:
name: device_info_plus
sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6
@@ -406,7 +406,7 @@ packages:
description:
path: "."
ref: master
resolved-ref: "2d417dd77e075cb12e82a390e50cc4554e877ec4"
resolved-ref: "8c80e3a6e231985763ff501ad7ae12d76995a2e8"
url: "https://github.com/sawankumarbundelkhandi/edge_detection"
source: git
version: "1.1.1"
@@ -470,10 +470,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: f9245fc33aeba9e0b938d7f3785f10b7a7230e05b8fc40f5a6a8342d7899e391
sha256: ecf52f978e72763ede54a93271318bbbca65a2be2d9ff658ec8ca4ea3a23d7ef
url: "https://pub.dev"
source: hosted
version: "3.0.4"
version: "5.2.4"
fixnum:
dependency: transitive
description:
@@ -756,10 +756,10 @@ packages:
dependency: "direct main"
description:
name: image
sha256: f6ffe2895e3c86c6ad5a27e6302cf807403463e397cb2f0c580f619ac2fa588b
sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
version: "3.3.0"
infinite_scroll_pagination:
dependency: "direct main"
description:
@@ -809,10 +809,10 @@ packages:
dependency: "direct main"
description:
name: introduction_screen
sha256: "307869da307a6ba291008f8d06030816e94fe0759f6e34d671f9c39c4d512937"
sha256: "26d06cff940b9f3f1ec6591a6beea4da31183574b279c373e142ca76882ce9ea"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
io:
dependency: transitive
description:
@@ -857,34 +857,34 @@ packages:
dependency: "direct main"
description:
name: local_auth
sha256: "792b06b9e7deb52f1f55b5de678a319261c395e61d804e0f3f97c732cf002aef"
sha256: "8cea55dca20d1e0efa5480df2d47ae30851e7a24cb8e7d225be7e67ae8485aa4"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: "95cfa61a43e0b4307d7b0abb94cad71fe47601292cafb54c2205b97b9f958fb8"
sha256: ba48fe0e1cae140a0813ce68c2540250d7f573a8ae4d4b6c681b2d2583584953
url: "https://pub.dev"
source: hosted
version: "1.0.16"
version: "1.0.17"
local_auth_ios:
dependency: transitive
description:
name: local_auth_ios
sha256: "1600673a460e60ff068af91dc80b507b2ac811645db439f757521dfced0d0ab2"
sha256: aa32478d7513066564139af57e11e2cad1bbd535c1efd224a88a8764c5665e3b
url: "https://pub.dev"
source: hosted
version: "1.0.11"
version: "1.0.12"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: b069647f81ed12c833d3f1f7c2880e20b86ca04429e786df76731ac2d4220c47
sha256: fbb6973f2fd088e2677f39a5ab550aa1cfbc00997859d5e865569872499d6d61
url: "https://pub.dev"
source: hosted
version: "1.0.5"
version: "1.0.6"
local_auth_windows:
dependency: transitive
description:
@@ -1152,18 +1152,18 @@ packages:
dependency: "direct main"
description:
name: permission_handler
sha256: "5749ebeb7ec0c3865ea17e3eb337174b87747be816dab582c551e1aff6f6bbf3"
sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8"
url: "https://pub.dev"
source: hosted
version: "9.2.0"
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: a512e0fa8abcb0659d938ec2df93a70eb1df1fdea5fdc6d79a866bfd858a28fc
sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2"
url: "https://pub.dev"
source: hosted
version: "9.0.2+1"
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
@@ -1762,5 +1762,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.5 <4.0.0"
dart: ">=3.0.0-35.0.dev <4.0.0"
flutter: ">=3.3.0"

View File

@@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.4.0+11
environment:
sdk: ">=2.17.0 <3.0.0"
sdk: '>=3.0.0-35.0.dev <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -33,7 +33,7 @@ dependencies:
flutter_localizations:
sdk: flutter
encrypted_shared_preferences: ^3.0.0
permission_handler: ^9.2.0
permission_handler: ^10.2.0
pdf: ^3.8.1
pdfx: ^2.3.0
edge_detection:
@@ -46,7 +46,7 @@ dependencies:
intl: ^0.17.0
flutter_svg: ^1.0.3
url_launcher: ^6.1.2
file_picker: ^3.0.4
file_picker: ^5.2.4
web_socket_channel: ^2.2.0
http: ^0.13.4
http_interceptor: ^2.0.0-beta.5
@@ -83,6 +83,7 @@ dependencies:
json_annotation: ^4.7.0
pretty_dio_logger: ^1.2.0-beta-1
collection: ^1.17.0
device_info_plus: ^4.1.3
dev_dependencies:
integration_test:

View File

@@ -35,7 +35,7 @@ void main() async {
group("Test DocumentsCubit reloadDocuments", () {
test("Assert correct initial state", () {
expect(DocumentsCubit(documentRepository).state, DocumentsState.initial);
expect(DocumentsCubit(documentRepository).state, const DocumentsState());
});
blocTest<DocumentsCubit, DocumentsState>(
@@ -49,11 +49,11 @@ void main() async {
),
),
build: () => DocumentsCubit(documentRepository),
seed: () => DocumentsState.initial,
seed: () => const DocumentsState(),
act: (bloc) => bloc.load(),
expect: () => [
DocumentsState(
isLoaded: true,
hasLoaded: true,
value: [
PagedSearchResult(
count: 10,
@@ -78,11 +78,11 @@ void main() async {
),
),
build: () => DocumentsCubit(documentRepository),
seed: () => DocumentsState.initial,
seed: () => const DocumentsState(),
act: (bloc) => bloc.load(),
expect: () => [
DocumentsState(
isLoaded: true,
hasLoaded: true,
value: [
PagedSearchResult(
count: 10,