feat: Rework error handling, upgrade dio, fixed bugs

- Fix grey screen bug when adding labels from documnet upload
- Add more permission checks to conditionally show widgets
This commit is contained in:
Anton Stubenbord
2023-07-22 14:17:48 +02:00
parent c4f2810974
commit 6566b2b8d7
70 changed files with 1446 additions and 1133 deletions

View File

@@ -70,13 +70,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<void> loadFullContent() async {
final doc = await _api.find(state.document.id);
if (doc == null) {
return;
}
emit(state.copyWith(
isFullContentLoaded: true,
fullContent: doc.content,
));
emit(
state.copyWith(
isFullContentLoaded: true,
fullContent: doc.content,
),
);
}
Future<void> assignAsn(
@@ -99,13 +98,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
Future<ResultType> openDocumentInSystemViewer() async {
final cacheDir = await FileService.temporaryDirectory;
//TODO: Why is this cleared here?
await FileService.clearDirectoryContent(PaperlessDirectoryType.temporary);
if (state.metaData == null) {
await loadMetaData();
}
final desc = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "));
state.metaData!.mediaFilename.replaceAll("/", " "),
);
final fileName = "${desc.filename}.pdf";
final file = File("${cacheDir.path}/$fileName");

View File

@@ -287,8 +287,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Widget _buildEditButton() {
bool canEdit = context.watchInternetConnection &&
LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.document);
LocalUserAccount.current.paperlessUser.canEditDocuments;
if (!canEdit) {
return const SizedBox.shrink();
}
@@ -319,8 +318,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final isConnected = connectivityState.isConnected;
final canDelete = isConnected &&
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete, PermissionTarget.document);
LocalUserAccount.current.paperlessUser.canDeleteDocuments;
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
@@ -430,7 +428,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
try {
await context.read<DocumentDetailsCubit>().delete(document);
showSnackBar(context, S.of(context)!.documentSuccessfullyDeleted);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} finally {
// Document deleted => go back to primary route

View File

@@ -4,7 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -48,10 +47,7 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@override
Widget build(BuildContext context) {
final userCanEditDocument =
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change,
PermissionTarget.document,
);
LocalUserAccount.current.paperlessUser.canEditDocuments;
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) =>
previous.document.archiveSerialNumber !=
@@ -124,12 +120,14 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
.read<DocumentDetailsCubit>()
.assignAsn(widget.document, asn: asn)
.then((value) => _onAsnUpdated())
.onError<PaperlessServerException>(
.onError<PaperlessApiException>(
(error, stackTrace) => showErrorMessage(context, error, stackTrace),
)
.onError<PaperlessValidationErrors>(
(error, stackTrace) => setState(() => _errors = error),
);
.onError<PaperlessFormValidationException>(
(error, stackTrace) {
setState(() => _errors = error.validationMessages);
},
);
FocusScope.of(context).unfocus();
}
@@ -141,9 +139,10 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
autoAssign: true,
)
.then((value) => _onAsnUpdated())
.onError<PaperlessServerException>(
.onError<PaperlessApiException>(
(error, stackTrace) => showErrorMessage(context, error, stackTrace),
);
)
.catchError((error) => showGenericError(context, error));
}
void _onAsnUpdated() {

View File

@@ -95,7 +95,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
locale: globalSettings.preferredLocaleSubtag,
);
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} catch (error) {
showGenericError(context, error);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
@@ -45,38 +46,35 @@ class DocumentOverviewWidget extends StatelessWidget {
context: context,
label: S.of(context)!.createdAt,
).paddedOnly(bottom: itemSpacing),
Visibility(
visible: document.documentType != null,
child: DetailsItem(
if (document.documentType != null &&
LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
DetailsItem(
label: S.of(context)!.documentType,
content: LabelText<DocumentType>(
style: Theme.of(context).textTheme.bodyLarge,
label: availableDocumentTypes[document.documentType],
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.correspondent != null,
child: DetailsItem(
if (document.correspondent != null &&
LocalUserAccount.current.paperlessUser.canViewCorrespondents)
DetailsItem(
label: S.of(context)!.correspondent,
content: LabelText<Correspondent>(
style: Theme.of(context).textTheme.bodyLarge,
label: availableCorrespondents[document.correspondent],
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.storagePath != null,
child: DetailsItem(
if (document.storagePath != null &&
LocalUserAccount.current.paperlessUser.canViewStoragePaths)
DetailsItem(
label: S.of(context)!.storagePath,
content: LabelText<StoragePath>(
label: availableStoragePaths[document.storagePath],
),
).paddedOnly(bottom: itemSpacing),
),
Visibility(
visible: document.tags.isNotEmpty,
child: DetailsItem(
if (document.tags.isNotEmpty &&
LocalUserAccount.current.paperlessUser.canViewTags)
DetailsItem(
label: S.of(context)!.tags,
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
@@ -86,7 +84,6 @@ class DocumentOverviewWidget extends StatelessWidget {
),
),
).paddedOnly(bottom: itemSpacing),
),
],
),
);

View File

@@ -90,7 +90,7 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
await context.read<DocumentDetailsCubit>().shareDocument(
shareOriginal: original,
);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} catch (error) {
showGenericError(context, error);

View File

@@ -123,12 +123,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
canCreateNewLabel: LocalUserAccount.current
.paperlessUser.canCreateCorrespondents,
),
if (_filteredSuggestions
?.hasSuggestedCorrespondents ??
@@ -164,12 +160,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
initialName: currentInput,
),
),
canCreateNewLabel: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
canCreateNewLabel: LocalUserAccount.current
.paperlessUser.canCreateDocumentTypes,
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue:
@@ -214,12 +206,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
child: AddStoragePathPage(
initalName: initialValue),
),
canCreateNewLabel: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.add,
PermissionTarget.storagePath,
),
canCreateNewLabel: LocalUserAccount.current
.paperlessUser.canCreateStoragePaths,
addLabelText: S.of(context)!.addStoragePath,
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
@@ -328,7 +316,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
try {
await context.read<DocumentEditCubit>().updateDocument(mergedDocument);
showSnackBar(context, S.of(context)!.documentSuccessfullyUpdated);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} finally {
setState(() {

View File

@@ -22,7 +22,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
scans.removeAt(fileIndex);
emit(scans);
} catch (_) {
throw const PaperlessServerException(ErrorCode.scanRemoveFailed);
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
}
}
@@ -37,7 +37,7 @@ class DocumentScannerCubit extends Cubit<List<File>> {
imageCache.clear();
emit([]);
} catch (_) {
throw const PaperlessServerException(ErrorCode.scanRemoveFailed);
throw const PaperlessApiException(ErrorCode.scanRemoveFailed);
}
}

View File

@@ -73,7 +73,9 @@ class _ScannerPageState extends State<ScannerPage>
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: const SliverSearchBar(),
sliver: SliverSearchBar(
titleText: S.of(context)!.scanner,
),
),
SliverOverlapAbsorber(
handle: actionsHandle,
@@ -322,7 +324,7 @@ class _ScannerPageState extends State<ScannerPage>
onDelete: () async {
try {
context.read<DocumentScannerCubit>().removeScan(index);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
@@ -339,7 +341,7 @@ class _ScannerPageState extends State<ScannerPage>
void _reset(BuildContext context) {
try {
context.read<DocumentScannerCubit>().reset();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -360,7 +362,7 @@ class _ScannerPageState extends State<ScannerPage>
)) {
showErrorMessage(
context,
const PaperlessServerException(ErrorCode.unsupportedFileFormat),
const PaperlessApiException(ErrorCode.unsupportedFileFormat),
);
return;
}

View File

@@ -1,32 +1,74 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
import 'package:paperless_mobile/features/settings/view/widgets/user_avatar.dart';
import 'package:provider/provider.dart';
class SliverSearchBar extends StatelessWidget {
final bool floating;
final bool pinned;
final String titleText;
const SliverSearchBar({
super.key,
this.floating = false,
this.pinned = false,
required this.titleText,
});
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
sliver: SliverPersistentHeader(
floating: floating,
pinned: pinned,
delegate: CustomizableSliverPersistentHeaderDelegate(
minExtent: kToolbarHeight,
maxExtent: kToolbarHeight,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: const DocumentSearchBar(),
),
if (LocalUserAccount.current.paperlessUser.canViewDocuments) {
return SliverAppBar(
toolbarHeight: kToolbarHeight,
flexibleSpace: Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: const DocumentSearchBar(),
),
),
);
automaticallyImplyLeading: false,
);
} else {
return SliverAppBar(
title: Text(titleText),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12.0),
child: IconButton(
padding: const EdgeInsets.all(6),
icon: GlobalSettingsBuilder(
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(account: account);
},
);
},
),
onPressed: () {
final apiVersion = context.read<ApiVersion>();
showDialog(
context: context,
builder: (context) => Provider.value(
value: apiVersion,
child: const ManageAccountsPage(),
),
);
},
),
),
],
);
}
}
}

