mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 13:15:49 -06:00
Fixed bugs, added serialization for documents state
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() {
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
36
lib/features/sharing/share_intent_queue.dart
Normal file
36
lib/features/sharing/share_intent_queue.dart
Normal 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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ library paperless_api;
|
||||
|
||||
export 'src/models/models.dart';
|
||||
export 'src/modules/modules.dart';
|
||||
export 'src/converters/converters.dart';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'document_model_json_converter.dart';
|
||||
export 'similar_document_model_json_converter.dart';
|
||||
export 'date_range_query_json_converter.dart';
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
83
packages/paperless_api/lib/src/models/document_filter.g.dart
Normal file
83
packages/paperless_api/lib/src/models/document_filter.g.dart
Normal 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',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
enum DateRangeQueryField {
|
||||
created,
|
||||
added,
|
||||
modified;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
enum DateRangeUnit {
|
||||
day,
|
||||
week,
|
||||
month,
|
||||
year;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
@JsonEnum()
|
||||
enum TagsQueryType {
|
||||
notAssigned,
|
||||
anyAssigned,
|
||||
ids,
|
||||
id,
|
||||
include,
|
||||
exclude;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
@JsonEnum()
|
||||
enum SortField {
|
||||
archiveSerialNumber("archive_serial_number"),
|
||||
correspondentName("correspondent__name"),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
@JsonEnum()
|
||||
enum SortOrder {
|
||||
ascending(""),
|
||||
descending("-");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>{};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class TagsQuery extends Equatable {
|
||||
const TagsQuery();
|
||||
Map<String, String> toQueryParameter();
|
||||
Map<String, dynamic> toJson();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
58
pubspec.lock
58
pubspec.lock
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user