mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-07 13:15:55 -06:00
Fixed bugs, added serialization for documents state
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user