View File

@@ -12,16 +12,17 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:provider/provider.dart';
class DocumentUploadResult {
final bool success;
@@ -56,7 +57,7 @@ class _DocumentUploadPreparationPageState
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
PaperlessValidationErrors _errors = {};
Map<String, String> _errors = {};
bool _isUploadLoading = false;
late bool _syncTitleAndFilename;
bool _showDatePickerDeleteIcon = false;
@@ -197,54 +198,64 @@ class _DocumentUploadPreparationPageState
),
),
// Correspondent
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(initialName: initialName),
if (LocalUserAccount
.current.paperlessUser.canViewCorrespondents)
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) => MultiProvider(
providers: [
Provider.value(
value: context.read<LabelRepository>(),
),
Provider.value(
value: context.read<ApiVersion>(),
)
],
child: AddCorrespondentPage(initialName: initialName),
),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateCorrespondents,
),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent + " *",
name: DocumentModel.correspondentKey,
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
),
// Document type
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(initialName: initialName),
if (LocalUserAccount.current.paperlessUser.canViewDocumentTypes)
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) => MultiProvider(
providers: [
Provider.value(
value: context.read<LabelRepository>(),
),
Provider.value(
value: context.read<ApiVersion>(),
)
],
child: AddDocumentTypePage(initialName: initialName),
),
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey,
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount
.current.paperlessUser.canCreateDocumentTypes,
),
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType + " *",
name: DocumentModel.documentTypeKey,
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
if (LocalUserAccount.current.paperlessUser.canViewTags)
TagsFormField(
name: DocumentModel.tagsKey,
allowCreation: true,
allowExclude: false,
allowOnlySelection: true,
options: state.tags,
),
),
TagsFormField(
name: DocumentModel.tagsKey,
allowCreation: true,
allowExclude: false,
allowOnlySelection: true,
options: state.tags,
),
Text(
"* " + S.of(context)!.uploadInferValuesHint,
style: Theme.of(context).textTheme.bodySmall,
@@ -301,14 +312,14 @@ class _DocumentUploadPreparationPageState
context,
DocumentUploadResult(true, taskId),
);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (errors) {
setState(() => _errors = errors);
} on PaperlessFormValidationException catch (exception) {
setState(() => _errors = exception.validationMessages);
} catch (unknownError, stackTrace) {
debugPrint(unknownError.toString());
showErrorMessage(
context, const PaperlessServerException.unknown(), stackTrace);
context, const PaperlessApiException.unknown(), stackTrace);
} finally {
setState(() {
_isUploadLoading = false;

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
@@ -53,14 +54,16 @@ class _DocumentsPageState extends State<DocumentsPage>
@override
void initState() {
super.initState();
final showSavedViews =
LocalUserAccount.current.paperlessUser.canViewSavedViews;
_tabController = TabController(
length: 2,
length: showSavedViews ? 2 : 1,
vsync: this,
);
Future.wait([
context.read<DocumentsCubit>().reload(),
context.read<SavedViewCubit>().reload(),
]).onError<PaperlessServerException>(
]).onError<PaperlessApiException>(
(error, stackTrace) {
showErrorMessage(context, error, stackTrace);
return [];
@@ -105,7 +108,7 @@ class _DocumentsPageState extends State<DocumentsPage>
listener: (context, state) {
try {
context.read<DocumentsCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
},
@@ -197,7 +200,10 @@ class _DocumentsPageState extends State<DocumentsPage>
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isEmpty) {
return const SliverSearchBar(floating: true);
return SliverSearchBar(
floating: true,
titleText: S.of(context)!.documents,
);
} else {
return DocumentSelectionSliverAppBar(
state: state,
@@ -226,7 +232,9 @@ class _DocumentsPageState extends State<DocumentsPage>
controller: _tabController,
tabs: [
Tab(text: S.of(context)!.documents),
Tab(text: S.of(context)!.views),
if (LocalUserAccount.current.paperlessUser
.canViewSavedViews)
Tab(text: S.of(context)!.views),
],
),
),
@@ -268,14 +276,16 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
),
Builder(
builder: (context) {
return _buildSavedViewsTab(
connectivityState,
context,
);
},
),
if (LocalUserAccount
.current.paperlessUser.canViewSavedViews)
Builder(
builder: (context) {
return _buildSavedViewsTab(
connectivityState,
context,
);
},
),
],
),
),
@@ -334,7 +344,7 @@ class _DocumentsPageState extends State<DocumentsPage>
context
.read<DocumentsCubit>()
.loadMore()
.onError<PaperlessServerException>(
.onError<PaperlessApiException>(
(error, stackTrace) => showErrorMessage(
context,
error,
@@ -419,7 +429,7 @@ class _DocumentsPageState extends State<DocumentsPage>
if (newView != null) {
try {
await context.read<SavedViewCubit>().add(newView);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -472,7 +482,7 @@ class _DocumentsPageState extends State<DocumentsPage>
.read<DocumentsCubit>()
.updateFilter(filter: filterIntent.filter!);
}
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -524,7 +534,7 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -555,7 +565,7 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -586,7 +596,7 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -617,7 +627,7 @@ class _DocumentsPageState extends State<DocumentsPage>
);
},
);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -626,7 +636,7 @@ class _DocumentsPageState extends State<DocumentsPage>
try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<DocumentsCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}
@@ -635,7 +645,7 @@ class _DocumentsPageState extends State<DocumentsPage>
try {
// We do not await here on purpose so we can show a linear progress indicator below the app bar.
await context.read<SavedViewCubit>().reload();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}

View File

@@ -160,10 +160,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.documentType,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateDocumentTypes,
);
}
@@ -175,10 +173,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.correspondent,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateCorrespondents,
);
}
@@ -190,10 +186,8 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
initialValue: widget.initialFilter.storagePath,
prefixIcon: const Icon(Icons.folder_outlined),
allowSelectUnassigned: false,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.storagePath,
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.canCreateStoragePaths,
);
}

