fix: Fixed saved views bug, formatted files, minor changes

This commit is contained in:
Anton Stubenbord
2023-06-10 16:29:12 +02:00
parent 3161343c35
commit 4c3f97136e
93 changed files with 1049 additions and 585 deletions

View File

@@ -95,7 +95,8 @@ class AppDrawer extends StatelessWidget {
MaterialPageRoute(
builder: (_) => MultiProvider(
providers: [
Provider.value(value: context.read<PaperlessServerStatsApi>()),
Provider.value(
value: context.read<PaperlessServerStatsApi>()),
Provider.value(value: context.read<ApiVersion>()),
],
child: const SettingsPage(),
@@ -128,7 +129,8 @@ class AppDrawer extends StatelessWidget {
),
RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
style: theme.textTheme.bodyMedium
?.copyWith(color: colorScheme.onSurface),
children: [
TextSpan(
text: S.of(context)!.findTheSourceCodeOn,
@@ -151,11 +153,13 @@ class AppDrawer extends StatelessWidget {
const SizedBox(height: 16),
Text(
'Credits',
style: theme.textTheme.titleMedium?.copyWith(color: colorScheme.onSurface),
style: theme.textTheme.titleMedium
?.copyWith(color: colorScheme.onSurface),
),
RichText(
text: TextSpan(
style: theme.textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
style: theme.textTheme.bodyMedium
?.copyWith(color: colorScheme.onSurface),
children: [
const TextSpan(
text: 'Onboarding images by ',
@@ -205,16 +209,16 @@ class AppDrawer extends StatelessWidget {
}
//Wrap(
// children: [
// const Text('Onboarding images by '),
// GestureDetector(
// onTap: followLink,
// child: RichText(
// 'pch.vector',
// style: TextStyle(color: Colors.blue),
// ),
// ),
// const Text(' on Freepik.')
// ],
// )
// children: [
// const Text('Onboarding images by '),
// GestureDetector(
// onTap: followLink,
// child: RichText(
// 'pch.vector',
// style: TextStyle(color: Colors.blue),
// ),
// ),
// const Text(' on Freepik.')
// ],
// )

View File

@@ -10,7 +10,8 @@ class ApplicationIntroSlideshow extends StatefulWidget {
const ApplicationIntroSlideshow({super.key});
@override
State<ApplicationIntroSlideshow> createState() => _ApplicationIntroSlideshowState();
State<ApplicationIntroSlideshow> createState() =>
_ApplicationIntroSlideshowState();
}
//TODO: INTL ALL

View File

@@ -8,7 +8,8 @@ import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bu
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
typedef LabelOptionsSelector<T extends Label> = Map<int, T> Function(DocumentBulkActionState state);
typedef LabelOptionsSelector<T extends Label> = Map<int, T> Function(
DocumentBulkActionState state);
class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
final String title;
@@ -31,16 +32,19 @@ class BulkEditLabelBottomSheet<T extends Label> extends StatefulWidget {
});
@override
State<BulkEditLabelBottomSheet<T>> createState() => _BulkEditLabelBottomSheetState<T>();
State<BulkEditLabelBottomSheet<T>> createState() =>
_BulkEditLabelBottomSheetState<T>();
}
class _BulkEditLabelBottomSheetState<T extends Label> extends State<BulkEditLabelBottomSheet<T>> {
class _BulkEditLabelBottomSheetState<T extends Label>
extends State<BulkEditLabelBottomSheet<T>> {
final _formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
builder: (context, state) {
return Padding(
@@ -76,11 +80,13 @@ class _BulkEditLabelBottomSheetState<T extends Label> extends State<BulkEditLabe
const SizedBox(width: 16),
FilledButton(
onPressed: () {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final value = _formKey.currentState?.getRawValue('labelFormField')
if (_formKey.currentState?.saveAndValidate() ??
false) {
final value = _formKey.currentState
?.getRawValue('labelFormField')
as IdQueryParameter?;
widget
.onSubmit(value?.maybeWhen(fromId: (id) => id, orElse: () => null));
widget.onSubmit(value?.maybeWhen(
fromId: (id) => id, orElse: () => null));
}
},
child: Text(S.of(context)!.apply),

View File

@@ -91,8 +91,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
_notifier.notifyUpdated(updatedDocument);
} else {
final int autoAsn = await _api.findNextAsn();
final updatedDocument =
await _api.update(document.copyWith(archiveSerialNumber: () => autoAsn));
final updatedDocument = await _api
.update(document.copyWith(archiveSerialNumber: () => autoAsn));
_notifier.notifyUpdated(updatedDocument);
}
}
@@ -104,7 +104,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (state.metaData == null) {
await loadMetaData();
}
final desc = FileDescription.fromPath(state.metaData!.mediaFilename.replaceAll("/", " "));
final desc = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "));
final fileName = "${desc.filename}.pdf";
final file = File("${cacheDir.path}/$fileName");
@@ -138,7 +139,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
await FileService.downloadsDirectory,
);
final desc = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure
state.metaData!.mediaFilename
.replaceAll("/", " "), // Flatten directory structure
);
if (!File(filePath).existsSync()) {
File(filePath).createSync();
@@ -205,7 +207,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
if (state.metaData == null) {
await loadMetaData();
}
final filePath = _buildDownloadFilePath(false, await FileService.temporaryDirectory);
final filePath =
_buildDownloadFilePath(false, await FileService.temporaryDirectory);
await _api.downloadToFile(
state.document,
filePath,
@@ -223,7 +226,8 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
String _buildDownloadFilePath(bool original, Directory dir) {
final description = FileDescription.fromPath(
state.metaData!.mediaFilename.replaceAll("/", " "), // Flatten directory structure
state.metaData!.mediaFilename
.replaceAll("/", " "), // Flatten directory structure
);
final extension = original ? description.extension : 'pdf';
return "${dir.path}/${description.filename}.$extension";

View File

@@ -45,7 +45,8 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
value: _rememberSelection,
onChanged: (value) => setState(() => _rememberSelection = value ?? false),
onChanged: (value) =>
setState(() => _rememberSelection = value ?? false),
title: Text(
S.of(context)!.rememberDecision,
style: Theme.of(context).textTheme.labelMedium,
@@ -61,7 +62,8 @@ class _SelectFileTypeDialogState extends State<SelectFileTypeDialog> {
if (_rememberSelection) {
widget.onRememberSelection(_downloadType);
}
Navigator.of(context).pop(_downloadType == FileDownloadType.original);
Navigator.of(context)
.pop(_downloadType == FileDownloadType.original);
},
),
],

View File

@@ -51,27 +51,35 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final tabLength = 4 + (apiVersion.hasMultiUserSupport ? 1 : 0);
return WillPopScope(
onWillPop: () async {
Navigator.of(context).pop(context.read<DocumentDetailsCubit>().state.document);
Navigator.of(context)
.pop(context.read<DocumentDetailsCubit>().state.document);
return false;
},
child: DefaultTabController(
length: tabLength,
child: BlocListener<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
listenWhen: (previous, current) =>
!previous.isConnected && current.isConnected,
listener: (context, state) {
context.read<DocumentDetailsCubit>().loadMetaData();
},
child: Scaffold(
extendBodyBehindAppBar: false,
floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButtonLocation:
FloatingActionButtonLocation.endDocked,
floatingActionButton: _buildEditButton(),
bottomNavigationBar: _buildBottomAppBar(),
body: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(context.watch<DocumentDetailsCubit>().state.document.title),
title: Text(context
.watch<DocumentDetailsCubit>()
.state
.document
.title),
leading: const BackButton(),
pinned: true,
forceElevated: innerBoxIsScrolled,
@@ -81,7 +89,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
background: Stack(
alignment: Alignment.topCenter,
children: [
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
BlocBuilder<DocumentDetailsCubit,
DocumentDetailsState>(
builder: (context, state) {
return Positioned.fill(
child: DocumentPreview(
@@ -97,8 +106,14 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.background.withOpacity(0.8),
Theme.of(context).colorScheme.background.withOpacity(0.5),
Theme.of(context)
.colorScheme
.background
.withOpacity(0.8),
Theme.of(context)
.colorScheme
.background
.withOpacity(0.5),
Colors.transparent,
Colors.transparent,
Colors.transparent,
@@ -120,7 +135,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text(
S.of(context)!.overview,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
@@ -128,7 +145,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text(
S.of(context)!.content,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
@@ -136,7 +155,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text(
S.of(context)!.metaData,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
@@ -144,7 +165,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text(
S.of(context)!.similarDocuments,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
@@ -153,7 +176,9 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
child: Text(
"Permissions",
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
),
@@ -182,7 +207,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentOverviewWidget(
document: state.document,
@@ -198,7 +224,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentContentWidget(
isFullContentLoaded: state.isFullContentLoaded,
@@ -211,7 +238,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentMetaDataWidget(
document: state.document,
@@ -223,7 +251,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
controller: _pagingScrollController,
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
SimilarDocumentsView(
pagingScrollController: _pagingScrollController,
@@ -235,7 +264,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
controller: _pagingScrollController,
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(context),
),
DocumentPermissionsWidget(
document: state.document,
@@ -289,15 +319,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
final isConnected = connectivityState.isConnected;
final canDelete = isConnected &&
LocalUserAccount.current.paperlessUser
.hasPermission(PermissionAction.delete, PermissionTarget.document);
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete, PermissionTarget.document);
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
IconButton(
tooltip: S.of(context)!.deleteDocumentTooltip,
icon: const Icon(Icons.delete),
onPressed: canDelete ? () => _onDelete(state.document) : null,
onPressed:
canDelete ? () => _onDelete(state.document) : null,
).paddedSymmetrically(horizontal: 4),
DocumentDownloadButton(
document: state.document,
@@ -307,7 +338,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
IconButton(
tooltip: S.of(context)!.previewTooltip,
icon: const Icon(Icons.visibility),
onPressed: (isConnected) ? () => _onOpen(state.document) : null,
onPressed:
(isConnected) ? () => _onOpen(state.document) : null,
).paddedOnly(right: 4.0),
IconButton(
tooltip: S.of(context)!.openInSystemViewer,
@@ -317,7 +349,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
DocumentShareButton(document: state.document),
IconButton(
tooltip: S.of(context)!.print, //TODO: INTL
onPressed: () => context.read<DocumentDetailsCubit>().printDocument(),
onPressed: () =>
context.read<DocumentDetailsCubit>().printDocument(),
icon: const Icon(Icons.print),
),
],
@@ -350,7 +383,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
),
],
child: BlocListener<DocumentEditCubit, DocumentEditState>(
listenWhen: (previous, current) => previous.document != current.document,
listenWhen: (previous, current) =>
previous.document != current.document,
listener: (context, state) {
cubit.replace(state.document);
},
@@ -370,7 +404,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
}
void _onOpenFileInSystemViewer() async {
final status = await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
final status =
await context.read<DocumentDetailsCubit>().openDocumentInSystemViewer();
if (status == ResultType.done) return;
if (status == ResultType.noAppToOpen) {
showGenericError(context, S.of(context)!.noAppToDisplayPDFFilesFound);
@@ -379,14 +414,16 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
showGenericError(context, translateError(context, ErrorCode.unknown));
}
if (status == ResultType.permissionDenied) {
showGenericError(context, S.of(context)!.couldNotOpenFilePermissionDenied);
showGenericError(
context, S.of(context)!.couldNotOpenFilePermissionDenied);
}
}
void _onDelete(DocumentModel document) async {
final delete = await showDialog(
context: context,
builder: (context) => DeleteDocumentConfirmationDialog(document: document),
builder: (context) =>
DeleteDocumentConfirmationDialog(document: document),
) ??
false;
if (delete) {
@@ -406,7 +443,8 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => DocumentView(
documentBytes: context.read<PaperlessDocumentsApi>().download(document),
documentBytes:
context.read<PaperlessDocumentsApi>().download(document),
title: document.title,
),
),

View File

@@ -18,7 +18,8 @@ class ArchiveSerialNumberField extends StatefulWidget {
});
@override
State<ArchiveSerialNumberField> createState() => _ArchiveSerialNumberFieldState();
State<ArchiveSerialNumberField> createState() =>
_ArchiveSerialNumberFieldState();
}
class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
@@ -39,21 +40,25 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
void _clearButtonListener() {
setState(() {
_showClearButton = _asnEditingController.text.isNotEmpty;
_canUpdate = int.tryParse(_asnEditingController.text) != widget.document.archiveSerialNumber;
_canUpdate = int.tryParse(_asnEditingController.text) !=
widget.document.archiveSerialNumber;
});
}
@override
Widget build(BuildContext context) {
final userCanEditDocument = LocalUserAccount.current.paperlessUser.hasPermission(
final userCanEditDocument =
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.change,
PermissionTarget.document,
);
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
listenWhen: (previous, current) =>
previous.document.archiveSerialNumber != current.document.archiveSerialNumber,
previous.document.archiveSerialNumber !=
current.document.archiveSerialNumber,
listener: (context, state) {
_asnEditingController.text = state.document.archiveSerialNumber?.toString() ?? '';
_asnEditingController.text =
state.document.archiveSerialNumber?.toString() ?? '';
setState(() {
_canUpdate = false;
});
@@ -80,13 +85,17 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
IconButton(
icon: const Icon(Icons.clear),
color: Theme.of(context).colorScheme.primary,
onPressed: userCanEditDocument ? _asnEditingController.clear : null,
onPressed: userCanEditDocument
? _asnEditingController.clear
: null,
),
IconButton(
icon: const Icon(Icons.plus_one_rounded),
color: Theme.of(context).colorScheme.primary,
onPressed:
context.watchInternetConnection && !_showClearButton ? _onAutoAssign : null,
context.watchInternetConnection && !_showClearButton
? _onAutoAssign
: null,
).paddedOnly(right: 8),
],
),
@@ -97,7 +106,9 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
),
TextButton.icon(
icon: const Icon(Icons.done),
onPressed: context.watchInternetConnection && _canUpdate ? _onSubmitted : null,
onPressed: context.watchInternetConnection && _canUpdate
? _onSubmitted
: null,
label: Text(S.of(context)!.save),
).padded(),
],

View File

@@ -24,7 +24,8 @@ class DetailsItem extends StatelessWidget {
}
DetailsItem.text(
String text, {super.key,
String text, {
super.key,
required this.label,
required BuildContext context,
}) : content = Text(

View File

@@ -44,14 +44,16 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
width: 16,
)
: const Icon(Icons.download),
onPressed:
widget.document != null && widget.enabled ? () => _onDownload(widget.document!) : null,
onPressed: widget.document != null && widget.enabled
? () => _onDownload(widget.document!)
: null,
).paddedOnly(right: 4);
}
Future<void> _onDownload(DocumentModel document) async {
try {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
bool original;
switch (globalSettings.defaultDownloadType) {

View File

@@ -6,7 +6,8 @@ class DocumentPermissionsWidget extends StatefulWidget {
const DocumentPermissionsWidget({super.key, required this.document});
@override
State<DocumentPermissionsWidget> createState() => _DocumentPermissionsWidgetState();
State<DocumentPermissionsWidget> createState() =>
_DocumentPermissionsWidgetState();
}
class _DocumentPermissionsWidgetState extends State<DocumentPermissionsWidget> {

View File

@@ -43,14 +43,16 @@ class _DocumentShareButtonState extends State<DocumentShareButton> {
child: CircularProgressIndicator(),
)
: const Icon(Icons.share),
onPressed:
widget.document != null && widget.enabled ? () => _onShare(widget.document!) : null,
onPressed: widget.document != null && widget.enabled
? () => _onShare(widget.document!)
: null,
).paddedOnly(right: 4);
}
Future<void> _onShare(DocumentModel document) async {
try {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
bool original;
switch (globalSettings.defaultShareType) {

View File

@@ -49,8 +49,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
@override
void initState() {
super.initState();
_filteredSuggestions =
widget.suggestions?.documentDifference(context.read<DocumentEditCubit>().state.document);
_filteredSuggestions = widget.suggestions
?.documentDifference(context.read<DocumentEditCubit>().state.document);
}
@override
@@ -94,14 +94,16 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
ListView(
children: [
_buildTitleFormField(state.document.title).padded(),
_buildCreatedAtFormField(state.document.created).padded(),
_buildCreatedAtFormField(state.document.created)
.padded(),
// Correspondent form field
Column(
children: [
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) => RepositoryProvider.value(
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(
initialName: initialValue,
@@ -109,26 +111,39 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
),
addLabelText: S.of(context)!.addCorrespondent,
labelText: S.of(context)!.correspondent,
options: context.watch<DocumentEditCubit>().state.correspondents,
initialValue: state.document.correspondent != null
? IdQueryParameter.fromId(state.document.correspondent!)
: const IdQueryParameter.unset(),
options: context
.watch<DocumentEditCubit>()
.state
.correspondents,
initialValue:
state.document.correspondent != null
? IdQueryParameter.fromId(
state.document.correspondent!)
: const IdQueryParameter.unset(),
name: fkCorrespondent,
prefixIcon: const Icon(Icons.person_outlined),
allowSelectUnassigned: true,
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
canCreateNewLabel: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
),
if (_filteredSuggestions?.hasSuggestedCorrespondents ?? false)
if (_filteredSuggestions
?.hasSuggestedCorrespondents ??
false)
_buildSuggestionsSkeleton<int>(
suggestions: _filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) => ActionChip(
label: Text(state.correspondents[itemData]!.name),
suggestions:
_filteredSuggestions!.correspondents,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.correspondents[itemData]!.name),
onPressed: () {
_formKey.currentState?.fields[fkCorrespondent]?.didChange(
_formKey
.currentState?.fields[fkCorrespondent]
?.didChange(
IdQueryParameter.fromId(itemData),
);
},
@@ -142,34 +157,45 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (currentInput) => RepositoryProvider.value(
addLabelPageBuilder: (currentInput) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(
initialName: currentInput,
),
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
canCreateNewLabel: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
addLabelText: S.of(context)!.addDocumentType,
labelText: S.of(context)!.documentType,
initialValue: state.document.documentType != null
? IdQueryParameter.fromId(state.document.documentType!)
: const IdQueryParameter.unset(),
initialValue:
state.document.documentType != null
? IdQueryParameter.fromId(
state.document.documentType!)
: const IdQueryParameter.unset(),
options: state.documentTypes,
name: _DocumentEditPageState.fkDocumentType,
prefixIcon: const Icon(Icons.description_outlined),
prefixIcon:
const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
),
if (_filteredSuggestions?.hasSuggestedDocumentTypes ?? false)
if (_filteredSuggestions
?.hasSuggestedDocumentTypes ??
false)
_buildSuggestionsSkeleton<int>(
suggestions: _filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) => ActionChip(
label: Text(state.documentTypes[itemData]!.name),
onPressed: () =>
_formKey.currentState?.fields[fkDocumentType]?.didChange(
suggestions:
_filteredSuggestions!.documentTypes,
itemBuilder: (context, itemData) =>
ActionChip(
label: Text(
state.documentTypes[itemData]!.name),
onPressed: () => _formKey
.currentState?.fields[fkDocumentType]
?.didChange(
IdQueryParameter.fromId(itemData),
),
),
@@ -182,12 +208,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
LabelFormField<StoragePath>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialValue) => RepositoryProvider.value(
addLabelPageBuilder: (initialValue) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddStoragePathPage(initalName: initialValue),
child: AddStoragePathPage(
initalName: initialValue),
),
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
canCreateNewLabel: LocalUserAccount
.current.paperlessUser
.hasPermission(
PermissionAction.add,
PermissionTarget.storagePath,
),
@@ -195,7 +224,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
labelText: S.of(context)!.storagePath,
options: state.storagePaths,
initialValue: state.document.storagePath != null
? IdQueryParameter.fromId(state.document.storagePath!)
? IdQueryParameter.fromId(
state.document.storagePath!)
: const IdQueryParameter.unset(),
name: fkStoragePath,
prefixIcon: const Icon(Icons.folder_outlined),
@@ -220,7 +250,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
.isNotEmpty ??
false)
_buildSuggestionsSkeleton<int>(
suggestions: (_filteredSuggestions?.tags.toSet() ?? {}),
suggestions:
(_filteredSuggestions?.tags.toSet() ?? {}),
itemBuilder: (context, itemData) {
final tag = state.tags[itemData]!;
return ActionChip(
@@ -230,13 +261,17 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
),
backgroundColor: tag.color,
onPressed: () {
final currentTags =
_formKey.currentState?.fields[fkTags]?.value as TagsQuery;
_formKey.currentState?.fields[fkTags]?.didChange(
final currentTags = _formKey.currentState
?.fields[fkTags]?.value as TagsQuery;
_formKey.currentState?.fields[fkTags]
?.didChange(
currentTags.maybeWhen(
ids: (include, exclude) => TagsQuery.ids(
include: [...include, itemData], exclude: exclude),
orElse: () => TagsQuery.ids(include: [itemData]),
ids: (include, exclude) =>
TagsQuery.ids(
include: [...include, itemData],
exclude: exclude),
orElse: () =>
TagsQuery.ids(include: [itemData]),
),
);
},
@@ -278,12 +313,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
var mergedDocument = document.copyWith(
title: values[fkTitle],
created: values[fkCreatedDate],
documentType: () =>
(values[fkDocumentType] as IdQueryParameter).whenOrNull(fromId: (id) => id),
correspondent: () =>
(values[fkCorrespondent] as IdQueryParameter).whenOrNull(fromId: (id) => id),
storagePath: () =>
(values[fkStoragePath] as IdQueryParameter).whenOrNull(fromId: (id) => id),
documentType: () => (values[fkDocumentType] as IdQueryParameter)
.whenOrNull(fromId: (id) => id),
correspondent: () => (values[fkCorrespondent] as IdQueryParameter)
.whenOrNull(fromId: (id) => id),
storagePath: () => (values[fkStoragePath] as IdQueryParameter)
.whenOrNull(fromId: (id) => id),
tags: (values[fkTags] as IdsTagsQuery).include,
content: values[fkContent],
);
@@ -340,7 +375,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
suggestions: _filteredSuggestions!.dates,
itemBuilder: (context, itemData) => ActionChip(
label: Text(DateFormat.yMMMd().format(itemData)),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]?.didChange(itemData),
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
?.didChange(itemData),
),
),
],
@@ -369,7 +405,8 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
itemBuilder: (context, index) => ColoredChipWrapper(
child: itemBuilder(context, suggestions.elementAt(index)),
),
separatorBuilder: (BuildContext context, int index) => const SizedBox(width: 4.0),
separatorBuilder: (BuildContext context, int index) =>
const SizedBox(width: 4.0),
),
),
],
@@ -405,7 +442,6 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
// final List<Option> options;
// final void Function(Option option) onAddOption;
// const OptionsFormField({
// super.key,
// required this.options,

View File

@@ -35,9 +35,12 @@ class ScannerPage extends StatefulWidget {
State<ScannerPage> createState() => _ScannerPageState();
}
class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle = SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle = SliverOverlapAbsorberHandle();
class _ScannerPageState extends State<ScannerPage>
with SingleTickerProviderStateMixin {
final SliverOverlapAbsorberHandle searchBarHandle =
SliverOverlapAbsorberHandle();
final SliverOverlapAbsorberHandle actionsHandle =
SliverOverlapAbsorberHandle();
@override
Widget build(BuildContext context) {
@@ -120,7 +123,6 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
? () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DocumentView(
documentBytes: _assembleFileBytes(
state,
forcePdf: true,
@@ -175,7 +177,8 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
final success = await EdgeDetection.detectEdge(file.path);
if (!success) {
if (kDebugMode) {
dev.log('[ScannerPage] Scan either not successful or canceled by user.');
dev.log(
'[ScannerPage] Scan either not successful or canceled by user.');
}
return;
}
@@ -197,7 +200,9 @@ class _ScannerPageState extends State<ScannerPage> with SingleTickerProviderStat
if ((uploadResult?.success ?? false) && uploadResult?.taskId != null) {
// For paperless version older than 1.11.3, task id will always be null!
context.read<DocumentScannerCubit>().reset();
context.read<TaskStatusCubit>().listenToTaskChanges(uploadResult!.taskId!);
context
.read<TaskStatusCubit>()
.listenToTaskChanges(uploadResult!.taskId!);
}
}

View File

@@ -11,7 +11,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'document_search_cubit.g.dart';
part 'document_search_state.dart';
class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPagingBlocMixin {
class DocumentSearchCubit extends Cubit<DocumentSearchState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@@ -23,7 +24,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
this.api,
this.notifier,
this._userAppState,
) : super(DocumentSearchState(searchHistory: _userAppState.documentSearchHistory)) {
) : super(DocumentSearchState(
searchHistory: _userAppState.documentSearchHistory)) {
notifier.addListener(
this,
onDeleted: remove,
@@ -46,7 +48,8 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
state.copyWith(
searchHistory: [
query,
...state.searchHistory.whereNot((previousQuery) => previousQuery == query)
...state.searchHistory
.whereNot((previousQuery) => previousQuery == query)
],
),
);
@@ -62,7 +65,9 @@ class DocumentSearchCubit extends Cubit<DocumentSearchState> with DocumentPaging
void removeHistoryEntry(String entry) {
emit(
state.copyWith(
searchHistory: state.searchHistory.whereNot((element) => element == entry).toList(),
searchHistory: state.searchHistory
.whereNot((element) => element == entry)
.toList(),
),
);
_userAppState

View File

@@ -65,7 +65,10 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
Flexible(
child: Text(
S.of(context)!.searchDocuments,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).hintColor,
),
@@ -112,7 +115,9 @@ class _DocumentSearchBarState extends State<DocumentSearchBar> {
icon: GlobalSettingsBuilder(
builder: (context, settings) {
return ValueListenableBuilder(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) {
final account = box.get(settings.currentLoggedInUser!)!;
return UserAvatar(account: account);

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart';
part 'document_upload_state.dart';
@@ -12,9 +14,13 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final PaperlessDocumentsApi _documentApi;
final LabelRepository _labelRepository;
final Connectivity _connectivity;
DocumentUploadCubit(this._labelRepository, this._documentApi)
: super(const DocumentUploadState()) {
DocumentUploadCubit(
this._labelRepository,
this._documentApi,
this._connectivity,
) : super(const DocumentUploadState()) {
_labelRepository.addListener(
this,
onChanged: (labels) {
@@ -31,6 +37,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
Uint8List bytes, {
required String filename,
required String title,
required String userId,
int? documentType,
int? correspondent,
Iterable<int> tags = const [],

View File

@@ -3,10 +3,13 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:hive/hive.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.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/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';
@@ -42,10 +45,12 @@ class DocumentUploadPreparationPage extends StatefulWidget {
}) : super(key: key);
@override
State<DocumentUploadPreparationPage> createState() => _DocumentUploadPreparationPageState();
State<DocumentUploadPreparationPage> createState() =>
_DocumentUploadPreparationPageState();
}
class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparationPage> {
class _DocumentUploadPreparationPageState
extends State<DocumentUploadPreparationPage> {
static const fkFileName = "filename";
static final fileNameDateFormat = DateFormat("yyyy_MM_ddTHH_mm_ss");
@@ -72,7 +77,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
title: Text(S.of(context)!.prepareDocument),
bottom: _isUploadLoading
? const PreferredSize(
child: LinearProgressIndicator(), preferredSize: Size.fromHeight(4.0))
child: LinearProgressIndicator(),
preferredSize: Size.fromHeight(4.0))
: null,
),
floatingActionButton: Visibility(
@@ -93,7 +99,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
FormBuilderTextField(
autovalidateMode: AutovalidateMode.always,
name: DocumentModel.titleKey,
initialValue: widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
initialValue:
widget.title ?? "scan_${fileNameDateFormat.format(_now)}",
validator: (value) {
if (value?.trim().isEmpty ?? true) {
return S.of(context)!.thisFieldIsRequired;
@@ -105,18 +112,22 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
suffixIcon: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_formKey.currentState?.fields[DocumentModel.titleKey]?.didChange("");
_formKey.currentState?.fields[DocumentModel.titleKey]
?.didChange("");
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]?.didChange("");
_formKey.currentState?.fields[fkFileName]
?.didChange("");
}
},
),
errorText: _errors[DocumentModel.titleKey],
),
onChanged: (value) {
final String transformedValue = _formatFilename(value ?? '');
final String transformedValue =
_formatFilename(value ?? '');
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue);
_formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
}
},
),
@@ -131,10 +142,12 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
suffixText: widget.fileExtension,
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _formKey.currentState?.fields[fkFileName]?.didChange(''),
onPressed: () => _formKey.currentState?.fields[fkFileName]
?.didChange(''),
),
),
initialValue: widget.filename ?? "scan_${fileNameDateFormat.format(_now)}",
initialValue: widget.filename ??
"scan_${fileNameDateFormat.format(_now)}",
),
// Synchronize title and filename
SwitchListTile(
@@ -144,10 +157,13 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
() => _syncTitleAndFilename = value,
);
if (_syncTitleAndFilename) {
final String transformedValue = _formatFilename(
_formKey.currentState?.fields[DocumentModel.titleKey]?.value as String);
final String transformedValue = _formatFilename(_formKey
.currentState
?.fields[DocumentModel.titleKey]
?.value as String);
if (_syncTitleAndFilename) {
_formKey.currentState?.fields[fkFileName]?.didChange(transformedValue);
_formKey.currentState?.fields[fkFileName]
?.didChange(transformedValue);
}
}
},
@@ -172,7 +188,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
? IconButton(
icon: const Icon(Icons.close),
onPressed: () {
_formKey.currentState!.fields[DocumentModel.createdKey]
_formKey.currentState!
.fields[DocumentModel.createdKey]
?.didChange(null);
},
)
@@ -183,7 +200,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
LabelFormField<Correspondent>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddCorrespondentPage(initialName: initialName),
),
@@ -193,7 +211,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
options: state.correspondents,
prefixIcon: const Icon(Icons.person_outline),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.correspondent,
),
@@ -202,7 +221,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
LabelFormField<DocumentType>(
showAnyAssignedOption: false,
showNotAssignedOption: false,
addLabelPageBuilder: (initialName) => RepositoryProvider.value(
addLabelPageBuilder: (initialName) =>
RepositoryProvider.value(
value: context.read<LabelRepository>(),
child: AddDocumentTypePage(initialName: initialName),
),
@@ -212,7 +232,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
options: state.documentTypes,
prefixIcon: const Icon(Icons.description_outlined),
allowSelectUnassigned: true,
canCreateNewLabel: LocalUserAccount.current.paperlessUser.hasPermission(
canCreateNewLabel:
LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.add,
PermissionTarget.documentType,
),
@@ -252,8 +273,9 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
final tags = (fv[DocumentModel.tagsKey] as TagsQuery?)
?.whenOrNull(ids: (include, exclude) => include) ??
[];
final correspondent = (fv[DocumentModel.correspondentKey] as IdQueryParameter?)
?.whenOrNull(fromId: (id) => id);
final correspondent =
(fv[DocumentModel.correspondentKey] as IdQueryParameter?)
?.whenOrNull(fromId: (id) => id);
final asn = fv[DocumentModel.asnKey] as int?;
final taskId = await cubit.upload(
widget.fileBytes,
@@ -261,6 +283,9 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
_formKey.currentState?.value[fkFileName],
widget.fileExtension,
),
userId: Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!,
title: title,
documentType: docType,
correspondent: correspondent,
@@ -282,7 +307,8 @@ class _DocumentUploadPreparationPageState extends State<DocumentUploadPreparatio
setState(() => _errors = errors);
} catch (unknownError, stackTrace) {
debugPrint(unknownError.toString());
showErrorMessage(context, const PaperlessServerException.unknown(), stackTrace);
showErrorMessage(
context, const PaperlessServerException.unknown(), stackTrace);
} finally {
setState(() {
_isUploadLoading = false;

View File

@@ -14,7 +14,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'documents_cubit.g.dart';
part 'documents_state.dart';
class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBlocMixin {
class DocumentsCubit extends HydratedCubit<DocumentsState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;
@@ -40,7 +41,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
replace(document);
emit(
state.copyWith(
selection: state.selection.map((e) => e.id == document.id ? document : e).toList(),
selection: state.selection
.map((e) => e.id == document.id ? document : e)
.toList(),
),
);
},
@@ -48,7 +51,8 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
remove(document);
emit(
state.copyWith(
selection: state.selection.where((e) => e.id != document.id).toList(),
selection:
state.selection.where((e) => e.id != document.id).toList(),
),
);
},
@@ -82,7 +86,9 @@ class DocumentsCubit extends HydratedCubit<DocumentsState> with DocumentPagingBl
if (state.selectedIds.contains(model.id)) {
emit(
state.copyWith(
selection: state.selection.where((element) => element.id != model.id).toList(),
selection: state.selection
.where((element) => element.id != model.id)
.toList(),
),
);
} else {

View File

@@ -86,7 +86,8 @@ class DocumentsState extends DocumentPagingState {
);
}
factory DocumentsState.fromJson(Map<String, dynamic> json) => _$DocumentsStateFromJson(json);
factory DocumentsState.fromJson(Map<String, dynamic> json) =>
_$DocumentsStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentsStateToJson(this);
}

View File

@@ -22,7 +22,8 @@ class _DocumentViewState extends State<DocumentView> {
@override
Widget build(BuildContext context) {
final isInitialized = _controller != null && _currentPage != null && _totalPages != null;
final isInitialized =
_controller != null && _currentPage != null && _totalPages != null;
final canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!;
final canGoToPreviousPage = isInitialized && _currentPage! > 0;
return Scaffold(

View File

@@ -161,101 +161,96 @@ class _DocumentsPageState extends State<DocumentsPage>
}
return true;
},
child: Stack(
children: [
NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isEmpty) {
return const SliverSearchBar(floating: true);
} else {
return DocumentSelectionSliverAppBar(
state: state,
);
}
},
),
),
SliverOverlapAbsorber(
handle: tabBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isNotEmpty) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
}
return SliverPersistentHeader(
pinned: true,
delegate:
CustomizableSliverPersistentHeaderDelegate(
minExtent: kTextTabBarHeight,
maxExtent: kTextTabBarHeight,
child: ColoredTabBar(
tabBar: TabBar(
controller: _tabController,
tabs: [
Tab(text: S.of(context)!.documents),
Tab(text: S.of(context)!.views),
],
),
),
),
);
},
),
),
],
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverOverlapAbsorber(
handle: searchBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isEmpty) {
return const SliverSearchBar(floating: true);
} else {
return DocumentSelectionSliverAppBar(
state: state,
);
}
final desiredTab =
(metrics.pixels / metrics.maxScrollExtent)
.round();
if (metrics.axis == Axis.horizontal &&
_currentTab != desiredTab) {
setState(() => _currentTab = desiredTab);
}
return false;
},
child: TabBarView(
controller: _tabController,
physics: context
.watch<DocumentsCubit>()
.state
.selection
.isNotEmpty
? const NeverScrollableScrollPhysics()
: null,
children: [
Builder(
builder: (context) {
return _buildDocumentsTab(
connectivityState,
context,
);
},
),
),
SliverOverlapAbsorber(
handle: tabBarHandle,
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
builder: (context, state) {
if (state.selection.isNotEmpty) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);
}
return SliverPersistentHeader(
pinned: true,
delegate:
CustomizableSliverPersistentHeaderDelegate(
minExtent: kTextTabBarHeight,
maxExtent: kTextTabBarHeight,
child: ColoredTabBar(
tabBar: TabBar(
controller: _tabController,
tabs: [
Tab(text: S.of(context)!.documents),
Tab(text: S.of(context)!.views),
],
),
),
),
Builder(
builder: (context) {
return _buildSavedViewsTab(
connectivityState,
context,
);
},
),
],
),
);
},
),
),
],
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
final metrics = notification.metrics;
if (metrics.maxScrollExtent == 0) {
return true;
}
final desiredTab =
(metrics.pixels / metrics.maxScrollExtent).round();
if (metrics.axis == Axis.horizontal &&
_currentTab != desiredTab) {
setState(() => _currentTab = desiredTab);
}
return false;
},
child: TabBarView(
controller: _tabController,
physics: context
.watch<DocumentsCubit>()
.state
.selection
.isNotEmpty
? const NeverScrollableScrollPhysics()
: null,
children: [
Builder(
builder: (context) {
return _buildDocumentsTab(
connectivityState,
context,
);
},
),
Builder(
builder: (context) {
return _buildSavedViewsTab(
connectivityState,
context,
);
},
),
],
),
),
),
),
),

View File

@@ -43,7 +43,9 @@ class DocumentPreview extends StatelessWidget {
fit: fit,
alignment: alignment,
cacheKey: "thumb_${document.id}",
imageUrl: context.read<PaperlessDocumentsApi>().getThumbnailUrl(document.id),
imageUrl: context
.read<PaperlessDocumentsApi>()
.getThumbnailUrl(document.id),
errorWidget: (ctxt, msg, __) => Text(msg),
placeholder: (context, value) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,

View File

@@ -42,8 +42,9 @@ class DocumentDetailedItem extends DocumentItem {
padding.bottom -
kBottomNavigationBarHeight -
kToolbarHeight;
final maxHeight =
highlights != null ? min(600.0, availableHeight) : min(500.0, availableHeight);
final maxHeight = highlights != null
? min(600.0, availableHeight)
: min(500.0, availableHeight);
return Card(
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
child: InkWell(
@@ -114,8 +115,10 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
correspondent:
context.watch<LabelRepository>().state.correspondents[document.correspondent],
correspondent: context
.watch<LabelRepository>()
.state
.correspondents[document.correspondent],
),
],
).paddedLTRB(8, 0, 8, 4),
@@ -130,8 +133,10 @@ class DocumentDetailedItem extends DocumentItem {
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
documentType:
context.watch<LabelRepository>().state.documentTypes[document.documentType],
documentType: context
.watch<LabelRepository>()
.state
.documentTypes[document.documentType],
),
],
).paddedLTRB(8, 0, 8, 4),

View File

@@ -30,8 +30,9 @@ class DocumentGridItem extends DocumentItem {
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 1.0,
color:
isSelected ? Theme.of(context).colorScheme.inversePrimary : Theme.of(context).cardColor,
color: isSelected
? Theme.of(context).colorScheme.inversePrimary
: Theme.of(context).cardColor,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: _onTap,
@@ -74,7 +75,8 @@ class DocumentGridItem extends DocumentItem {
const Spacer(),
TagsWidget(
tags: document.tags
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
.map((e) =>
context.watch<LabelRepository>().state.tags[e]!)
.toList(),
isMultiLine: false,
onTagSelected: onTagSelected,

View File

@@ -22,14 +22,17 @@ class DocumentFilterForm extends StatefulWidget {
formKey.currentState?.save();
final v = formKey.currentState!.value;
return DocumentFilter(
correspondent: v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
DocumentFilter.initial.correspondent,
correspondent:
v[DocumentFilterForm.fkCorrespondent] as IdQueryParameter? ??
DocumentFilter.initial.correspondent,
documentType: v[DocumentFilterForm.fkDocumentType] as IdQueryParameter? ??
DocumentFilter.initial.documentType,
storagePath: v[DocumentFilterForm.fkStoragePath] as IdQueryParameter? ??
DocumentFilter.initial.storagePath,
tags: v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
query: v[DocumentFilterForm.fkQuery] as TextQuery? ?? DocumentFilter.initial.query,
tags:
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
query: v[DocumentFilterForm.fkQuery] as TextQuery? ??
DocumentFilter.initial.query,
created: (v[DocumentFilterForm.fkCreatedAt] as DateRangeQuery),
added: (v[DocumentFilterForm.fkAddedAt] as DateRangeQuery),
asnQuery: initialFilter.asnQuery,
@@ -134,12 +137,15 @@ class _DocumentFilterFormState extends State<DocumentFilterForm> {
}
void _checkQueryConstraints() {
final filter = DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
final filter =
DocumentFilterForm.assembleFilter(widget.formKey, widget.initialFilter);
if (filter.forceExtendedQuery) {
setState(() => _allowOnlyExtendedQuery = true);
final queryField = widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
final queryField =
widget.formKey.currentState?.fields[DocumentFilterForm.fkQuery];
queryField?.didChange(
(queryField.value as TextQuery?)?.copyWith(queryType: QueryType.extended),
(queryField.value as TextQuery?)
?.copyWith(queryType: QueryType.extended),
);
} else {
setState(() => _allowOnlyExtendedQuery = false);

View File

@@ -27,10 +27,12 @@ class SortFieldSelectionBottomSheet extends StatefulWidget {
});
@override
State<SortFieldSelectionBottomSheet> createState() => _SortFieldSelectionBottomSheetState();
State<SortFieldSelectionBottomSheet> createState() =>
_SortFieldSelectionBottomSheetState();
}
class _SortFieldSelectionBottomSheetState extends State<SortFieldSelectionBottomSheet> {
class _SortFieldSelectionBottomSheetState
extends State<SortFieldSelectionBottomSheet> {
late SortField? _currentSortField;
late SortOrder _currentSortOrder;

View File

@@ -33,12 +33,15 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
onPressed: () async {
final shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => BulkDeleteConfirmationDialog(state: state),
builder: (context) =>
BulkDeleteConfirmationDialog(state: state),
) ??
false;
if (shouldDelete) {
try {
await context.read<DocumentsCubit>().bulkDelete(state.selection);
await context
.read<DocumentsCubit>()
.bulkDelete(state.selection);
showSnackBar(
context,
S.of(context)!.documentsSuccessfullyDeleted,
@@ -62,21 +65,24 @@ class DocumentSelectionSliverAppBar extends StatelessWidget {
label: Text(S.of(context)!.correspondent),
avatar: const Icon(Icons.edit),
onPressed: () {
pushBulkEditCorrespondentRoute(context, selection: state.selection);
pushBulkEditCorrespondentRoute(context,
selection: state.selection);
},
).paddedOnly(left: 8, right: 4),
ActionChip(
label: Text(S.of(context)!.documentType),
avatar: const Icon(Icons.edit),
onPressed: () async {
pushBulkEditDocumentTypeRoute(context, selection: state.selection);
pushBulkEditDocumentTypeRoute(context,
selection: state.selection);
},
).paddedOnly(left: 8, right: 4),
ActionChip(
label: Text(S.of(context)!.storagePath),
avatar: const Icon(Icons.edit),
onPressed: () async {
pushBulkEditStoragePathRoute(context, selection: state.selection);
pushBulkEditStoragePathRoute(context,
selection: state.selection);
},
).paddedOnly(left: 8, right: 4),
_buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4),

View File

@@ -50,7 +50,9 @@ class SortDocumentsButton extends StatelessWidget {
initialSortField: state.filter.sortField,
initialSortOrder: state.filter.sortOrder,
onSubmit: (field, order) {
return context.read<DocumentsCubit>().updateCurrentFilter(
return context
.read<DocumentsCubit>()
.updateCurrentFilter(
(filter) => filter.copyWith(
sortField: field,
sortOrder: order,

View File

@@ -20,8 +20,10 @@ class EditCorrespondentPage extends StatelessWidget {
return EditLabelPage<Correspondent>(
label: correspondent,
fromJsonT: Correspondent.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeCorrespondent(label),
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceCorrespondent(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeCorrespondent(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.correspondent,

View File

@@ -18,8 +18,10 @@ class EditDocumentTypePage extends StatelessWidget {
child: EditLabelPage<DocumentType>(
label: documentType,
fromJsonT: DocumentType.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeDocumentType(label),
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceDocumentType(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeDocumentType(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.documentType,

View File

@@ -19,8 +19,10 @@ class EditStoragePathPage extends StatelessWidget {
child: EditLabelPage<StoragePath>(
label: storagePath,
fromJsonT: StoragePath.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeStoragePath(label),
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceStoragePath(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeStoragePath(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.storagePath,

View File

@@ -22,8 +22,10 @@ class EditTagPage extends StatelessWidget {
child: EditLabelPage<Tag>(
label: tag,
fromJsonT: Tag.fromJson,
onSubmit: (context, label) => context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) => context.read<EditLabelCubit>().removeTag(label),
onSubmit: (context, label) =>
context.read<EditLabelCubit>().replaceTag(label),
onDelete: (context, label) =>
context.read<EditLabelCubit>().removeTag(label),
canDelete: LocalUserAccount.current.paperlessUser.hasPermission(
PermissionAction.delete,
PermissionTarget.tag,

View File

@@ -6,8 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hive/hive.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/global/constants.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
@@ -42,12 +45,23 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
int _currentIndex = 0;
late 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!;
// For sharing files coming from outside the app while the app is still opened
_shareMediaSubscription = ReceiveSharingIntent.getMediaStream().listen(
(files) =>
ShareIntentQueue.instance.addAll(files, userId: currentUser));
// For sharing files coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((files) =>
ShareIntentQueue.instance.addAll(files, userId: currentUser));
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_listenForReceivedFiles();
});
@@ -59,7 +73,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
void _listenToInboxChanges() {
_inboxTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
_inboxTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
if (!mounted) {
timer.cancel();
} else {
@@ -93,17 +107,21 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_inboxTimer.cancel();
_shareMediaSubscription.cancel();
super.dispose();
}
void _listenForReceivedFiles() async {
if (ShareIntentQueue.instance.hasUnhandledFiles) {
await _handleReceivedFile(ShareIntentQueue.instance.pop()!);
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser!;
if (ShareIntentQueue.instance.userHasUnhandlesFiles(currentUser)) {
await _handleReceivedFile(ShareIntentQueue.instance.pop(currentUser)!);
}
ShareIntentQueue.instance.addListener(() async {
final queue = ShareIntentQueue.instance;
while (queue.hasUnhandledFiles) {
final file = queue.pop()!;
while (queue.userHasUnhandlesFiles(currentUser)) {
final file = queue.pop(currentUser)!;
await _handleReceivedFile(file);
}
});
@@ -115,7 +133,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
);
}
Future<void> _handleReceivedFile(SharedMediaFile file) async {
Future<void> _handleReceivedFile(final SharedMediaFile file) async {
SharedMediaFile mediaFile;
if (Platform.isIOS) {
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
@@ -128,7 +146,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
} else {
mediaFile = file;
}
debugPrint("Consuming media file: ${mediaFile.path}");
if (!_isFileTypeSupported(mediaFile)) {
Fluttertoast.showToast(
msg: translateError(context, ErrorCode.unsupportedFileFormat),
@@ -149,7 +167,7 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
}
final fileDescription = FileDescription.fromPath(mediaFile.path);
if (await File(mediaFile.path).exists()) {
final bytes = File(mediaFile.path).readAsBytesSync();
final bytes = await File(mediaFile.path).readAsBytes();
final result = await pushDocumentUploadPreparationPage(
context,
bytes: bytes,

View File

@@ -53,36 +53,42 @@ class HomeRoute extends StatelessWidget {
Config(
// Isolated cache per user.
localUserId,
fileService: DioFileService(context.read<SessionManager>().client),
fileService:
DioFileService(context.read<SessionManager>().client),
),
),
),
ProxyProvider<SessionManager, PaperlessDocumentsApi>(
update: (context, value, previous) => paperlessProviderFactory.createDocumentsApi(
update: (context, value, previous) =>
paperlessProviderFactory.createDocumentsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessLabelsApi>(
update: (context, value, previous) => paperlessProviderFactory.createLabelsApi(
update: (context, value, previous) =>
paperlessProviderFactory.createLabelsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessSavedViewsApi>(
update: (context, value, previous) => paperlessProviderFactory.createSavedViewsApi(
update: (context, value, previous) =>
paperlessProviderFactory.createSavedViewsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessServerStatsApi>(
update: (context, value, previous) => paperlessProviderFactory.createServerStatsApi(
update: (context, value, previous) =>
paperlessProviderFactory.createServerStatsApi(
value.client,
apiVersion: paperlessApiVersion,
),
),
ProxyProvider<SessionManager, PaperlessTasksApi>(
update: (context, value, previous) => paperlessProviderFactory.createTasksApi(
update: (context, value, previous) =>
paperlessProviderFactory.createTasksApi(
value.client,
apiVersion: paperlessApiVersion,
),
@@ -98,29 +104,41 @@ class HomeRoute extends StatelessWidget {
return MultiProvider(
providers: [
ProxyProvider<PaperlessLabelsApi, LabelRepository>(
update: (context, value, previous) => LabelRepository(value)..initialize(),
update: (context, value, previous) =>
LabelRepository(value)..initialize(),
),
ProxyProvider<PaperlessSavedViewsApi, SavedViewRepository>(
update: (context, value, previous) => SavedViewRepository(value)..initialize(),
update: (context, value, previous) =>
SavedViewRepository(value)..initialize(),
),
],
builder: (context, child) {
return MultiProvider(
providers: [
ProxyProvider3<PaperlessDocumentsApi, DocumentChangedNotifier, LabelRepository,
ProxyProvider3<
PaperlessDocumentsApi,
DocumentChangedNotifier,
LabelRepository,
DocumentsCubit>(
update: (context, docApi, notifier, labelRepo, previous) => DocumentsCubit(
update:
(context, docApi, notifier, labelRepo, previous) =>
DocumentsCubit(
docApi,
notifier,
labelRepo,
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState)
.get(currentLocalUserId)!,
)..reload(),
)..initialize(),
),
Provider(create: (context) => DocumentScannerCubit()),
ProxyProvider4<PaperlessDocumentsApi, PaperlessServerStatsApi, LabelRepository,
DocumentChangedNotifier, InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier, previous) =>
ProxyProvider4<
PaperlessDocumentsApi,
PaperlessServerStatsApi,
LabelRepository,
DocumentChangedNotifier,
InboxCubit>(
update: (context, docApi, statsApi, labelRepo, notifier,
previous) =>
InboxCubit(
docApi,
statsApi,
@@ -129,19 +147,22 @@ class HomeRoute extends StatelessWidget {
)..initialize(),
),
ProxyProvider<SavedViewRepository, SavedViewCubit>(
update: (context, savedViewRepo, previous) => SavedViewCubit(
update: (context, savedViewRepo, previous) =>
SavedViewCubit(
savedViewRepo,
)..initialize(),
),
),
ProxyProvider<LabelRepository, LabelCubit>(
update: (context, value, previous) => LabelCubit(value),
),
ProxyProvider<PaperlessTasksApi, TaskStatusCubit>(
update: (context, value, previous) => TaskStatusCubit(value),
update: (context, value, previous) =>
TaskStatusCubit(value),
),
if (paperlessApiVersion >= 3)
ProxyProvider<PaperlessUserApiV3, UserRepository>(
update: (context, value, previous) => UserRepository(value)..initialize(),
update: (context, value, previous) =>
UserRepository(value)..initialize(),
),
],
child: HomePage(paperlessApiVersion: paperlessApiVersion),

View File

@@ -30,7 +30,9 @@ class VerifyIdentityPage extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context)!.useTheConfiguredBiometricFactorToAuthenticate)
Text(S
.of(context)!
.useTheConfiguredBiometricFactorToAuthenticate)
.paddedSymmetrically(horizontal: 16),
const Icon(
Icons.fingerprint,
@@ -52,7 +54,9 @@ class VerifyIdentityPage extends StatelessWidget {
),
),
ElevatedButton(
onPressed: () => context.read<AuthenticationCubit>().restoreSessionState(),
onPressed: () => context
.read<AuthenticationCubit>()
.restoreSessionState(),
child: Text(S.of(context)!.verifyIdentity),
),
],

View File

@@ -129,8 +129,9 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
),
],
bottom: PreferredSize(
preferredSize:
!widget.allowOnlySelection ? const Size.fromHeight(32) : const Size.fromHeight(1),
preferredSize: !widget.allowOnlySelection
? const Size.fromHeight(32)
: const Size.fromHeight(1),
child: Column(
children: [
Divider(color: theme.colorScheme.outline),
@@ -233,7 +234,8 @@ class _FullscreenTagsFormState extends State<FullscreenTagsForm> {
yield _buildNotAssignedOption();
}
var matches = _options.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
var matches = _options
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
if (matches.isEmpty && widget.allowCreation) {
yield Text(S.of(context)!.noItemsFound);
yield TextButton(
@@ -299,7 +301,9 @@ class SelectableTagWidget extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
title: Text(tag.name),
trailing: excluded ? const Icon(Icons.close) : (selected ? const Icon(Icons.done) : null),
trailing: excluded
? const Icon(Icons.close)
: (selected ? const Icon(Icons.done) : null),
leading: CircleAvatar(
backgroundColor: tag.color,
child: (tag.isInboxTag)

View File

@@ -32,9 +32,9 @@ class TagsFormField extends StatelessWidget {
initialValue: initialValue,
builder: (field) {
final values = _generateOptions(context, field.value, field).toList();
final isEmpty =
(field.value is IdsTagsQuery && (field.value as IdsTagsQuery).include.isEmpty) ||
field.value == null;
final isEmpty = (field.value is IdsTagsQuery &&
(field.value as IdsTagsQuery).include.isEmpty) ||
field.value == null;
bool anyAssigned = field.value is AnyAssignedTagsQuery;
return OpenContainer<TagsQuery>(
middleColor: Theme.of(context).colorScheme.background,
@@ -59,7 +59,8 @@ class TagsFormField extends StatelessWidget {
height: 32,
child: ListView.separated(
scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) => const SizedBox(width: 4),
separatorBuilder: (context, index) =>
const SizedBox(width: 4),
itemBuilder: (context, index) => values[index],
itemCount: values.length,
),
@@ -99,11 +100,14 @@ class TagsFormField extends StatelessWidget {
} else {
final widgets = query.map(
ids: (value) => [
for (var inc in value.include) _buildTagIdQueryWidget(context, inc, field, false),
for (var exc in value.exclude) _buildTagIdQueryWidget(context, exc, field, true),
for (var inc in value.include)
_buildTagIdQueryWidget(context, inc, field, false),
for (var exc in value.exclude)
_buildTagIdQueryWidget(context, exc, field, true),
],
anyAssigned: (value) => [
for (var id in value.tagIds) _buildAnyAssignedTagWidget(context, id, field, value),
for (var id in value.tagIds)
_buildAnyAssignedTagWidget(context, id, field, value),
],
notAssigned: (value) => [_buildNotAssignedTagWidget(context, field)],
);
@@ -124,15 +128,19 @@ class TagsFormField extends StatelessWidget {
final tag = options[id]!;
return QueryTagChip(
onDeleted: () => field.didChange(formValue.copyWith(
include: formValue.include.whereNot((element) => element == id).toList(),
exclude: formValue.exclude.whereNot((element) => element == id).toList(),
include:
formValue.include.whereNot((element) => element == id).toList(),
exclude:
formValue.exclude.whereNot((element) => element == id).toList(),
)),
onSelected: allowExclude
? () {
if (formValue.include.contains(id)) {
field.didChange(
formValue.copyWith(
include: formValue.include.whereNot((element) => element == id).toList(),
include: formValue.include
.whereNot((element) => element == id)
.toList(),
exclude: [...formValue.exclude, id],
),
);
@@ -140,7 +148,9 @@ class TagsFormField extends StatelessWidget {
field.didChange(
formValue.copyWith(
include: [...formValue.include, id],
exclude: formValue.exclude.whereNot((element) => element == id).toList(),
exclude: formValue.exclude
.whereNot((element) => element == id)
.toList(),
),
);
}

View File

@@ -35,7 +35,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
!(initialValue?.isOnlyAssigned() ?? false) || showAnyAssignedOption,
),
assert(
!(initialValue?.isOnlyNotAssigned() ?? false) || showNotAssignedOption,
!(initialValue?.isOnlyNotAssigned() ?? false) ||
showNotAssignedOption,
),
assert((addNewLabelText != null) == (onCreateNewLabel != null));
@@ -43,7 +44,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState();
}
class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelForm<T>> {
class _FullscreenLabelFormState<T extends Label>
extends State<FullscreenLabelForm<T>> {
bool _showClearIcon = false;
final _textEditingController = TextEditingController();
final _focusNode = FocusNode();
@@ -133,9 +135,11 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
final highlight = AutocompleteHighlightedOption.of(context) == index;
final highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0,
@@ -191,7 +195,8 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
for (final option in widget.options.values) {
// Don't include the initial value in the selection
final initialValue = widget.initialValue;
if (initialValue is SetIdQueryParameter && option.id == initialValue.id) {
if (initialValue is SetIdQueryParameter &&
option.id == initialValue.id) {
continue;
}
yield IdQueryParameter.fromId(option.id!);
@@ -199,8 +204,8 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
}
} else {
// Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed.
final matches =
widget.options.values.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
final matches = widget.options.values
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
if (matches.isNotEmpty) {
for (final match in matches) {
yield IdQueryParameter.fromId(match.id!);
@@ -270,7 +275,9 @@ class _FullscreenLabelFormState<T extends Label> extends State<FullscreenLabelFo
selectedTileColor: Theme.of(context).focusColor,
title: Text(widget.options[id]!.name),
onTap: onTap,
enabled: widget.allowSelectUnassigned ? true : widget.options[id]!.documentCount != 0,
enabled: widget.allowSelectUnassigned
? true
: widget.options[id]!.documentCount != 0,
),
)!; // Never null, since we already return on unset before
}

View File

@@ -58,8 +58,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isEnabled =
options.values.any((e) => (e.documentCount ?? 0) > 0) || addLabelPageBuilder != null;
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
addLabelPageBuilder != null;
return FormBuilderField<IdQueryParameter>(
name: name,
initialValue: initialValue,
@@ -70,7 +70,9 @@ class LabelFormField<T extends Label> extends StatelessWidget {
text: _buildText(context, field.value),
);
final displayedSuggestions = suggestions
.whereNot((e) => e.id == field.value?.maybeWhen(fromId: (id) => id, orElse: () => -1))
.whereNot((e) =>
e.id ==
field.value?.maybeWhen(fromId: (id) => id, orElse: () => -1))
.toList();
return Column(
@@ -95,7 +97,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
suffixIcon: controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () => field.didChange(const IdQueryParameter.unset()),
onPressed: () =>
field.didChange(const IdQueryParameter.unset()),
)
: null,
),
@@ -110,7 +113,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
? (initialName) {
return Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) => addLabelPageBuilder!(initialName),
builder: (context) =>
addLabelPageBuilder!(initialName),
),
);
}
@@ -141,7 +145,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
scrollDirection: Axis.horizontal,
itemCount: displayedSuggestions.length,
itemBuilder: (context, index) {
final suggestion = displayedSuggestions.elementAt(index);
final suggestion =
displayedSuggestions.elementAt(index);
return ColoredChipWrapper(
child: ActionChip(
label: Text(suggestion.name),

View File

@@ -74,8 +74,11 @@ class LabelTabView<T extends Label> extends StatelessWidget {
name: l.name,
content: contentBuilder?.call(l) ??
Text(
translateMatchingAlgorithmName(context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false) ? ": ${l.match}" : ""),
translateMatchingAlgorithmName(
context, l.matchingAlgorithm) +
((l.match?.isNotEmpty ?? false)
? ": ${l.match}"
: ""),
maxLines: 2,
),
onOpenEditPage: canEdit ? onEdit : null,

View File

@@ -49,7 +49,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final apiVersion = await _getApiVersion(_sessionManager.client);
// Mark logged in user as currently active user.
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = localUserId;
await globalSettings.save();
@@ -64,11 +65,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async {
emit(const AuthenticationState.switchingAccounts());
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.currentLoggedInUser == localUserId) {
return;
}
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
if (!userAccountBox.containsKey(localUserId)) {
debugPrint("User $localUserId not yet registered.");
@@ -78,15 +81,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final account = userAccountBox.get(localUserId)!;
if (account.settings.isBiometricAuthenticationEnabled) {
final authenticated =
await _localAuthService.authenticateLocalUser("Authenticate to switch your account.");
final authenticated = await _localAuthService
.authenticateLocalUser("Authenticate to switch your account.");
if (!authenticated) {
debugPrint("User not authenticated.");
return;
}
}
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials,
(credentialsBox) async {
await withEncryptedBox<UserCredentials, void>(
HiveBoxes.localUserCredentials, (credentialsBox) async {
if (!credentialsBox.containsKey(localUserId)) {
await credentialsBox.close();
debugPrint("Invalid authentication for $localUserId");
@@ -108,7 +111,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await _updateRemoteUser(
_sessionManager,
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).get(localUserId)!,
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.get(localUserId)!,
apiVersion,
);
@@ -140,12 +144,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
Future<void> removeAccount(String userId) async {
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userAppStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
final userAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userAppStateBox =
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
await userAccountBox.delete(userId);
await userAppStateBox.delete(userId);
await withEncryptedBox<UserCredentials, void>(HiveBoxes.localUserCredentials, (box) {
await withEncryptedBox<UserCredentials, void>(
HiveBoxes.localUserCredentials, (box) {
box.delete(userId);
});
}
@@ -154,26 +161,29 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// Performs a conditional hydration based on the local authentication success.
///
Future<void> restoreSessionState() async {
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final localUserId = globalSettings.currentLoggedInUser;
if (localUserId == null) {
// If there is nothing to restore, we can quit here.
return;
}
final localUserAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccount = localUserAccountBox.get(localUserId)!;
if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
final localAuthSuccess =
await _localAuthService.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
final localAuthSuccess = await _localAuthService
.authenticateLocalUser("Authenticate to log back in"); //TODO: INTL
if (!localAuthSuccess) {
emit(const AuthenticationState.requriresLocalAuthentication());
return;
}
}
final authentication = await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) {
final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) {
return box.get(globalSettings.currentLoggedInUser!);
});
@@ -202,7 +212,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Future<void> logout() async {
await _resetExternalState();
final globalSettings = Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.currentLoggedInUser = null;
await globalSettings.save();
emit(const AuthenticationState.unauthenticated());
@@ -240,8 +251,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
authToken: token,
);
final userAccountBox = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userStateBox = Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
final userAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final userStateBox =
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
if (userAccountBox.containsKey(localUserId)) {
throw Exception("User already exists!");

View File

@@ -3,7 +3,8 @@ part of 'authentication_cubit.dart';
@freezed
class AuthenticationState with _$AuthenticationState {
const factory AuthenticationState.unauthenticated() = _Unauthenticated;
const factory AuthenticationState.requriresLocalAuthentication() = _RequiresLocalAuthentication;
const factory AuthenticationState.requriresLocalAuthentication() =
_RequiresLocalAuthentication;
const factory AuthenticationState.authenticated({
required String localUserId,
required int apiVersion,

View File

@@ -27,8 +27,8 @@ class OldAuthenticationState with EquatableMixin {
}) {
return OldAuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
showBiometricAuthenticationScreen:
showBiometricAuthenticationScreen ?? this.showBiometricAuthenticationScreen,
showBiometricAuthenticationScreen: showBiometricAuthenticationScreen ??
this.showBiometricAuthenticationScreen,
username: username ?? this.username,
fullName: fullName ?? this.fullName,
localUserId: localUserId ?? this.localUserId,

View File

@@ -54,7 +54,8 @@ class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
final localAccounts = Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localAccounts =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
return Scaffold(
resizeToAvoidBottomInset: false,
body: FormBuilder(
@@ -91,7 +92,9 @@ class _LoginPageState extends State<LoginPage> {
child: UserAccountListTile(
account: account,
onTap: () {
context.read<AuthenticationCubit>().switchAccount(account.id);
context
.read<AuthenticationCubit>()
.switchAccount(account.id);
},
),
);
@@ -126,14 +129,16 @@ class _LoginPageState extends State<LoginPage> {
final form = _formKey.currentState!.value;
ClientCertificate? clientCert;
final clientCertFormModel =
form[ClientCertificateFormField.fkClientCertificate] as ClientCertificateFormModel?;
form[ClientCertificateFormField.fkClientCertificate]
as ClientCertificateFormModel?;
if (clientCertFormModel != null) {
clientCert = ClientCertificate(
bytes: clientCertFormModel.bytes,
passphrase: clientCertFormModel.passphrase,
);
}
final credentials = form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
final credentials =
form[UserCredentialsFormField.fkCredentials] as LoginFormCredentials;
try {
await widget.onSubmit(
context,

View File

@@ -7,7 +7,6 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'obscured_input_text_form_field.dart';
class ClientCertificateFormField extends StatefulWidget {
@@ -20,10 +19,12 @@ class ClientCertificateFormField extends StatefulWidget {
}) : super(key: key);
@override
State<ClientCertificateFormField> createState() => _ClientCertificateFormFieldState();
State<ClientCertificateFormField> createState() =>
_ClientCertificateFormFieldState();
}
class _ClientCertificateFormFieldState extends State<ClientCertificateFormField> {
class _ClientCertificateFormFieldState
extends State<ClientCertificateFormField> {
File? _selectedFile;
@override
Widget build(BuildContext context) {
@@ -42,7 +43,8 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
return null;
},
builder: (field) {
final theme = Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
final theme =
Theme.of(context).copyWith(dividerColor: Colors.transparent); //new
return Theme(
data: theme,
child: ExpansionTile(
@@ -119,7 +121,8 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
);
}
Future<void> _onSelectFile(FormFieldState<ClientCertificateFormModel?> field) async {
Future<void> _onSelectFile(
FormFieldState<ClientCertificateFormModel?> field) async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: false,
);
@@ -128,13 +131,15 @@ class _ClientCertificateFormFieldState extends State<ClientCertificateFormField>
setState(() {
_selectedFile = file;
});
final changedValue = field.value?.copyWith(bytes: file.readAsBytesSync()) ??
ClientCertificateFormModel(bytes: file.readAsBytesSync());
final changedValue =
field.value?.copyWith(bytes: file.readAsBytesSync()) ??
ClientCertificateFormModel(bytes: file.readAsBytesSync());
field.didChange(changedValue);
}
}
Widget _buildSelectedFileText(FormFieldState<ClientCertificateFormModel?> field) {
Widget _buildSelectedFileText(
FormFieldState<ClientCertificateFormModel?> field) {
if (field.value == null) {
assert(_selectedFile == null);
return Text(

View File

@@ -67,7 +67,8 @@ class _ServerAddressFormFieldState extends State<ServerAddressFormField> {
.where((element) => element.contains(textEditingValue.text));
},
onSelected: (option) => _formatInput(),
fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) {
fieldViewBuilder:
(context, textEditingController, focusNode, onFieldSubmitted) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
@@ -146,9 +147,11 @@ class _AutocompleteOptions extends StatelessWidget {
onSelected(option);
},
child: Builder(builder: (BuildContext context) {
final bool highlight = AutocompleteHighlightedOption.of(context) == index;
final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5);
});
}

View File

@@ -14,7 +14,8 @@ class UserCredentialsFormField extends StatefulWidget {
}) : super(key: key);
@override
State<UserCredentialsFormField> createState() => _UserCredentialsFormFieldState();
State<UserCredentialsFormField> createState() =>
_UserCredentialsFormFieldState();
}
class _UserCredentialsFormFieldState extends State<UserCredentialsFormField> {

View File

@@ -38,8 +38,9 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
toolbarHeight: kToolbarHeight - 4,
title: Text(widget.titleString),
bottom: PreferredSize(
child:
_isCheckingConnection ? const LinearProgressIndicator() : const SizedBox(height: 4.0),
child: _isCheckingConnection
? const LinearProgressIndicator()
: const SizedBox(height: 4.0),
preferredSize: const Size.fromHeight(4.0),
),
),
@@ -69,8 +70,9 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
),
FilledButton(
child: Text(S.of(context)!.continueLabel),
onPressed:
_reachabilityStatus == ReachabilityStatus.reachable ? widget.onContinue : null,
onPressed: _reachabilityStatus == ReachabilityStatus.reachable
? widget.onContinue
: null,
),
],
),
@@ -85,12 +87,15 @@ class _ServerConnectionPageState extends State<ServerConnectionPage> {
final certForm = widget.formBuilderKey.currentState
?.getRawValue(ClientCertificateFormField.fkClientCertificate)
as ClientCertificateFormModel?;
final status = await context.read<ConnectivityStatusService>().isPaperlessServerReachable(
final status = await context
.read<ConnectivityStatusService>()
.isPaperlessServerReachable(
address ??
widget.formBuilderKey.currentState!
.getRawValue(ServerAddressFormField.fkServerAddress),
certForm != null
? ClientCertificate(bytes: certForm.bytes, passphrase: certForm.passphrase)
? ClientCertificate(
bytes: certForm.bytes, passphrase: certForm.passphrase)
: null,
);
setState(() {

View File

@@ -25,7 +25,8 @@ class _ServerLoginPageState extends State<ServerLoginPage> {
@override
Widget build(BuildContext context) {
final serverAddress = (widget.formBuilderKey.currentState
?.getRawValue(ServerAddressFormField.fkServerAddress) as String?)
?.getRawValue(ServerAddressFormField.fkServerAddress)
as String?)
?.replaceAll(RegExp(r'https?://'), '') ??
'';
return Scaffold(

View File

@@ -35,6 +35,10 @@ mixin DocumentPagingBlocMixin<State extends DocumentPagingState>
}
}
Future<void> initialize() {
return updateFilter();
}
///
/// Updates document filter and automatically reloads documents. Always resets page to 1.
/// Use [loadMore] to load more data.

View File

@@ -11,17 +11,17 @@ part 'saved_view_cubit.freezed.dart';
class SavedViewCubit extends Cubit<SavedViewState> {
final SavedViewRepository _savedViewRepository;
SavedViewCubit(this._savedViewRepository) : super(const SavedViewState.initial()) {
SavedViewCubit(this._savedViewRepository)
: super(const SavedViewState.initial()) {
_savedViewRepository.addListener(
this,
onChanged: (views) {
emit(
state.maybeWhen(
loaded: (savedViews) => (state as _SavedViewLoadedState).copyWith(
savedViews: views.savedViews,
),
orElse: () => state,
),
views.when(
initial: (savedViews) => emit(const SavedViewState.initial()),
loading: (savedViews) => emit(const SavedViewState.loading()),
loaded: (savedViews) =>
emit(SavedViewState.loaded(savedViews: savedViews)),
error: (savedViews) => emit(const SavedViewState.error()),
);
},
);
@@ -35,7 +35,7 @@ class SavedViewCubit extends Cubit<SavedViewState> {
return _savedViewRepository.delete(view);
}
Future<void> initialize() async {
Future<void> reload() async {
final views = await _savedViewRepository.findAll();
final values = {for (var element in views) element.id!: element};
if (!isClosed) {
@@ -47,8 +47,6 @@ class SavedViewCubit extends Cubit<SavedViewState> {
}
}
Future<void> reload() => initialize();
@override
Future<void> close() {
_savedViewRepository.removeListener(this);

View File

@@ -2,12 +2,12 @@ part of 'saved_view_cubit.dart';
@freezed
class SavedViewState with _$SavedViewState {
const factory SavedViewState.initial() = _SavedViewIntialState;
const factory SavedViewState.initial() = _Initial;
const factory SavedViewState.loading() = _SavedViewLoadingState;
const factory SavedViewState.loading() = _Loading;
const factory SavedViewState.loaded({required Map<int, SavedView> savedViews}) =
_SavedViewLoadedState;
const factory SavedViewState.loaded(
{required Map<int, SavedView> savedViews}) = _Loaded;
const factory SavedViewState.error() = _SavedViewErrorState;
const factory SavedViewState.error() = _Error;
}

View File

@@ -4,6 +4,7 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/navigation/push_routes.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_loading_sliver_list.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class SavedViewList extends StatelessWidget {
@@ -16,42 +17,40 @@ class SavedViewList extends StatelessWidget {
return BlocBuilder<SavedViewCubit, SavedViewState>(
builder: (context, state) {
return state.when(
initial: () => SliverToBoxAdapter(child: Container()),
loading: () => const SliverToBoxAdapter(
child: Center(
child: Text("Saved views loading..."), //TODO: INTL
),
),
initial: () => const SavedViewLoadingSliverList(),
loading: () => const SavedViewLoadingSliverList(),
loaded: (savedViews) {
if (savedViews.isEmpty) {
return SliverToBoxAdapter(
child: HintCard(
hintText: S.of(context)!.createViewsToQuicklyFilterYourDocuments,
hintText: S
.of(context)!
.createViewsToQuicklyFilterYourDocuments,
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final view = savedViews.values.elementAt(index);
return ListTile(
enabled: connectivity.isConnected,
title: Text(view.name),
subtitle: Text(
S.of(context)!.nFiltersSet(view.filterRules.length),
),
onTap: () {
pushSavedViewDetailsRoute(context, savedView: view);
},
);
},
childCount: savedViews.length,
),
return SliverList.builder(
itemBuilder: (context, index) {
final view = savedViews.values.elementAt(index);
return ListTile(
enabled: connectivity.isConnected,
title: Text(view.name),
subtitle: Text(
S.of(context)!.nFiltersSet(view.filterRules.length),
),
onTap: () {
pushSavedViewDetailsRoute(context, savedView: view);
},
);
},
itemCount: savedViews.length,
);
},
error: () => const Center(
child: Text(
"An error occurred while trying to load the saved views.",
error: () => const SliverToBoxAdapter(
child: Center(
child: Text(
"An error occurred while trying to load the saved views.",
),
),
),
);

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
class SavedViewLoadingSliverList extends StatelessWidget {
const SavedViewLoadingSliverList({super.key});
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemBuilder: (context, index) => ShimmerPlaceholder(
child: ListTile(
title: Align(
alignment: Alignment.centerLeft,
child: Container(
width: 300,
height: 14,
color: Colors.white,
),
),
subtitle: Align(
alignment: Alignment.centerLeft,
child: Container(
width: 150,
height: 12,
color: Colors.white,
),
),
),
),
);
}
}

View File

@@ -9,7 +9,8 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'saved_view_details_state.dart';
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState> with DocumentPagingBlocMixin {
class SavedViewDetailsCubit extends Cubit<SavedViewDetailsState>
with DocumentPagingBlocMixin {
@override
final PaperlessDocumentsApi api;

View File

@@ -26,11 +26,14 @@ class ManageAccountsPage extends StatelessWidget {
return const SizedBox.shrink();
}
return ValueListenableBuilder(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount)
.listenable(),
builder: (context, box, _) {
final userIds = box.keys.toList().cast<String>();
final otherAccounts = userIds
.whereNot((element) => element == globalSettings.currentLoggedInUser)
.whereNot(
(element) => element == globalSettings.currentLoggedInUser)
.toList();
return SimpleDialog(
insetPadding: const EdgeInsets.all(24),
@@ -68,10 +71,13 @@ class ManageAccountsPage extends StatelessWidget {
],
onSelected: (value) async {
if (value == 0) {
final currentUser = globalSettings.currentLoggedInUser!;
final currentUser =
globalSettings.currentLoggedInUser!;
await context.read<AuthenticationCubit>().logout();
Navigator.of(context).pop();
await context.read<AuthenticationCubit>().removeAccount(currentUser);
await context
.read<AuthenticationCubit>()
.removeAccount(currentUser);
}
},
),
@@ -89,7 +95,8 @@ class ManageAccountsPage extends StatelessWidget {
PopupMenuItem(
child: ListTile(
title: Text(S.of(context)!.switchAccount),
leading: const Icon(Icons.switch_account_rounded),
leading:
const Icon(Icons.switch_account_rounded),
),
value: 0,
),
@@ -150,7 +157,8 @@ class ManageAccountsPage extends StatelessWidget {
MaterialPageRoute(
builder: (context) => LoginPage(
titleString: S.of(context)!.addAccount,
onSubmit: (context, username, password, serverUrl, clientCertificate) async {
onSubmit: (context, username, password, serverUrl,
clientCertificate) async {
final userId = await context.read<AuthenticationCubit>().addAccount(
credentials: LoginFormCredentials(
username: username,
@@ -179,7 +187,8 @@ class ManageAccountsPage extends StatelessWidget {
}
}
void _onSwitchAccount(BuildContext context, String currentUser, String newUser) async {
void _onSwitchAccount(
BuildContext context, String currentUser, String newUser) async {
if (currentUser == newUser) return;
Navigator.of(context).pop();

View File

@@ -26,7 +26,9 @@ class SettingsPage extends StatelessWidget {
textAlign: TextAlign.center,
),
subtitle: FutureBuilder<PaperlessServerInformationModel>(
future: context.read<PaperlessServerStatsApi>().getServerInformation(),
future: context
.read<PaperlessServerStatsApi>()
.getServerInformation(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(

View File

@@ -14,8 +14,8 @@ class ClearCacheSetting extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
title: const Text("Clear downloaded files"), //TODO: INTL
subtitle:
const Text("Deletes all files downloaded from this app."), //TODO: INTL
subtitle: const Text(
"Deletes all files downloaded from this app."), //TODO: INTL
onTap: () async {
final dir = await FileService.downloadsDirectory;
final deletedSize = _dirSize(dir);

View File

@@ -32,7 +32,8 @@ class ColorSchemeOptionSetting extends StatelessWidget {
options: [
RadioOption(
value: ColorSchemeOption.classic,
label: translateColorSchemeOption(context, ColorSchemeOption.classic),
label: translateColorSchemeOption(
context, ColorSchemeOption.classic),
),
RadioOption(
value: ColorSchemeOption.dynamic,

View File

@@ -25,15 +25,18 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
options: [
RadioOption(
value: FileDownloadType.alwaysAsk,
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk),
label: _downloadFileTypeToString(
context, FileDownloadType.alwaysAsk),
),
RadioOption(
value: FileDownloadType.original,
label: _downloadFileTypeToString(context, FileDownloadType.original),
label: _downloadFileTypeToString(
context, FileDownloadType.original),
),
RadioOption(
value: FileDownloadType.archived,
label: _downloadFileTypeToString(context, FileDownloadType.archived),
label: _downloadFileTypeToString(
context, FileDownloadType.archived),
),
],
initialValue: settings.defaultDownloadType,
@@ -51,7 +54,8 @@ class DefaultDownloadFileTypeSetting extends StatelessWidget {
);
}
String _downloadFileTypeToString(BuildContext context, FileDownloadType type) {
String _downloadFileTypeToString(
BuildContext context, FileDownloadType type) {
switch (type) {
case FileDownloadType.original:
return S.of(context)!.original;

View File

@@ -25,15 +25,18 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
options: [
RadioOption(
value: FileDownloadType.alwaysAsk,
label: _downloadFileTypeToString(context, FileDownloadType.alwaysAsk),
label: _downloadFileTypeToString(
context, FileDownloadType.alwaysAsk),
),
RadioOption(
value: FileDownloadType.original,
label: _downloadFileTypeToString(context, FileDownloadType.original),
label: _downloadFileTypeToString(
context, FileDownloadType.original),
),
RadioOption(
value: FileDownloadType.archived,
label: _downloadFileTypeToString(context, FileDownloadType.archived),
label: _downloadFileTypeToString(
context, FileDownloadType.archived),
),
],
initialValue: settings.defaultShareType,
@@ -51,7 +54,8 @@ class DefaultShareFileTypeSetting extends StatelessWidget {
);
}
String _downloadFileTypeToString(BuildContext context, FileDownloadType type) {
String _downloadFileTypeToString(
BuildContext context, FileDownloadType type) {
switch (type) {
case FileDownloadType.original:
return S.of(context)!.original;

View File

@@ -10,7 +10,8 @@ class GlobalSettingsBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
valueListenable:
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).listenable(),
builder: (context, value, _) {
final settings = value.getValue()!;
return builder(context, settings);

View File

@@ -7,7 +7,8 @@ class LanguageSelectionSetting extends StatefulWidget {
const LanguageSelectionSetting({super.key});
@override
State<LanguageSelectionSetting> createState() => _LanguageSelectionSettingState();
State<LanguageSelectionSetting> createState() =>
_LanguageSelectionSettingState();
}
class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
@@ -27,7 +28,8 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
builder: (context, settings) {
return ListTile(
title: Text(S.of(context)!.language),
subtitle: Text(_languageOptions[settings.preferredLocaleSubtag]!.name),
subtitle:
Text(_languageOptions[settings.preferredLocaleSubtag]!.name),
onTap: () => showDialog<String>(
context: context,
builder: (_) => RadioSettingsDialog<String>(
@@ -39,7 +41,8 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
for (var language in _languageOptions.entries)
RadioOption(
value: language.key,
label: language.value.name + (language.value.isComplete ? '' : '*'),
label: language.value.name +
(language.value.isComplete ? '' : '*'),
),
],
initialValue: settings.preferredLocaleSubtag,

View File

@@ -50,7 +50,8 @@ class _RadioSettingsDialogState<T> extends State<RadioSettingsDialog<T>> {
mainAxisSize: MainAxisSize.min,
children: [
if (widget.descriptionText != null)
Text(widget.descriptionText!, style: Theme.of(context).textTheme.bodySmall),
Text(widget.descriptionText!,
style: Theme.of(context).textTheme.bodySmall),
...widget.options.map(_buildOptionListTile),
if (widget.footer != null) widget.footer!,
],

View File

@@ -12,7 +12,8 @@ class ThemeModeSetting extends StatelessWidget {
builder: (context, settings) {
return ListTile(
title: Text(S.of(context)!.appearance),
subtitle: Text(_mapThemeModeToLocalizedString(settings.preferredThemeMode, context)),
subtitle: Text(_mapThemeModeToLocalizedString(
settings.preferredThemeMode, context)),
onTap: () => showDialog<ThemeMode>(
context: context,
builder: (_) => RadioSettingsDialog<ThemeMode>(

View File

@@ -11,15 +11,18 @@ class UserAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final backgroundColor = Colors.primaries[account.id.hashCode % Colors.primaries.length];
final foregroundColor = backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
final backgroundColor =
Colors.primaries[account.id.hashCode % Colors.primaries.length];
final foregroundColor =
backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
return CircleAvatar(
child: Text((account.paperlessUser.fullName ?? account.paperlessUser.username)
.split(" ")
.take(2)
.map((e) => e.substring(0, 1))
.map((e) => e.toUpperCase())
.join("")),
child: Text(
(account.paperlessUser.fullName ?? account.paperlessUser.username)
.split(" ")
.take(2)
.map((e) => e.substring(0, 1))
.map((e) => e.toUpperCase())
.join("")),
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
);

View File

@@ -18,10 +18,12 @@ class UserAccountBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Box<LocalUserAccount>>(
valueListenable: Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
valueListenable:
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount).listenable(),
builder: (context, accountBox, _) {
final currentUser =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!.currentLoggedInUser;
final currentUser = Hive.box<GlobalSettings>(HiveBoxes.globalSettings)
.getValue()!
.currentLoggedInUser;
if (currentUser != null) {
final account = accountBox.get(currentUser);
return builder(context, account);

View File

@@ -4,33 +4,53 @@ import 'package:flutter/widgets.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
class ShareIntentQueue extends ChangeNotifier {
final Queue<SharedMediaFile> _queue = Queue();
final Map<String, Queue<SharedMediaFile>> _queues = {};
ShareIntentQueue._();
static final instance = ShareIntentQueue._();
void add(SharedMediaFile file) {
void add(
SharedMediaFile file, {
required String userId,
}) {
debugPrint("Adding received file to queue: ${file.path}");
_queue.add(file);
_getQueue(userId).add(file);
notifyListeners();
}
void addAll(Iterable<SharedMediaFile> files) {
void addAll(
Iterable<SharedMediaFile> files, {
required String userId,
}) {
debugPrint(
"Adding received files to queue: ${files.map((e) => e.path).join(",")}");
_queue.addAll(files);
_getQueue(userId).addAll(files);
notifyListeners();
}
SharedMediaFile? pop() {
if (hasUnhandledFiles) {
return _queue.removeFirst();
SharedMediaFile? pop(String userId) {
if (userHasUnhandlesFiles(userId)) {
return _getQueue(userId).removeFirst();
// Don't notify listeners, only when new item is added.
} else {
return null;
}
}
bool get hasUnhandledFiles => _queue.isNotEmpty;
Queue<SharedMediaFile> _getQueue(String userId) {
if (!_queues.containsKey(userId)) {
_queues[userId] = Queue<SharedMediaFile>();
}
return _queues[userId]!;
}
bool userHasUnhandlesFiles(String userId) => _getQueue(userId).isNotEmpty;
}
class UserAwareShareMediaFile {
final String userId;
final SharedMediaFile sharedFile;
UserAwareShareMediaFile(this.userId, this.sharedFile);
}

View File

@@ -7,7 +7,8 @@ import 'package:paperless_mobile/features/paged_document_view/cubit/paged_docume
part 'similar_documents_state.dart';
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState> with DocumentPagingBlocMixin {
class SimilarDocumentsCubit extends Cubit<SimilarDocumentsState>
with DocumentPagingBlocMixin {
final int documentId;
@override

View File

@@ -35,8 +35,10 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
@override
Widget build(BuildContext context) {
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
listenWhen: (previous, current) => !previous.isConnected && current.isConnected,
listener: (context, state) => context.read<SimilarDocumentsCubit>().initialize(),
listenWhen: (previous, current) =>
!previous.isConnected && current.isConnected,
listener: (context, state) =>
context.read<SimilarDocumentsCubit>().initialize(),
builder: (context, connectivity) {
return BlocBuilder<SimilarDocumentsCubit, SimilarDocumentsState>(
builder: (context, state) {
@@ -45,7 +47,9 @@ class _SimilarDocumentsViewState extends State<SimilarDocumentsView>
child: OfflineWidget(),
);
}
if (state.hasLoaded && !state.isLoading && state.documents.isEmpty) {
if (state.hasLoaded &&
!state.isLoading &&
state.documents.isEmpty) {
return SliverToBoxAdapter(
child: Center(
child: Text(S.of(context)!.noItemsFound),

View File

@@ -25,7 +25,8 @@ class UserAccountListTile extends StatelessWidget {
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (account.paperlessUser.fullName != null) Text(account.paperlessUser.fullName!),
if (account.paperlessUser.fullName != null)
Text(account.paperlessUser.fullName!),
Text(
account.serverUrl.replaceFirst(RegExp(r'https://?'), ''),
style: TextStyle(color: theme.colorScheme.primary),