Fixed bugs, added serialization for documents state

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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