View File

@@ -47,7 +47,7 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
S.of(context)!.documentsSuccessfullyDeleted,
);
context.read<DocumentsCubit>().resetSelection();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}

View File

@@ -114,7 +114,7 @@ class EditLabelForm<T extends Label> extends StatelessWidget {
if (shouldDelete) {
try {
onDelete(context, label);
} on PaperlessServerException catch (error) {
} on PaperlessApiException catch (error) {
showErrorMessage(context, error);
} catch (error, stackTrace) {
log("An error occurred!", error: error, stackTrace: stackTrace);

View File

@@ -24,10 +24,8 @@ class EditCorrespondentPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.correspondent,
),
canDelete:
LocalUserAccount.current.paperlessUser.canDeleteCorrespondents,
);
}),
);

View File

@@ -22,10 +22,8 @@ class EditDocumentTypePage extends StatelessWidget {
context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.documentType,
),
canDelete:
LocalUserAccount.current.paperlessUser.canDeleteDocumentTypes,
),
);
}

View File

@@ -23,10 +23,7 @@ class EditStoragePathPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.storagePath,
),
canDelete: LocalUserAccount.current.paperlessUser.canDeleteStoragePaths,
additionalFields: [
StoragePathAutofillFormBuilderField(
name: StoragePath.pathKey,

View File

@@ -26,10 +26,7 @@ class EditTagPage extends StatelessWidget {
context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.tag,
),
canDelete: LocalUserAccount.current.paperlessUser.canDeleteTags,
additionalFields: [
FormBuilderColorPickerField(
initialValue: tag.color,

View File

@@ -4,7 +4,6 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
@@ -54,7 +53,7 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
late bool _enableMatchFormField;
PaperlessValidationErrors _errors = {};
Map<String, String> _errors = {};
@override
void initState() {
@@ -69,7 +68,8 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
Widget build(BuildContext context) {
List<MatchingAlgorithm> selectableMatchingAlgorithmValues =
getSelectableMatchingAlgorithmValues(
context.watch<ApiVersion>().hasMultiUserSupport);
context.watch<ApiVersion>().hasMultiUserSupport,
);
return Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
@@ -168,10 +168,10 @@ class _LabelFormState<T extends Label> extends State<LabelForm<T>> {
final parsed = widget.fromJsonT(mergedJson);
final createdLabel = await widget.submitButtonConfig.onSubmit(parsed);
Navigator.pop(context, createdLabel);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on PaperlessValidationErrors catch (errors) {
setState(() => _errors = errors);
} on PaperlessFormValidationException catch (exception) {
setState(() => _errors = exception.validationMessages);
}
}
}

View File

@@ -44,14 +44,14 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0;
late Timer _inboxTimer;
Timer? _inboxTimer;
late final StreamSubscription _shareMediaSubscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_listenToInboxChanges();
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
@@ -73,13 +73,15 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
void _listenToInboxChanges() {
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
if (!mounted) {
timer.cancel();
} else {
context.read<InboxCubit>().refreshItemsInInboxCount();
}
});
if (LocalUserAccount.current.paperlessUser.canViewTags) {
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
if (!mounted) {
timer.cancel();
} else {
context.read<InboxCubit>().refreshItemsInInboxCount();
}
});
}
}
@override
@@ -89,7 +91,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
log('App is now in foreground');
context.read<ConnectivityCubit>().reload();
log("Reloaded device connectivity state");
if (!_inboxTimer.isActive) {
if (!(_inboxTimer?.isActive ?? true)) {
_listenToInboxChanges();
}
break;
@@ -98,7 +100,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
case AppLifecycleState.detached:
default:
log('App is now in background');
_inboxTimer.cancel();
_inboxTimer?.cancel();
break;
}
}
@@ -106,7 +108,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_inboxTimer.cancel();
_inboxTimer?.cancel();
_shareMediaSubscription.cancel();
super.dispose();
}
@@ -158,8 +160,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
return;
}
if (!LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document)) {
if (!LocalUserAccount.current.paperlessUser.canCreateDocuments) {
Fluttertoast.showToast(
msg: "You do not have the permissions to upload documents.",
);
@@ -200,8 +201,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
label: S.of(context)!.documents,
),
if (LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.add, PermissionTarget.document))
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
RouteDescription(
icon: const Icon(Icons.document_scanner_outlined),
selectedIcon: Icon(
@@ -218,33 +218,31 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
),
label: S.of(context)!.labels,
),
RouteDescription(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: Icon(
Icons.inbox,
color: Theme.of(context).colorScheme.primary,
if (LocalUserAccount.current.paperlessUser.canViewTags)
RouteDescription(
icon: const Icon(Icons.inbox_outlined),
selectedIcon: Icon(
Icons.inbox,
color: Theme.of(context).colorScheme.primary,
),
label: S.of(context)!.inbox,
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
},
),
),
label: S.of(context)!.inbox,
badgeBuilder: (icon) => BlocBuilder<InboxCubit, InboxState>(
builder: (context, state) {
return Badge.count(
isLabelVisible: state.itemsInInboxCount > 0,
count: state.itemsInInboxCount,
child: icon,
);
},
),
),
];
final routes = <Widget>[
const DocumentsPage(),
if (LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.document,
))
if (LocalUserAccount.current.paperlessUser.canCreateDocuments)
const ScannerPage(),
const LabelsPage(),
const InboxPage(),
if (LocalUserAccount.current.paperlessUser.canViewTags) const InboxPage(),
];
return MultiBlocListener(
listeners: [

View File

@@ -3,6 +3,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
@@ -43,7 +44,14 @@ class HomeRoute extends StatelessWidget {
Widget build(BuildContext context) {
return GlobalSettingsBuilder(
builder: (context, settings) {
final currentLocalUserId = settings.currentLoggedInUser!;
final currentLocalUserId = settings.currentLoggedInUser;
if (currentLocalUserId == null) {
// This is the case when the current user logs out of the app.
return SizedBox.shrink();
}
final currentUser =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.get(currentLocalUserId)!;
final apiVersion = ApiVersion(paperlessApiVersion);
return MultiProvider(
providers: [
@@ -104,12 +112,31 @@ class HomeRoute extends StatelessWidget {
return MultiProvider(
providers: [
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
update: (context, value, previous) =>
LabelRepository(value)..initialize(),
update: (context, value, previous) {
final repo = LabelRepository(value);
if (currentUser.paperlessUser.canViewCorrespondents) {
repo.findAllCorrespondents();
}
if (currentUser.paperlessUser.canViewDocumentTypes) {
repo.findAllDocumentTypes();
}
if (currentUser.paperlessUser.canViewTags) {
repo.findAllTags();
}
if (currentUser.paperlessUser.canViewStoragePaths) {
repo.findAllStoragePaths();
}
return repo;
},
),
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
update: (context, value, previous) =>
SavedViewRepository(value)..initialize(),
update: (context, value, previous) {
final repo = SavedViewRepository(value);
if (currentUser.paperlessUser.canViewSavedViews) {
repo.initialize();
}
return repo;
},
),
],
builder: (context, child) {

View File

@@ -38,8 +38,8 @@ class _InboxPageState extends State<InboxPage>
@override
Widget build(BuildContext context) {
final canEditDocument = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.document);
final canEditDocument =
LocalUserAccount.current.paperlessUser.canEditDocuments;
return Scaffold(
drawer: const AppDrawer(),
floatingActionButton: BlocBuilder<InboxCubit, InboxState>(
@@ -65,7 +65,9 @@ class _InboxPageState extends State<InboxPage>
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: const SliverSearchBar(),
sliver: SliverSearchBar(
titleText: S.of(context)!.inbox,
),
)
],
body: BlocBuilder<InboxCubit, InboxState>(
@@ -222,14 +224,14 @@ class _InboxPageState extends State<InboxPage>
),
);
return true;
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} on ServerMessageException catch (error) {
showGenericError(context, error.message);
} catch (error) {
showErrorMessage(
context,
const PaperlessServerException.unknown(),
const PaperlessApiException.unknown(),
);
}
return false;
@@ -243,7 +245,7 @@ class _InboxPageState extends State<InboxPage>
await context
.read<InboxCubit>()
.undoRemoveFromInbox(document, removedTags);
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}

View File

@@ -238,10 +238,8 @@ class _InboxItemState extends State<InboxItem> {
}
Widget _buildActions(BuildContext context) {
final canEdit = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.change, PermissionTarget.document);
final canDelete = LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.delete, PermissionTarget.document);
final canEdit = LocalUserAccount.current.paperlessUser.canEditDocuments;
final canDelete = LocalUserAccount.current.paperlessUser.canDeleteDocuments;
final chipShape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(32),
);

View File

@@ -73,10 +73,7 @@ class TagsFormField extends StatelessWidget {
initialValue: field.value,
allowOnlySelection: allowOnlySelection,
allowCreation: allowCreation &&
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.tag,
),
LocalUserAccount.current.paperlessUser.canCreateTags,
allowExclude: allowExclude,
),
onClosed: (data) {

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
@@ -39,291 +42,327 @@ class _LabelsPageState extends State<LabelsPage>
late final TabController _tabController;
int _currentIndex = 0;
int _calculateTabCount(UserModel user) => [
user.canViewCorrespondents,
user.canViewDocumentTypes,
user.canViewTags,
user.canViewStoragePaths,
].fold(0, (value, element) => value + (element ? 1 : 0));
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this)
final user = LocalUserAccount.current.paperlessUser;
_tabController = TabController(
length: _calculateTabCount(user), vsync: this)
..addListener(() => setState(() => _currentIndex = _tabController.index));
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) {
return SafeArea(
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton(
onPressed: [
_openAddCorrespondentPage,
_openAddDocumentTypePage,
_openAddTagPage,
_openAddStoragePathPage,
][_currentIndex],
child: const Icon(Icons.add),
),
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: const SliverSearchBar(),
return ValueListenableBuilder(
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
builder: (context, box, child) {
final currentUserId =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser;
final user = box.get(currentUserId)!.paperlessUser;
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
builder: (context, connectedState) {
return SafeArea(
child: Scaffold(
drawer: const AppDrawer(),
floatingActionButton: FloatingActionButton(
onPressed: [
if (user.canViewCorrespondents) _openAddCorrespondentPage,
if (user.canViewDocumentTypes) _openAddDocumentTypePage,
if (user.canViewTags) _openAddTagPage,
if (user.canViewStoragePaths) _openAddStoragePathPage,
][_currentIndex],
child: const Icon(Icons.add),
),
SliverOverlapAbsorber(
handle: tabBarHandle,
sliver: SliverPersistentHeader(
pinned: true,
delegate: CustomizableSliverPersistentHeaderDelegate(
child: ColoredTabBar(
tabBar: TabBar(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: SliverSearchBar(
titleText: S.of(context)!.labels,
),
),
SliverOverlapAbsorber(
handle: tabBarHandle,
sliver: SliverPersistentHeader(
pinned: true,
delegate: CustomizableSliverPersistentHeaderDelegate(
child: ColoredTabBar(
tabBar: TabBar(
controller: _tabController,
tabs: [
if (user.canViewCorrespondents)
Tab(
icon: Tooltip(
message: S.of(context)!.correspondents,
child: Icon(
Icons.person_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewDocumentTypes)
Tab(
icon: Tooltip(
message: S.of(context)!.documentTypes,
child: Icon(
Icons.description_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewTags)
Tab(
icon: Tooltip(
message: S.of(context)!.tags,
child: Icon(
Icons.label_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
if (user.canViewStoragePaths)
Tab(
icon: Tooltip(
message: S.of(context)!.storagePaths,
child: Icon(
Icons.folder_open,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
],
),
),
minExtent: kTextTabBarHeight,
maxExtent: kTextTabBarHeight,
),
),
),
],
body: BlocBuilder<LabelCubit, LabelState>(
builder: (context, state) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
}
final desiredTab =
((metrics.pixels / metrics.maxScrollExtent) *
(_tabController.length - 1))
.round();
if (metrics.axis == Axis.horizontal &&
_currentIndex != desiredTab) {
setState(() => _currentIndex = desiredTab);
}
return true;
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () async {
try {
await [
context
.read<LabelCubit>()
.reloadCorrespondents,
context
.read<LabelCubit>()
.reloadDocumentTypes,
context.read<LabelCubit>().reloadTags,
context.read<LabelCubit>().reloadStoragePaths,
][_currentIndex]
.call();
} catch (error, stackTrace) {
debugPrint(
"[LabelsPage] RefreshIndicator.onRefresh "
"${[
"correspondents",
"document types",
"tags",
"storage paths"
][_currentIndex]}: "
"An error occurred (${error.toString()})",
);
debugPrintStack(stackTrace: stackTrace);
}
},
child: TabBarView(
controller: _tabController,
tabs: [
Tab(
icon: Icon(
Icons.person_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
children: [
if (user.canViewCorrespondents)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: state.correspondents,
filterBuilder: (label) =>
DocumentFilter(
correspondent:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditCorrespondents,
canAddNew:
user.canCreateCorrespondents,
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewCorrespondent,
emptyStateDescription: S
.of(context)!
.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
),
),
Tab(
icon: Icon(
Icons.description_outlined,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
if (user.canViewDocumentTypes)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) =>
DocumentFilter(
documentType:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditDocumentTypes,
canAddNew:
user.canCreateDocumentTypes,
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel: S
.of(context)!
.addNewDocumentType,
emptyStateDescription: S
.of(context)!
.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
),
),
Tab(
icon: Icon(
Icons.label_outline,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
if (user.canViewTags)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) =>
DocumentFilter(
tags: TagsQuery.ids(
include: [label.id!]),
),
canEdit: user.canEditTags,
canAddNew: user.canCreateTags,
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel:
S.of(context)!.addNewTag,
emptyStateDescription:
S.of(context)!.noTagsSetUp,
onAddNew: _openAddTagPage,
),
],
);
},
),
),
Tab(
icon: Icon(
Icons.folder_open,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
if (user.canViewStoragePaths)
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(
handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) =>
DocumentFilter(
storagePath:
IdQueryParameter.fromId(
label.id!),
),
canEdit: user.canEditStoragePaths,
canAddNew:
user.canCreateStoragePaths,
contentBuilder: (path) =>
Text(path.path),
emptyStateActionButtonLabel: S
.of(context)!
.addNewStoragePath,
emptyStateDescription: S
.of(context)!
.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],
);
},
),
),
],
),
),
minExtent: kTextTabBarHeight,
maxExtent: kTextTabBarHeight),
);
},
),
),
],
body: BlocBuilder<LabelCubit, LabelState>(
builder: (context, state) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
}
final desiredTab =
((metrics.pixels / metrics.maxScrollExtent) *
(_tabController.length - 1))
.round();
if (metrics.axis == Axis.horizontal &&
_currentIndex != desiredTab) {
setState(() => _currentIndex = desiredTab);
}
return true;
},
child: RefreshIndicator(
edgeOffset: kTextTabBarHeight,
notificationPredicate: (notification) =>
connectedState.isConnected,
onRefresh: () async {
try {
await [
context.read<LabelCubit>().reloadCorrespondents,
context.read<LabelCubit>().reloadDocumentTypes,
context.read<LabelCubit>().reloadTags,
context.read<LabelCubit>().reloadStoragePaths,
][_currentIndex]
.call();
} catch (error, stackTrace) {
debugPrint(
"[LabelsPage] RefreshIndicator.onRefresh "
"${[
"correspondents",
"document types",
"tags",
"storage paths"
][_currentIndex]}: "
"An error occurred (${error.toString()})",
);
debugPrintStack(stackTrace: stackTrace);
}
},
child: TabBarView(
controller: _tabController,
children: [
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Correspondent>(
labels: state.correspondents,
filterBuilder: (label) => DocumentFilter(
correspondent:
IdQueryParameter.fromId(label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.correspondent),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.correspondent),
onEdit: _openEditCorrespondentPage,
emptyStateActionButtonLabel:
S.of(context)!.addNewCorrespondent,
emptyStateDescription:
S.of(context)!.noCorrespondentsSetUp,
onAddNew: _openAddCorrespondentPage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<DocumentType>(
labels: state.documentTypes,
filterBuilder: (label) => DocumentFilter(
documentType:
IdQueryParameter.fromId(label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.documentType),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.documentType),
onEdit: _openEditDocumentTypePage,
emptyStateActionButtonLabel:
S.of(context)!.addNewDocumentType,
emptyStateDescription:
S.of(context)!.noDocumentTypesSetUp,
onAddNew: _openAddDocumentTypePage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<Tag>(
labels: state.tags,
filterBuilder: (label) => DocumentFilter(
tags:
TagsQuery.ids(include: [label.id!]),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.tag),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.tag),
onEdit: _openEditTagPage,
leadingBuilder: (t) => CircleAvatar(
backgroundColor: t.color,
child: t.isInboxTag
? Icon(
Icons.inbox,
color: t.textColor,
)
: null,
),
emptyStateActionButtonLabel:
S.of(context)!.addNewTag,
emptyStateDescription:
S.of(context)!.noTagsSetUp,
onAddNew: _openAddTagPage,
),
],
);
},
),
Builder(
builder: (context) {
return CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: searchBarHandle),
SliverOverlapInjector(handle: tabBarHandle),
LabelTabView<StoragePath>(
labels: state.storagePaths,
onEdit: _openEditStoragePathPage,
filterBuilder: (label) => DocumentFilter(
storagePath:
IdQueryParameter.fromId(label.id!),
),
canEdit: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.change,
PermissionTarget.storagePath),
canAddNew: LocalUserAccount
.current.paperlessUser
.hasPermission(PermissionAction.add,
PermissionTarget.storagePath),
contentBuilder: (path) => Text(path.path),
emptyStateActionButtonLabel:
S.of(context)!.addNewStoragePath,
emptyStateDescription:
S.of(context)!.noStoragePathsSetUp,
onAddNew: _openAddStoragePathPage,
),
],
);
},
),
],
),
),
);
},
),
),
),
);
},
);
},
),
);
});
}
void _openEditCorrespondentPage(Correspondent correspondent) {

View File

@@ -36,8 +36,7 @@ class LabelItem<T extends Label> extends StatelessWidget {
Widget _buildReferencedDocumentsWidget(BuildContext context) {
final canOpen = (label.documentCount ?? 0) > 0 &&
LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.view, PermissionTarget.document);
LocalUserAccount.current.paperlessUser.canViewDocuments;
return TextButton.icon(
label: const Icon(Icons.link),
icon: Text(formatMaxCount(label.documentCount)),

View File

@@ -365,7 +365,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
apiVersion: apiVersion,
)
.findCurrentUser();
} on DioError catch (error, stackTrace) {
} on DioException catch (error, stackTrace) {
_debugPrintMessage(
"_addUser",
"An error occurred: ${error.message}",

View File

@@ -105,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
),
),
ServerConnectionPage(
titleString: widget.titleString,
titleText: widget.titleString,
formBuilderKey: _formKey,
onContinue: () {
_pageController.nextPage(
@@ -126,7 +126,6 @@ class _LoginPageState extends State<LoginPage> {
}
Future<void> _login() async {
FocusScope.of(context).unfocus();
if (_formKey.currentState?.saveAndValidate() ?? false) {
final form = _formKey.currentState!.value;
@@ -150,7 +149,7 @@ class _LoginPageState extends State<LoginPage> {
form[ServerAddressFormField.fkServerAddress],
clientCert,
);
} on PaperlessServerException catch (error) {
} on PaperlessApiException catch (error) {
showErrorMessage(context, error);
} on ServerMessageException catch (error) {
showLocalizedError(context, error.message);

View File

@@ -66,7 +66,10 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.values
.where((element) => element.contains(textEditingValue.text));
},
onSelected: (option) => _formatInput(),
onSelected: (option) {
_formatInput();
field.didChange(_textEditingController.text);
},
fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
return TextField(
@@ -111,6 +114,10 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
String address = _textEditingController.text.trim();
address = address.replaceAll(RegExp(r'^\/+|\/+$'), '');
_textEditingController.text = address;
_textEditingController.selection = TextSelection(
baseOffset: address.length,
extentOffset: address.length,
);
widget.onSubmit(address);
}
}

View File

@@ -13,14 +13,14 @@ import 'package:provider/provider.dart';
class ServerConnectionPage extends StatefulWidget {
final GlobalKey<FormBuilderState> formBuilderKey;
final void Function() onContinue;
final String titleString;
final VoidCallback onContinue;
final String titleText;
const ServerConnectionPage({
super.key,
required this.formBuilderKey,
required this.onContinue,
required this.titleString,
required this.titleText,
});
@override
@@ -36,7 +36,7 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
return Scaffold(
appBar: AppBar(
toolbarHeight: kToolbarHeight - 4,
title: Text(widget.titleString),
title: Text(widget.titleText),
bottom: PreferredSize(
child: _isCheckingConnection
? const LinearProgressIndicator()

View File

@@ -26,7 +26,7 @@ mixin DocumentPagingViewMixin<T extends StatefulWidget,
if (shouldLoadMoreDocuments) {
try {
await _bloc.loadMore();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}

View File

@@ -27,7 +27,7 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
super.initState();
try {
context.read<SimilarDocumentsCubit>().initialize();
} on PaperlessServerException catch (error, stackTrace) {
} on PaperlessApiException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
}
}